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:
Cauê Faleiros
2026-03-13 10:25:23 -03:00
parent 1d49161a05
commit 7ab54053db
18 changed files with 588 additions and 33 deletions

View File

@@ -1,13 +1,14 @@
import React, { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { getAttendanceById, getUserById } from '../services/dataService';
import { Attendance, User, FunnelStage } from '../types';
import { getAttendanceById, getUserById, getFunnels } from '../services/dataService';
import { Attendance, User, FunnelStage, FunnelStageDef } from '../types';
import { ArrowLeft, CheckCircle2, AlertCircle, Clock, Calendar, MessageSquare, ShoppingBag, Award, TrendingUp } from 'lucide-react';
export const AttendanceDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const [data, setData] = useState<Attendance | undefined>();
const [agent, setAgent] = useState<User | undefined>();
const [funnelDefs, setFunnelDefs] = useState<FunnelStageDef[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
@@ -15,8 +16,15 @@ export const AttendanceDetail: React.FC = () => {
if (id) {
setLoading(true);
try {
const att = await getAttendanceById(id);
const tenantId = localStorage.getItem('ctms_tenant_id') || '';
const [att, fDefs] = await Promise.all([
getAttendanceById(id),
getFunnels(tenantId)
]);
setData(att);
setFunnelDefs(fDefs);
if (att) {
const u = await getUserById(att.user_id);
setAgent(u);
@@ -34,7 +42,10 @@ export const AttendanceDetail: React.FC = () => {
if (loading) return <div className="p-12 text-center text-zinc-400 dark:text-dark-muted transition-colors">Carregando detalhes...</div>;
if (!data) return <div className="p-12 text-center text-zinc-500 dark:text-dark-muted transition-colors">Registro de atendimento não encontrado</div>;
const getStageColor = (stage: FunnelStage) => {
const getStageColor = (stage: string) => {
const def = funnelDefs.find(f => f.name === stage);
if (def) return def.color_class;
switch (stage) {
case FunnelStage.WON: return 'text-green-700 bg-green-50 border-green-200 dark:text-green-400 dark:bg-green-900/30 dark:border-green-800';
case FunnelStage.LOST: return 'text-red-700 bg-red-50 border-red-200 dark:text-red-400 dark:bg-red-900/30 dark:border-red-800';