import { useEffect, useState } from 'react'; import { useParams, Link, useOutletContext } from 'react-router-dom'; import { User, Tag, Package, DollarSign, Clock, Phone, ChevronDown, ShoppingBag, ReceiptText } from 'lucide-react'; import { AreaChart, Area, BarChart, Bar, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import BackButton from '../components/BackButton'; import DateRangePicker from '../components/DateRangePicker'; import PaginationControls from '../components/PaginationControls'; import RefreshStatus from '../components/RefreshStatus'; import type { ClientDetailsAnalytics, DateRange, OrderData } from '../types'; import { fetchClientDetailsAnalytics } from '../dataService'; import { formatDisplayName, removeTrailingSellerId } from '../displayFormatters'; const CHART_GRID_COLOR = 'var(--chart-grid)'; const CHART_AXIS_COLOR = 'var(--chart-axis)'; const CHART_CURSOR_COLOR = 'var(--chart-cursor)'; const CHART_DETAIL_BAR_COLOR = 'var(--chart-detail-bar)'; const WEEKDAY_BAR_COLOR = '#25C2FF'; const HOUR_BAR_COLOR = '#52DFA0'; const formatDateKey = (date: Date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; type CustomTooltipProps = { active?: boolean; payload?: Array<{ value: number }>; label?: string; }; type PatternPoint = { label: string; value: number; }; const getTopPatternPoint = (points: PatternPoint[]) => ( points.reduce((topPoint, point) => { if (!topPoint || point.value > topPoint.value) return point; return topPoint; }, null) ); const getPatternShare = (value: number, total: number) => ( total > 0 ? Math.round((value / total) * 100) : 0 ); const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { if (active && payload && payload.length) { return (

{label}

Gasto: {new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(payload[0].value)}

); } return null; }; const PatternTooltip = ({ active, payload, label }: CustomTooltipProps) => { if (active && payload && payload.length) { const value = payload[0].value; return (

{label}

{value} {value === 1 ? 'pedido' : 'pedidos'}

); } return null; }; const getOrderMetadata = (order: OrderData) => { const sellerName = formatDisplayName(removeTrailingSellerId(order.nome_vendedor || '')); return [ order.cliente_nome_fantasia ? `Fantasia: ${formatDisplayName(order.cliente_nome_fantasia)}` : '', sellerName ? `Vendedor: ${sellerName}` : '', order.marketplace ? `Marketplace: ${order.marketplace}` : '', order.canal_venda ? `Canal: ${order.canal_venda}` : '', order.numero_ecommerce ? `E-commerce: ${order.numero_ecommerce}` : '' ].filter(Boolean); }; const ClientDetailsSkeleton = () => (
{[0, 1, 2, 3].map(item => (
))}
{[0, 1].map(item => (
{[0, 1, 2, 3, 4, 5, 6].map(bar => (
))}
))}
{[0, 1, 2, 3].map(item => (
))}
); const ClientDetails = () => { const { clientToken } = useParams<{ clientToken: string }>(); const decodedClientToken = clientToken ? decodeURIComponent(clientToken) : ''; const { dateRange, setDateRange } = useOutletContext<{ dateRange: DateRange, setDateRange: (range: DateRange) => void }>(); const [details, setDetails] = useState(null); const [isLoading, setIsLoading] = useState(true); const [currentPage, setCurrentPage] = useState(1); const [ordersPerPage, setOrdersPerPage] = useState(5); const [expandedOrderIds, setExpandedOrderIds] = useState>(() => new Set()); useEffect(() => { let isMounted = true; const loadClientDetails = async () => { if (!decodedClientToken) { if (isMounted) { setDetails(null); setIsLoading(false); } return; } setIsLoading(true); const nextDetails = await fetchClientDetailsAnalytics(decodedClientToken, dateRange); if (isMounted) { setDetails(nextDetails); setIsLoading(false); } }; void loadClientDetails(); return () => { isMounted = false; }; }, [dateRange, decodedClientToken]); const formatCurrency = (value: number) => { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value); }; const formatNumber = (value: number) => { return new Intl.NumberFormat('pt-BR').format(value); }; const handleDateRangeChange = (range: DateRange) => { setCurrentPage(1); setExpandedOrderIds(new Set()); setDateRange(range); }; const toggleOrder = (orderId: string) => { setExpandedOrderIds(current => { const next = new Set(current); if (next.has(orderId)) { next.delete(orderId); } else { next.add(orderId); } return next; }); }; if (isLoading && !details) { return ; } if (!details?.hasClient) { return (

Cliente não encontrado.

Voltar para clientes
); } const { chartData, groupedOrders, purchaseHours = [], purchaseHourRangeLabel = 'Últimos 60 dias', purchaseWeekdayRangeLabel = 'Todo período', purchaseWeekdays = [], allTimeOrderCount, clientName, clientPhone, periodAverageTicket, periodItems, periodOrderCount, periodSpent } = details; const displayName = clientName || 'Cliente'; const totalPages = Math.ceil(groupedOrders.length / ordersPerPage); const safeCurrentPage = Math.min(currentPage, totalPages || 1); const startIndex = (safeCurrentPage - 1) * ordersPerPage; const paginatedOrders = groupedOrders.slice(startIndex, startIndex + ordersPerPage); const hasWeekdayPattern = purchaseWeekdays.some(day => day.value > 0); const hasHourPattern = purchaseHours.some(hour => hour.value > 0); const weekdayPatternTotal = purchaseWeekdays.reduce((total, day) => total + day.value, 0); const hourPatternTotal = purchaseHours.reduce((total, hour) => total + hour.value, 0); const topWeekday = getTopPatternPoint(purchaseWeekdays); const topHour = getTopPatternPoint(purchaseHours); const isRefreshing = isLoading && Boolean(details); const isSingleDayRange = formatDateKey(dateRange.start) === formatDateKey(dateRange.end); return (
{/* Header Area */}

{displayName}

{formatNumber(allTimeOrderCount)} pedidos no histórico completo

{clientPhone && ( <> {clientPhone} )}

Gasto no Período

{formatCurrency(periodSpent)}

Ticket no Período

{formatCurrency(periodAverageTicket)}

Total Pedidos

{formatNumber(periodOrderCount)}

Itens no Período

{formatNumber(periodItems)}

Gasto por {isSingleDayRange ? 'Horário' : 'Data'}

{chartData.length === 0 ? (
Nenhum gasto no período selecionado.
) : (
formatCurrency(Number(value))} /> } cursor={{ fill: CHART_CURSOR_COLOR }} />
)}

Padrão de Compra

Quando este cliente costuma comprar, separando histórico de data e horário confiável.

Compras por Dia

{hasWeekdayPattern && topWeekday && (

Dia mais forte: {topWeekday.label} · {topWeekday.value} {topWeekday.value === 1 ? 'pedido' : 'pedidos'} · {getPatternShare(topWeekday.value, weekdayPatternTotal)}%

)}
{purchaseWeekdayRangeLabel}
{hasWeekdayPattern ? (
} cursor={{ fill: CHART_CURSOR_COLOR }} /> {purchaseWeekdays.map(day => ( ))}
) : (
Sem compras no período.
)}

Compras por Horário

{hasHourPattern && topHour && (

Horário mais forte: {topHour.label} · {topHour.value} {topHour.value === 1 ? 'pedido' : 'pedidos'} · {getPatternShare(topHour.value, hourPatternTotal)}%

)}
{purchaseHourRangeLabel}
{hasHourPattern ? (
Number(String(value).replace('h', '')) % 3 === 0 ? String(value) : ''} /> } cursor={{ fill: CHART_CURSOR_COLOR }} /> {purchaseHours.map(hour => ( ))}
) : (
Sem horário de compra disponível para os últimos 60 dias.
)}
{/* Orders List */}
{paginatedOrders.length === 0 ? (

Nenhum pedido no período selecionado.

Altere o filtro de data para ver outros pedidos deste cliente.

) : paginatedOrders.map((group) => { const isExpanded = expandedOrderIds.has(group.orderId); return (
{isExpanded && (
{group.items.map((order, index) => { const metadata = getOrderMetadata(order); return (
ID: {order.ID_Produto}
{order.Data_Pedido && (
Comprado: {order.Data_Pedido}
)}

{order.Descricao_Produto}

Qtd: {order.Quantidade}
Preço: {formatCurrency(order.Valor_Unitario)}
{metadata.length > 0 && (
{metadata.map(value => ( {value} ))}
)}

Subtotal

{formatCurrency(order.Quantidade * order.Valor_Unitario)}

); })}
)}
); })}
{ setOrdersPerPage(pageSize); setCurrentPage(1); }} className="px-6 py-4 border border-zinc-200 dark:border-dark-border rounded-2xl bg-white dark:bg-dark-card" />
); }; export default ClientDetails;