import { useEffect, useMemo, useState } from 'react'; import { Link, useOutletContext } from 'react-router-dom'; import { Search, ChevronRight, Filter, ChevronLeft, Download } from 'lucide-react'; import type { ClientAnalyticsItem, DateRange, RfmAnalytics, RfmClient } from '../types'; import { exportToCSV, fetchClientAnalytics, fetchRfmAnalytics } from '../dataService'; import DateRangePicker from '../components/DateRangePicker'; import type { ClientSortOption, ClientSummary } from '../analytics/clients'; const clientTypeStyles: Record = { 'Sem análise': 'border-zinc-600/30 bg-zinc-600/15 text-zinc-300', '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 = [ 'Sem análise', '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 } = useOutletContext<{ dateRange: DateRange, setDateRange: (range: DateRange) => void }>(); const [searchTerm, setSearchTerm] = useState(''); const [sortBy, setSortBy] = useState('recent'); const [clientTypeFilter, setClientTypeFilter] = useState('all'); const [rfmAnalytics, setRfmAnalytics] = useState(null); const [clientAnalytics, setClientAnalytics] = useState([]); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); useEffect(() => { let isMounted = true; const loadClients = async () => { const clientsData = await fetchClientAnalytics(dateRange); if (isMounted) { setClientAnalytics(clientsData); setRfmAnalytics(null); } const rfmData = await fetchRfmAnalytics(dateRange); if (isMounted) { setRfmAnalytics(rfmData); } }; void loadClients(); return () => { isMounted = false; }; }, [dateRange]); const allClientsData = useMemo(() => { const normalizedSearch = searchTerm.trim().toLowerCase(); const rfmByCustomerKey = new Map((rfmAnalytics?.clients || []).map(client => [client.customerKey, client])); const clients = clientAnalytics.map((client): ClientSummary => { const rfmClient = rfmByCustomerKey.get(client.customerKey); return { customerKey: client.customerKey, clientToken: client.clientToken, name: client.name, phone: client.phone, totalSpent: client.totalSpent, averageTicket: client.orderCount ? client.totalSpent / client.orderCount : 0, totalItems: client.quantityPurchased, orderCount: client.orderCount, lastPurchase: client.lastPurchaseDate ? new Date(client.lastPurchaseDate).getTime() : 0, clientType: rfmClient ? (backendSegmentToClientType[rfmClient.segmentKey] || rfmClient.segmentLabel) : 'Sem análise', rfmScore: rfmClient?.rfmScore || '000', rfmPriority: rfmClient ? getRfmPriority(rfmClient) : 0 }; }); const filteredClients = normalizedSearch ? clients.filter(client => client.name.toLowerCase().includes(normalizedSearch) || client.phone.toLowerCase().includes(normalizedSearch) ) : clients; return sortClients(filteredClients, sortBy); }, [clientAnalytics, searchTerm, sortBy, rfmAnalytics]); 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 formatNumber = (value: number) => { return new Intl.NumberFormat('pt-BR').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 RFV sincronizada com a página RFV quando disponível.

{clientTypes.map(type => ( ))}
{paginatedData.map((client, index) => ( ))}
Posição Nome do Cliente Tipo RFV 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)} {formatNumber(client.totalItems)} un. ({formatNumber(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;