import { useMemo, useState } from 'react'; import { Button, Card, Empty, Flex, Modal, Progress, Table, Tag, Tooltip, Typography } from 'antd'; import type { TableColumnsType } from 'antd'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faMapLocationDot, faUserTie } from '@fortawesome/free-solid-svg-icons'; import type { UfFaturamento, UfRep } from '@sar/api-interface'; import brazilMap from '@svg-maps/brazil'; const { Text } = Typography; function fmt(v: number): string { return v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); } // R$ compacto para a lista lateral (R$ 12,4 mil / R$ 1,2 mi) function fmtCompacto(v: number): string { if (v >= 1_000_000) return `R$ ${(v / 1_000_000).toLocaleString('pt-BR', { maximumFractionDigits: 1 })} mi`; if (v >= 1_000) return `R$ ${(v / 1_000).toLocaleString('pt-BR', { maximumFractionDigits: 1 })} mil`; return fmt(v); } // Escala sequencial (um matiz, claro→escuro) derivada do azul JCS. const SEQ_STEPS = ['#dbe9f6', '#aecde9', '#7BA7D7', '#3d74b8', '#004a99']; const COR_SEM_VENDA = '#edf0f3'; function corDaUf(valor: number, max: number): string { if (valor <= 0 || max <= 0) return COR_SEM_VENDA; const idx = Math.min(SEQ_STEPS.length - 1, Math.floor((valor / max) * SEQ_STEPS.length)); return SEQ_STEPS[idx] ?? COR_SEM_VENDA; } const NOME_UF = new Map(brazilMap.locations.map((l) => [l.id.toUpperCase(), l.name])); const repColumns: TableColumnsType = [ { title: 'Representante', key: 'rep', render: (_: unknown, r: UfRep) => ( {r.nomeVendedor ?? `Cód. ${r.codVendedor}`} ), ellipsis: true, }, { title: 'Pedidos', dataIndex: 'pedidos', width: 90, align: 'right', className: 'tabular-nums', }, { title: 'Clientes', dataIndex: 'clientes', width: 90, align: 'right', className: 'tabular-nums', }, { title: 'Faturamento', dataIndex: 'faturamento', width: 150, align: 'right', render: (v: number) => fmt(v), className: 'tabular-nums', }, ]; function detalheColumns(): TableColumnsType { return [ { title: 'Estado', key: 'uf', render: (_: unknown, r: UfFaturamento) => ( {r.uf} {NOME_UF.get(r.uf) ?? 'Sem UF cadastrada'} ), }, { title: 'Pedidos', dataIndex: 'pedidos', width: 90, align: 'right', className: 'tabular-nums', }, { title: 'Clientes', dataIndex: 'clientes', width: 90, align: 'right', className: 'tabular-nums', }, { title: 'Faturamento', dataIndex: 'faturamento', width: 150, align: 'right', render: (v: number) => ( {fmt(v)} ), }, { title: 'Participação', dataIndex: 'pct', width: 170, render: (pct: number) => ( {pct.toLocaleString('pt-BR')}% ), }, ]; } export function MapaBrasilCard({ dados, periodoLabel, }: { dados: UfFaturamento[]; periodoLabel: string; }) { const [detalheAberto, setDetalheAberto] = useState(false); const [ufsExpandidas, setUfsExpandidas] = useState([]); const [ufHover, setUfHover] = useState(null); const porUf = useMemo(() => new Map(dados.map((d) => [d.uf, d])), [dados]); const max = dados.reduce((m, d) => Math.max(m, d.faturamento), 0); const comVenda = dados.filter((d) => d.faturamento > 0 && d.uf !== 'ND'); const topUfs = comVenda.slice(0, 8); const semUf = dados.find((d) => d.uf === 'ND'); const abrirDetalhe = (uf?: string) => { const primeira = comVenda[0]?.uf; setUfsExpandidas(uf ? [uf] : primeira ? [primeira] : []); setDetalheAberto(true); }; return ( Faturamento por Estado } extra={ } style={{ height: '100%' }} styles={{ body: { paddingTop: 12 } }} > {dados.length === 0 ? ( ) : ( {/* Mapa choropleth */} {brazilMap.locations.map((loc) => { const uf = loc.id.toUpperCase(); const dado = porUf.get(uf); const valor = dado?.faturamento ?? 0; const hover = ufHover === uf; return ( {loc.name} ({uf}) {dado ? ( <> {fmt(valor)} · {dado.pct.toLocaleString('pt-BR')}% do total {dado.pedidos} pedido{dado.pedidos !== 1 ? 's' : ''} · {dado.clientes}{' '} cliente{dado.clientes !== 1 ? 's' : ''} ) : ( Sem vendas no período )} } > setUfHover(uf)} onMouseLeave={() => setUfHover(null)} onClick={() => dado && abrirDetalhe(uf)} /> ); })} {/* Legenda da escala */} Sem venda Menor {SEQ_STEPS.map((c) => ( ))} Maior {/* Ranking de estados */} {topUfs.map((d, i) => ( setUfHover(d.uf)} onMouseLeave={() => setUfHover(null)} onClick={() => abrirDetalhe(d.uf)} > {i + 1}º {d.uf} {fmtCompacto(d.faturamento)} {d.pct.toLocaleString('pt-BR')}% ))} {comVenda.length > topUfs.length && ( + {comVenda.length - topUfs.length} outro {comVenda.length - topUfs.length !== 1 ? 's' : ''} estado {comVenda.length - topUfs.length !== 1 ? 's' : ''} — ver Detalhar )} {semUf && ( {fmtCompacto(semUf.faturamento)} sem UF cadastrada )} )} {/* Detalhe por estado → representantes */} Faturamento por Estado — {periodoLabel} } open={detalheAberto} onCancel={() => setDetalheAberto(false)} footer={null} width={900} centered > rowKey="uf" columns={detalheColumns()} dataSource={dados} pagination={false} size="small" scroll={{ y: 420 }} expandable={{ expandedRowKeys: ufsExpandidas, onExpandedRowsChange: (keys) => setUfsExpandidas([...keys] as string[]), expandedRowRender: (r) => ( rowKey="codVendedor" columns={repColumns} dataSource={r.reps} pagination={false} size="small" /> ), }} locale={{ emptyText: 'Nenhuma venda no período.' }} /> ); }