feat: implement customizable funnel stages per tenant
- Modified attendances.funnel_stage in DB from ENUM to VARCHAR. - Created tenant_funnels table and backend API routes to manage custom stages. - Added /admin/funnels page for Admins/Managers to create, edit, order, and color-code their funnel stages. - Updated Dashboard, UserDetail, and AttendanceDetail to fetch and render dynamic funnel stages instead of hardcoded enums. - Added defensive checks and logging to GET /users/:idOrSlug to fix sporadic 500 errors during impersonation handoffs.
This commit is contained in:
@@ -5,9 +5,9 @@ import {
|
||||
import {
|
||||
Users, Clock, Phone, TrendingUp, Filter
|
||||
} from 'lucide-react';
|
||||
import { getAttendances, getUsers, getTeams, getUserById } from '../services/dataService';
|
||||
import { getAttendances, getUsers, getTeams, getUserById, getFunnels } from '../services/dataService';
|
||||
import { COLORS } from '../constants';
|
||||
import { Attendance, DashboardFilter, FunnelStage, User } from '../types';
|
||||
import { Attendance, DashboardFilter, FunnelStage, User, FunnelStageDef } from '../types';
|
||||
import { KPICard } from '../components/KPICard';
|
||||
import { DateRangePicker } from '../components/DateRangePicker';
|
||||
import { SellersTable } from '../components/SellersTable';
|
||||
@@ -28,6 +28,7 @@ export const Dashboard: React.FC = () => {
|
||||
const [prevData, setPrevData] = useState<Attendance[]>([]);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [teams, setTeams] = useState<any[]>([]);
|
||||
const [funnelDefs, setFunnelDefs] = useState<FunnelStageDef[]>([]);
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
|
||||
const [filters, setFilters] = useState<DashboardFilter>({
|
||||
@@ -55,12 +56,13 @@ export const Dashboard: React.FC = () => {
|
||||
const prevEnd = new Date(filters.dateRange.start.getTime());
|
||||
const prevFilters = { ...filters, dateRange: { start: prevStart, end: prevEnd } };
|
||||
|
||||
// Fetch users, attendances, teams and current user in parallel
|
||||
const [fetchedUsers, fetchedData, prevFetchedData, fetchedTeams, me] = await Promise.all([
|
||||
// Fetch users, attendances, teams, funnels and current user in parallel
|
||||
const [fetchedUsers, fetchedData, prevFetchedData, fetchedTeams, fetchedFunnels, me] = await Promise.all([
|
||||
getUsers(tenantId),
|
||||
getAttendances(tenantId, filters),
|
||||
getAttendances(tenantId, prevFilters),
|
||||
getTeams(tenantId),
|
||||
getFunnels(tenantId),
|
||||
storedUserId ? getUserById(storedUserId) : null
|
||||
]);
|
||||
|
||||
@@ -68,6 +70,7 @@ export const Dashboard: React.FC = () => {
|
||||
setData(fetchedData);
|
||||
setPrevData(prevFetchedData);
|
||||
setTeams(fetchedTeams);
|
||||
setFunnelDefs(fetchedFunnels.sort((a, b) => a.order_index - b.order_index));
|
||||
if (me) setCurrentUser(me);
|
||||
} catch (error) {
|
||||
console.error("Error loading dashboard data:", error);
|
||||
@@ -128,6 +131,20 @@ export const Dashboard: React.FC = () => {
|
||||
|
||||
// --- Chart Data: Funnel ---
|
||||
const funnelData = useMemo(() => {
|
||||
const counts = data.reduce((acc, curr) => {
|
||||
acc[curr.funnel_stage] = (acc[curr.funnel_stage] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
if (funnelDefs.length > 0) {
|
||||
return funnelDefs.map(stage => ({
|
||||
name: stage.name,
|
||||
value: counts[stage.name] || 0,
|
||||
color: stage.color_class.split(' ')[0].replace('bg-', '') // Extract base color name for Recharts
|
||||
}));
|
||||
}
|
||||
|
||||
// Fallback if funnels aren't loaded yet
|
||||
const stagesOrder = [
|
||||
FunnelStage.NO_CONTACT,
|
||||
FunnelStage.IDENTIFICATION,
|
||||
@@ -135,17 +152,11 @@ export const Dashboard: React.FC = () => {
|
||||
FunnelStage.WON,
|
||||
FunnelStage.LOST
|
||||
];
|
||||
|
||||
const counts = data.reduce((acc, curr) => {
|
||||
acc[curr.funnel_stage] = (acc[curr.funnel_stage] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return stagesOrder.map(stage => ({
|
||||
name: stage,
|
||||
value: counts[stage] || 0
|
||||
}));
|
||||
}, [data]);
|
||||
}, [data, funnelDefs]);
|
||||
|
||||
// --- Chart Data: Origin ---
|
||||
const originData = useMemo(() => {
|
||||
@@ -266,17 +277,18 @@ export const Dashboard: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<select
|
||||
<select
|
||||
className="bg-zinc-50 dark:bg-dark-bg border border-zinc-200 dark:border-dark-border px-3 py-2 rounded-lg text-sm text-zinc-700 dark:text-zinc-200 outline-none focus:ring-2 focus:ring-brand-yellow/20 cursor-pointer hover:border-zinc-300 dark:hover:border-dark-border transition-all"
|
||||
value={filters.funnelStage}
|
||||
onChange={(e) => handleFilterChange('funnelStage', e.target.value)}
|
||||
>
|
||||
<option value="all">Todas Etapas</option>
|
||||
{Object.values(FunnelStage).map(stage => (
|
||||
{funnelDefs.length > 0 ? funnelDefs.map(stage => (
|
||||
<option key={stage.id} value={stage.name}>{stage.name}</option>
|
||||
)) : Object.values(FunnelStage).map(stage => (
|
||||
<option key={stage} value={stage}>{stage}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
className="bg-zinc-50 dark:bg-dark-bg border border-zinc-200 dark:border-dark-border px-3 py-2 rounded-lg text-sm text-zinc-700 dark:text-zinc-200 outline-none focus:ring-2 focus:ring-brand-yellow/20 cursor-pointer hover:border-zinc-300 dark:hover:border-dark-border transition-all"
|
||||
value={filters.origin}
|
||||
|
||||
Reference in New Issue
Block a user