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.
void loadRelatorio()}
>
Atualizar
Filtros
Data inicial
{format(dataInicio, "dd/MM/yyyy", { locale: ptBR })}
{
if (d) setDataInicio(d);
}}
locale={ptBR}
defaultMonth={dataInicio}
initialFocus
className="pointer-events-auto p-3"
/>
Data final
{format(dataFim, "dd/MM/yyyy", { locale: ptBR })}
{
if (d) setDataFim(d);
}}
locale={ptBR}
defaultMonth={dataFim}
disabled={(date) => date < startOfDay(dataInicio)}
initialFocus
className="pointer-events-auto p-3"
/>
{!parceiroFixoSupervisor ? (
Parceiros
{parceirosSelecaoLabel}
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.");
}}
/>
Todos os parceiros
{parceiros.map((p) => (
toggleParceiro(p.id, c === true)}
/>
{p.codinome?.trim() ? `${p.nome} (${p.codinome.trim()})` : p.nome}
))}
) : (
Parceiro
{parceiros.map((p) => (
{p.codinome?.trim() ? `${p.nome} (${p.codinome.trim()})` : p.nome}
))}
{parceiros.length === 0 && me?.parceiroId ? (
Parceiro vinculado
) : null}
)}
void loadRelatorio()}>
Aplicar
{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
))}
)}
);
}