import { useState } from 'react'; import { Alert, Badge, Button, Card, Col, Flex, Row, Skeleton, Space, Statistic, Table, Tag, Typography, } from 'antd'; import type { TableColumnsType } from 'antd'; import { ArrowLeftOutlined, ExclamationCircleOutlined, FireOutlined, RiseOutlined, TeamOutlined, WarningOutlined, } from '@ant-design/icons'; import { useNavigate } from '@tanstack/react-router'; import type { CarteiraCliente } from '@sar/api-interface'; import { useReportCarteira } from '../../lib/queries/reports'; const { Title, Text } = Typography; function fmt(v: number | string): string { return Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); } function fmtDate(v: string | null | undefined): string { if (!v) return '—'; return new Date(v + 'T00:00:00').toLocaleDateString('pt-BR'); } type Filtro = 'todos' | 'ativos' | 'risco' | 'inativos' | 'semPedido'; function statusBadge(c: CarteiraCliente) { if (c.totalPedidos === 0) return Sem pedido} />; if (c.diasSemPedido != null && c.diasSemPedido >= 60) return ( Inativo 60d+} /> ); if (c.diasSemPedido != null && c.diasSemPedido >= 30) return ( Em risco 30d+} /> ); return ( Ativo} /> ); } function InsightCard({ icon, cor, titulo, descricao, }: { icon: React.ReactNode; cor: string; titulo: string; descricao: string; }) { return ( {icon} {/* minWidth: 0 — flex item precisa poder encolher; titulo/descricao embutem razão social e valores que podem não ter ponto de quebra. */}
{titulo}
{descricao}
); } export function CarteirePage() { const navigate = useNavigate(); const { data, isLoading, isError, error } = useReportCarteira(); const [filtro, setFiltro] = useState('todos'); const clientes = data?.data ?? []; const ativos = clientes.filter( (c) => c.totalPedidos > 0 && (c.diasSemPedido == null || c.diasSemPedido < 30), ); const emRisco = clientes.filter( (c) => c.diasSemPedido != null && c.diasSemPedido >= 30 && c.diasSemPedido < 60, ); const inativos = clientes.filter((c) => c.diasSemPedido != null && c.diasSemPedido >= 60); const semPedido = clientes.filter((c) => c.totalPedidos === 0); const filtered = (() => { if (filtro === 'ativos') return ativos; if (filtro === 'risco') return emRisco; if (filtro === 'inativos') return inativos; if (filtro === 'semPedido') return semPedido; return clientes; })(); // Insights calculados const top5Fat = clientes .slice() .sort((a, b) => Number(b.faturamentoTotal) - Number(a.faturamentoTotal)) .slice(0, 5); const top5Pct = top5Fat.reduce((acc, c) => acc + c.participacaoPct, 0); const maiorCliEmRisco = emRisco.sort( (a, b) => Number(b.faturamentoTotal) - Number(a.faturamentoTotal), )[0]; const maiorCliInativo = inativos.sort( (a, b) => Number(b.faturamentoTotal) - Number(a.faturamentoTotal), )[0]; const insights: { icon: React.ReactNode; cor: string; titulo: string; descricao: string }[] = []; if (top5Pct >= 60) { insights.push({ icon: , cor: '#f59e0b', titulo: `Concentração: top 5 clientes = ${top5Pct.toFixed(0)}% do faturamento`, descricao: 'Risco alto de dependência. Considere expandir outros clientes da carteira.', }); } if (maiorCliEmRisco) { const nome = maiorCliEmRisco.razao ?? maiorCliEmRisco.nome ?? `Cliente ${maiorCliEmRisco.idCliente}`; insights.push({ icon: , cor: '#f59e0b', titulo: `${nome} está ${maiorCliEmRisco.diasSemPedido}d sem comprar`, descricao: `Faturamento histórico: ${fmt(maiorCliEmRisco.faturamentoTotal)} — priorize o contato.`, }); } if (maiorCliInativo) { const nome = maiorCliInativo.razao ?? maiorCliInativo.nome ?? `Cliente ${maiorCliInativo.idCliente}`; insights.push({ icon: , cor: '#ef4444', titulo: `${nome} inativo há ${maiorCliInativo.diasSemPedido}d`, descricao: `Era um cliente de ${fmt(maiorCliInativo.faturamentoTotal)} — vale reativar.`, }); } if (semPedido.length > 0) { insights.push({ icon: , cor: '#003B8E', titulo: `${semPedido.length} cliente${semPedido.length > 1 ? 's' : ''} sem nenhum pedido`, descricao: 'Potencial inexplorado na sua carteira. Primeira visita pode gerar receita nova.', }); } const columns: TableColumnsType = [ { title: 'Cliente', key: 'cliente', render: (_: unknown, c: CarteiraCliente) => ( navigate({ to: '/clientes/$id', params: { id: String(c.idCliente) } })} > {c.razao ?? c.nome ?? `Cód. ${c.idCliente}`} {c.razao && c.nome && ( {c.nome} )} ), }, { title: 'Status', key: 'status', width: 140, render: (_: unknown, c: CarteiraCliente) => statusBadge(c), }, { title: 'Último Pedido', dataIndex: 'ultimoPedido', width: 130, render: (v: string | null) => ( {fmtDate(v)} ), sorter: (a, b) => (a.ultimoPedido ?? '').localeCompare(b.ultimoPedido ?? ''), }, { title: 'Dias Sem Compra', dataIndex: 'diasSemPedido', width: 140, align: 'right' as const, render: (v: number | null) => v != null ? ( = 60 ? '#ef4444' : v >= 30 ? '#f59e0b' : '#22c55e' }} > {v}d ) : ( ), sorter: (a, b) => (a.diasSemPedido ?? 9999) - (b.diasSemPedido ?? 9999), }, { title: 'Faturado (total)', dataIndex: 'faturamentoTotal', width: 150, align: 'right' as const, render: (v: string) => ( {fmt(v)} ), sorter: (a, b) => Number(a.faturamentoTotal) - Number(b.faturamentoTotal), defaultSortOrder: 'descend' as const, }, { title: 'Participação', dataIndex: 'participacaoPct', width: 110, align: 'right' as const, render: (v: number) => ( = 10 ? 'blue' : v >= 5 ? 'geekblue' : 'default'} style={{ borderRadius: 20, fontWeight: 600 }} > {v.toFixed(1)}% ), sorter: (a, b) => a.participacaoPct - b.participacaoPct, }, { title: 'Ticket Médio', dataIndex: 'ticketMedio', width: 130, align: 'right' as const, render: (v: string) => ( {fmt(v)} ), sorter: (a, b) => Number(a.ticketMedio) - Number(b.ticketMedio), }, { title: 'Pedidos', dataIndex: 'totalPedidos', width: 80, align: 'right' as const, render: (v: number) => {v}, sorter: (a, b) => a.totalPedidos - b.totalPedidos, }, ]; const filtros: { key: Filtro; label: string; count: number; cor: string }[] = [ { key: 'todos', label: 'Todos', count: clientes.length, cor: '#64748b' }, { key: 'ativos', label: 'Ativos', count: ativos.length, cor: '#22c55e' }, { key: 'risco', label: 'Em risco (30d+)', count: emRisco.length, cor: '#f59e0b' }, { key: 'inativos', label: 'Inativos (60d+)', count: inativos.length, cor: '#ef4444' }, { key: 'semPedido', label: 'Sem pedido', count: semPedido.length, cor: '#94A3B8' }, ]; return (
{/* Cabeçalho */}
); }