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:
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { getAttendances, getUserById } from '../services/dataService';
|
||||
import { Attendance, User, FunnelStage, DashboardFilter } from '../types';
|
||||
import { getAttendances, getUserById, getFunnels } from '../services/dataService';
|
||||
import { Attendance, User, FunnelStage, DashboardFilter, FunnelStageDef } from '../types';
|
||||
import { ArrowLeft, Mail, Phone, Clock, MessageSquare, ChevronLeft, ChevronRight, Eye, Filter } from 'lucide-react';
|
||||
import { DateRangePicker } from '../components/DateRangePicker';
|
||||
|
||||
@@ -11,6 +11,7 @@ export const UserDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [user, setUser] = useState<User | undefined>();
|
||||
const [attendances, setAttendances] = useState<Attendance[]>([]);
|
||||
const [funnelDefs, setFunnelDefs] = useState<FunnelStageDef[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [filters, setFilters] = useState<DashboardFilter>({
|
||||
@@ -31,11 +32,15 @@ export const UserDetail: React.FC = () => {
|
||||
setUser(u);
|
||||
|
||||
if (u && tenantId) {
|
||||
const data = await getAttendances(tenantId, {
|
||||
...filters,
|
||||
userId: id
|
||||
});
|
||||
const [data, funnels] = await Promise.all([
|
||||
getAttendances(tenantId, {
|
||||
...filters,
|
||||
userId: id
|
||||
}),
|
||||
getFunnels(tenantId)
|
||||
]);
|
||||
setAttendances(data);
|
||||
setFunnelDefs(funnels);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading user details", error);
|
||||
@@ -66,7 +71,10 @@ export const UserDetail: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getStageBadgeColor = (stage: FunnelStage) => {
|
||||
const getStageBadgeColor = (stage: string) => {
|
||||
const def = funnelDefs.find(f => f.name === stage);
|
||||
if (def) return def.color_class;
|
||||
|
||||
switch (stage) {
|
||||
case FunnelStage.WON: return 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800';
|
||||
case FunnelStage.LOST: return 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800';
|
||||
@@ -127,17 +135,18 @@ export const UserDetail: React.FC = () => {
|
||||
onChange={(range) => handleFilterChange('dateRange', range)}
|
||||
/>
|
||||
|
||||
<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-zinc-700 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-zinc-700 transition-all"
|
||||
value={filters.origin}
|
||||
|
||||
Reference in New Issue
Block a user