Use RFM summaries for clients list
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s

This commit is contained in:
Cauê Faleiros
2026-06-12 14:36:22 -03:00
parent ba27a342f8
commit 68a79838a2
2 changed files with 28 additions and 33 deletions

View File

@@ -6,7 +6,8 @@ import { fetchData, fetchStock, logout } from '../dataService';
const Layout = () => { const Layout = () => {
const location = useLocation(); const location = useLocation();
const needsRawData = location.pathname.startsWith('/products') || location.pathname.startsWith('/clients'); const needsRawData = location.pathname.startsWith('/products') ||
(location.pathname.startsWith('/clients/') && location.pathname !== '/clients');
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => { const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
return localStorage.getItem('graph_sidebar_collapsed') === 'true'; return localStorage.getItem('graph_sidebar_collapsed') === 'true';
}); });
@@ -47,7 +48,7 @@ const Layout = () => {
useEffect(() => { useEffect(() => {
if (!needsRawData) return; if (!needsRawData) return;
// Product and client pages still depend on raw orders until their API migration is complete. // Product pages and client details still depend on raw orders until their API migration is complete.
// eslint-disable-next-line react-hooks/set-state-in-effect // eslint-disable-next-line react-hooks/set-state-in-effect
void loadData(true); void loadData(true);
}, [loadData, needsRawData]); }, [loadData, needsRawData]);

View File

@@ -1,10 +1,10 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Link, useOutletContext } from 'react-router-dom'; import { Link, useOutletContext } from 'react-router-dom';
import { Search, ChevronRight, Filter, ChevronLeft, Download } from 'lucide-react'; import { Search, ChevronRight, Filter, ChevronLeft, Download } from 'lucide-react';
import type { OrderData, DateRange, RfmAnalytics, RfmClient } from '../types'; import type { DateRange, RfmAnalytics, RfmClient } from '../types';
import { exportToCSV, fetchRfmAnalytics } from '../dataService'; import { exportToCSV, fetchRfmAnalytics } from '../dataService';
import DateRangePicker from '../components/DateRangePicker'; import DateRangePicker from '../components/DateRangePicker';
import { buildClientsSummary, type ClientSortOption, type ClientSummary } from '../analytics/clients'; import type { ClientSortOption, ClientSummary } from '../analytics/clients';
const clientTypeStyles: Record<string, string> = { const clientTypeStyles: Record<string, string> = {
'Campeão': 'border-emerald-500/30 bg-emerald-500/15 text-emerald-300', 'Campeão': 'border-emerald-500/30 bg-emerald-500/15 text-emerald-300',
@@ -63,10 +63,9 @@ const sortClients = (clients: ClientSummary[], sortBy: ClientSortOption) => {
}; };
const Clients = () => { const Clients = () => {
const { dateRange, setDateRange, ordersData } = useOutletContext<{ const { dateRange, setDateRange } = useOutletContext<{
dateRange: DateRange, dateRange: DateRange,
setDateRange: (range: DateRange) => void, setDateRange: (range: DateRange) => void
ordersData: OrderData[]
}>(); }>();
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState<ClientSortOption>('recent'); const [sortBy, setSortBy] = useState<ClientSortOption>('recent');
@@ -92,35 +91,30 @@ const Clients = () => {
}; };
}, [dateRange]); }, [dateRange]);
const rfmClientsByIdentity = useMemo(() => {
const map = new Map<string, RfmClient>();
(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 allClientsData = useMemo(() => {
const localClients = buildClientsSummary(ordersData, dateRange, searchTerm, sortBy); const normalizedSearch = searchTerm.trim().toLowerCase();
const enrichedClients = localClients.map(client => { const clients = (rfmAnalytics?.clients || []).map((client): ClientSummary => ({
const rfmClient = (client.phone && rfmClientsByIdentity.get(`phone:${client.phone}`)) || name: client.name,
rfmClientsByIdentity.get(`name:${client.name.toLowerCase()}`); phone: client.phone,
totalSpent: client.monetary,
averageTicket: client.frequency ? client.monetary / client.frequency : 0,
totalItems: client.quantityPurchased,
orderCount: client.frequency,
lastPurchase: client.lastPurchaseDate ? new Date(client.lastPurchaseDate).getTime() : 0,
clientType: backendSegmentToClientType[client.segmentKey] || client.segmentLabel,
rfmScore: client.rfmScore,
rfmPriority: getRfmPriority(client)
}));
if (!rfmClient) return client; const filteredClients = normalizedSearch
? clients.filter(client =>
client.name.toLowerCase().includes(normalizedSearch) ||
client.phone.toLowerCase().includes(normalizedSearch)
)
: clients;
return { return sortClients(filteredClients, sortBy);
...client, }, [searchTerm, sortBy, rfmAnalytics]);
clientType: backendSegmentToClientType[rfmClient.segmentKey] || client.clientType,
rfmScore: rfmClient.rfmScore,
rfmPriority: getRfmPriority(rfmClient)
};
});
return sortClients(enrichedClients, sortBy);
}, [searchTerm, sortBy, ordersData, dateRange, rfmClientsByIdentity]);
const clientsData = useMemo(() => { const clientsData = useMemo(() => {
if (clientTypeFilter === 'all') return allClientsData; if (clientTypeFilter === 'all') return allClientsData;