import { useCallback, useEffect, useMemo, useState } from "react"; import { format, startOfDay } from "date-fns"; import { ptBR } from "date-fns/locale"; import { CalendarIcon, ChevronDown, LayoutDashboard, Loader2, RefreshCw, Sparkles } from "lucide-react"; import { motion } from "motion/react"; import { Cell, Legend, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { useAuthAccess } from "@/contexts/AuthAccessContext"; import { dashboardPontosPorClienteService, type DashboardPontosPorParceiro, } from "@/services/fechamento/dashboardPontosPorCliente"; import { fechamentoParceirosService, type ParceiroItem } from "@/services/fechamento/parceiros"; import { cn } from "@/lib/utils"; function formatPontos(valor: number): string { return valor.toLocaleString("pt-BR", { minimumFractionDigits: 0, maximumFractionDigits: 4 }); } /** Primeiro dia do mês civil (hora local). */ function inicioDoMesAtual(ref = new Date()): Date { return new Date(ref.getFullYear(), ref.getMonth(), 1); } /** Último dia do mês civil (hora local). */ function fimDoMesAtual(ref = new Date()): Date { return new Date(ref.getFullYear(), ref.getMonth() + 1, 0); } function dateToApiYmd(d: Date): string { return format(d, "yyyy-MM-dd"); } function sliceColor(i: number): string { const hue = (i * 47 + 198) % 360; return `hsl(${hue} 72% ${52 - (i % 4) * 4}%)`; } function DistribuicaoClientesDonut({ bloco }: { bloco: DashboardPontosPorParceiro }) { const total = bloco.totalParceiro > 0 ? bloco.totalParceiro : 1; const data = useMemo( () => bloco.linhas.map((l) => ({ name: l.cliente, value: l.pontos, })), [bloco.linhas], ); if (data.length === 0) { return (
Sem clientes neste período.
); } return (
(percent ?? 0) >= 0.06 ? `${String(name).slice(0, 14)}${String(name).length > 14 ? "…" : ""} ${(((percent ?? 0) as number) * 100).toFixed(0)}%` : "" } labelLine={false} > {data.map((_, i) => ( ))} { if (!active || !payload?.[0]) return null; const row = payload[0].payload as { name: string; value: number }; const pct = total > 0 ? (row.value / total) * 100 : 0; return (

{row.name}

{formatPontos(row.value)} pts · {pct.toFixed(1)}%

); }} /> ( {String(value).slice(0, 28)} )} wrapperStyle={{ paddingTop: 4 }} />
); } export default function DashboardPontos() { const { papel, me } = useAuthAccess(); const [parceiros, setParceiros] = useState([]); const [dataInicio, setDataInicio] = useState(() => inicioDoMesAtual()); const [dataFim, setDataFim] = useState(() => fimDoMesAtual()); /** `null` = todos os parceiros (sem filtro na API). */ const [selectedPartnerIds, setSelectedPartnerIds] = useState(null); const [parceirosMenuOpen, setParceirosMenuOpen] = useState(false); const [loading, setLoading] = useState(true); const [dados, setDados] = useState([]); const parceiroFixoSupervisor = papel === "supervisor" && me?.parceiroId; const selectionKey = selectedPartnerIds === null ? "all" : [...selectedPartnerIds].sort().join(","); const periodoKey = `${dataInicio.getTime()}-${dataFim.getTime()}`; const loadParceiros = useCallback(async () => { if (parceiroFixoSupervisor && me?.parceiroId) { try { const res = await fechamentoParceirosService.listarParceiros({ estaAtivo: "all", page: 1, perPage: 200, }); const um = res.data.find((p) => p.id === me.parceiroId); setParceiros(um ? [um] : []); } catch (e) { const message = e instanceof Error ? e.message : "Erro ao carregar parceiros."; toast.error(message); setParceiros([]); } return; } try { const lista = await fechamentoParceirosService.listarParceirosAtivos(); setParceiros(lista); } catch (e) { const message = e instanceof Error ? e.message : "Erro ao carregar parceiros."; toast.error(message); setParceiros([]); } }, [me?.parceiroId, parceiroFixoSupervisor]); const loadRelatorio = useCallback(async () => { try { setLoading(true); let parceiroIds: string[] | undefined; if (parceiroFixoSupervisor && me?.parceiroId) { parceiroIds = [me.parceiroId]; } else if (selectedPartnerIds !== null && selectedPartnerIds.length > 0) { parceiroIds = selectedPartnerIds; } if (startOfDay(dataFim) < startOfDay(dataInicio)) { toast.error("A data final não pode ser anterior à inicial."); setDados([]); return; } const res = await dashboardPontosPorClienteService.relatorioPontosPorCliente({ dataInicio: dateToApiYmd(dataInicio), dataFim: dateToApiYmd(dataFim), parceiroIds, }); setDados(res); } catch (e) { const message = e instanceof Error ? e.message : "Erro ao carregar dashboard."; toast.error(message); setDados([]); } finally { setLoading(false); } }, [dataFim, dataInicio, me?.parceiroId, parceiroFixoSupervisor, periodoKey, selectionKey]); useEffect(() => { void loadParceiros(); }, [loadParceiros]); useEffect(() => { void loadRelatorio(); }, [loadRelatorio, me?.parceiroId, papel]); const allParceiroIds = useMemo(() => parceiros.map((p) => p.id), [parceiros]); const parceirosSelecaoLabel = useMemo(() => { if (parceiroFixoSupervisor) { const p = parceiros[0]; if (!p) return "Parceiro vinculado"; return p.codinome?.trim() ? `${p.nome} (${p.codinome.trim()})` : p.nome; } if (selectedPartnerIds === null) return "Todos os parceiros"; if (selectedPartnerIds.length === 1) { const p = parceiros.find((x) => x.id === selectedPartnerIds[0]); if (!p) return "1 parceiro"; return p.codinome?.trim() ? `${p.nome} (${p.codinome.trim()})` : p.nome; } return `${selectedPartnerIds.length} parceiros selecionados`; }, [parceiroFixoSupervisor, parceiros, selectedPartnerIds]); const toggleParceiro = useCallback( (id: string, checked: boolean) => { setSelectedPartnerIds((prev) => { if (parceiroFixoSupervisor) return prev; if (prev === null) { if (checked) return null; const next = allParceiroIds.filter((x) => x !== id); if (next.length === 0) { toast.error("Selecione ao menos um parceiro."); return prev; } return next; } const set = new Set(prev); if (checked) set.add(id); else set.delete(id); const arr = Array.from(set); if (arr.length === 0) { toast.error("Selecione ao menos um parceiro."); return prev; } if (arr.length === allParceiroIds.length) return null; return arr; }); }, [allParceiroIds, parceiroFixoSupervisor], ); const setTodosParceiros = useCallback(() => { if (!parceiroFixoSupervisor) setSelectedPartnerIds(null); }, [parceiroFixoSupervisor]); const isParceiroChecked = (id: string) => selectedPartnerIds === null || (selectedPartnerIds?.includes(id) ?? false); return (
Intelligence Score

Dashboard

Distribuição de pontos por cliente — visão rápida do que cada parceiro entregou no período.

Filtros
{ if (d) setDataInicio(d); }} locale={ptBR} defaultMonth={dataInicio} initialFocus className="pointer-events-auto p-3" />
{ if (d) setDataFim(d); }} locale={ptBR} defaultMonth={dataFim} disabled={(date) => date < startOfDay(dataInicio)} initialFocus className="pointer-events-auto p-3" />
{!parceiroFixoSupervisor ? (
Selecionar parceiros
{ if (c === true) { setTodosParceiros(); return; } if (allParceiroIds.length <= 1) return; setSelectedPartnerIds([allParceiroIds[0]]); toast.info("Filtro por um parceiro. Use «Todos» para voltar a exibir todos."); }} />
{parceiros.map((p) => (
toggleParceiro(p.id, c === true)} />
))}
) : (
)}
{loading && dados.length === 0 ? (
) : dados.length === 0 ? ( Nenhum dado para os filtros atuais. Ajuste o período ou os parceiros. ) : ( {dados.map((bloco, idx) => (
{bloco.parceiroNome} {formatPontos(bloco.totalParceiro)} pts
))}
)}
); }