import { useEffect, useMemo, useState } from 'react'; import { Link, useOutletContext } from 'react-router-dom'; import { Search, ChevronRight, Filter, ChevronLeft, Download } from 'lucide-react'; import type { OrderData, DateRange, RfmAnalytics, RfmClient } from '../types'; import { exportToCSV, fetchRfmAnalytics } from '../dataService'; import DateRangePicker from '../components/DateRangePicker'; import { buildClientsSummary, type ClientSortOption, type ClientSummary } from '../analytics/clients'; const clientTypeStyles: Record = { 'Campeão': 'border-emerald-500/30 bg-emerald-500/15 text-emerald-300', 'Potencial Leal': 'border-sky-500/30 bg-sky-500/15 text-sky-300', 'Novo Cliente': 'border-cyan-500/30 bg-cyan-500/15 text-cyan-300', 'Cliente Leal': 'border-blue-500/30 bg-blue-500/15 text-blue-300', 'Precisa de Atenção': 'border-amber-500/30 bg-amber-500/15 text-amber-300', 'Quase Dormindo': 'border-orange-500/30 bg-orange-500/15 text-orange-300', 'Em Risco': 'border-rose-500/30 bg-rose-500/15 text-rose-300', 'Hibernando': 'border-fuchsia-500/30 bg-fuchsia-500/15 text-fuchsia-300', 'Perdido': 'border-zinc-500/30 bg-zinc-500/15 text-zinc-300' }; const clientTypes = [ 'Campeão', 'Potencial Leal', 'Novo Cliente', 'Cliente Leal', 'Precisa de Atenção', 'Quase Dormindo', 'Em Risco', 'Hibernando', 'Perdido' ]; const backendSegmentToClientType: Record = { champions: 'Campeão', potential_loyalists: 'Potencial Leal', new_customers: 'Novo Cliente', loyal_customers: 'Cliente Leal', need_attention: 'Precisa de Atenção', about_to_sleep: 'Quase Dormindo', at_risk: 'Em Risco', hibernating: 'Hibernando', lost: 'Perdido' }; const getRfmPriority = (client: Pick) => { return (client.recencyScore * 100) + (client.valueScore * 10) + client.monetaryScore; }; const sortClients = (clients: ClientSummary[], sortBy: ClientSortOption) => { return [...clients].sort((a, b) => { switch (sortBy) { case 'recent': return b.lastPurchase - a.lastPurchase; case 'spent_desc': return b.totalSpent - a.totalSpent; case 'spent_asc': return a.totalSpent - b.totalSpent; case 'ticket_desc': return b.averageTicket - a.averageTicket; case 'ticket_asc': return a.averageTicket - b.averageTicket; case 'rfm_priority': return b.rfmPriority - a.rfmPriority; case 'items_desc': return b.totalItems - a.totalItems; case 'items_asc': return a.totalItems - b.totalItems; default: return 0; } }); }; const Clients = () => { const { dateRange, setDateRange, ordersData } = useOutletContext<{ dateRange: DateRange, setDateRange: (range: DateRange) => void, ordersData: OrderData[] }>(); const [searchTerm, setSearchTerm] = useState(''); const [sortBy, setSortBy] = useState('recent'); const [clientTypeFilter, setClientTypeFilter] = useState('all'); const [rfmAnalytics, setRfmAnalytics] = useState(null); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); useEffect(() => { let isMounted = true; const loadRfm = async () => { const data = await fetchRfmAnalytics(dateRange); if (isMounted) setRfmAnalytics(data); }; void loadRfm(); return () => { isMounted = false; }; }, [dateRange]); const rfmClientsByIdentity = useMemo(() => { const map = new Map(); (rfmAnalytics?.clients || []).forEach(client => { if (client.phone) map.set(`phone:${client.phone}`, client); map.set(`name:${client.name.toLowerCase()}`, client); }); return map; }, [rfmAnalytics]); const allClientsData = useMemo(() => { const localClients = buildClientsSummary(ordersData, dateRange, searchTerm, sortBy); const enrichedClients = localClients.map(client => { const rfmClient = (client.phone && rfmClientsByIdentity.get(`phone:${client.phone}`)) || rfmClientsByIdentity.get(`name:${client.name.toLowerCase()}`); if (!rfmClient) return client; return { ...client, clientType: backendSegmentToClientType[rfmClient.segmentKey] || client.clientType, rfmScore: rfmClient.rfmScore, rfmPriority: getRfmPriority(rfmClient) }; }); return sortClients(enrichedClients, sortBy); }, [searchTerm, sortBy, ordersData, dateRange, rfmClientsByIdentity]); const clientsData = useMemo(() => { if (clientTypeFilter === 'all') return allClientsData; return allClientsData.filter(client => client.clientType === clientTypeFilter); }, [allClientsData, clientTypeFilter]); const clientTypeCounts = useMemo(() => { return clientTypes.reduce>((counts, type) => { counts[type] = allClientsData.filter(client => client.clientType === type).length; return counts; }, {}); }, [allClientsData]); // Pagination logic const totalPages = Math.ceil(clientsData.length / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const paginatedData = clientsData.slice(startIndex, startIndex + itemsPerPage); const formatCurrency = (value: number) => { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value); }; const selectClientType = (type: string) => { setClientTypeFilter(current => current === type ? 'all' : type); setCurrentPage(1); }; return (

Clientes

Métricas de engajamento e histórico de consumo dos seus clientes.

{ setDateRange(range); setCurrentPage(1); }} />
{ setSearchTerm(e.target.value); setCurrentPage(1); }} className="w-full sm:w-64 bg-dark-card border border-dark-border text-dark-text rounded-xl pl-10 pr-4 py-2.5 focus:outline-none focus:border-brand-primary hover:border-brand-primary transition-colors shadow-sm" />

Tipos de Clientes

Classificação RFM sincronizada com a página RFM quando disponível.

{clientTypes.map(type => ( ))}
{paginatedData.map((client, index) => ( ))}
Posição Nome do Cliente Tipo RFM Total Gasto Ticket Médio Produtos Comprados Ações
{startIndex + index + 1} {client.name} {client.clientType}
{client.rfmScore.split('').map((score, scoreIndex) => ( {score} ))}
{formatCurrency(client.totalSpent)} {formatCurrency(client.averageTicket)} {client.totalItems} un. ({client.orderCount} {client.orderCount === 1 ? 'pedido' : 'pedidos'}) Ver detalhes
{/* Pagination Controls */}
Mostrar itens por página
Mostrando {clientsData.length > 0 ? startIndex + 1 : 0} a {Math.min(startIndex + itemsPerPage, clientsData.length)} de {clientsData.length} clientes
); }; export default Clients;