Use RFM summaries for clients list
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s
This commit is contained in:
@@ -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]);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user