import type { DateRange, OrderData } from '../types.ts'; import { filterOrdersByDateRange, getClientDisplayName, getOrderItemRevenue, getOrderCustomerKey, parseOrderDate } from './orders.ts'; export type ClientSortOption = | 'recent' | 'spent_desc' | 'spent_asc' | 'ticket_desc' | 'ticket_asc' | 'rfm_priority' | 'items_desc' | 'items_asc'; export interface ClientSummary { customerKey: string; clientToken: string; name: string; phone: string; totalSpent: number; averageTicket: number; totalItems: number; orderCount: number; lastPurchase: number; clientType: string; rfmScore: string; rfmPriority: number; } export interface GroupedClientOrder { date: string; orderId: string; orderTotal: number; items: OrderData[]; } export interface ClientDetailsMetrics { chartData: Array<{ date: string; value: number; }>; purchaseWeekdays: Array<{ label: string; value: number; }>; purchaseHours: Array<{ label: string; value: number; }>; groupedOrders: GroupedClientOrder[]; allTimeOrderCount: number; clientName: string; clientPhone: string; hasClient: boolean; periodAverageTicket: number; periodOrderCount: number; periodSpent: number; periodItems: number; } const WEEKDAY_LABELS = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sab']; const getOrderHour = (order: OrderData): number | null => { const timestamp = order.Recebido_Em || (/\d{1,2}:\d{2}/.test(order.Data_Pedido || '') ? order.Data_Pedido : ''); if (!timestamp) return null; const date = new Date(timestamp); if (!Number.isNaN(date.getTime())) return date.getHours(); const timeMatch = String(timestamp).match(/\b(\d{1,2}):\d{2}/); if (!timeMatch) return null; const hour = Number(timeMatch[1]); return hour >= 0 && hour <= 23 ? hour : null; }; const scoreTertile = (value: number, values: number[], higherIsBetter = true) => { const numericValues = values.filter(Number.isFinite); if (!numericValues.length) return 1; if (numericValues.length === 1) return 3; const min = Math.min(...numericValues); const max = Math.max(...numericValues); if (min === max) return 2; const sorted = [...numericValues].sort((a, b) => higherIsBetter ? a - b : b - a); const index = sorted.findIndex(candidate => candidate === value); const percentile = index / (sorted.length - 1); return Math.min(3, Math.max(1, Math.floor(percentile * 3) + 1)); }; const getClientType = (recencyScore: number, valueScore: number) => { const segmentMap: Record = { '3-3': 'Campeão', '3-2': 'Potencial Leal', '3-1': 'Novo Cliente', '2-3': 'Cliente Leal', '2-2': 'Precisa de Atenção', '2-1': 'Quase Dormindo', '1-3': 'Em Risco', '1-2': 'Hibernando', '1-1': 'Perdido' }; return segmentMap[`${recencyScore}-${valueScore}`] || 'Perdido'; }; const enrichClientsWithRfmType = ( clients: Array>, dateRange: DateRange ): ClientSummary[] => { const rangeEndTime = dateRange.end.getTime(); const recencyValues = clients.map(client => Math.max(0, Math.floor((rangeEndTime - client.lastPurchase) / 86400000))); const frequencyValues = clients.map(client => client.orderCount); const monetaryValues = clients.map(client => client.totalSpent); return clients.map((client, index) => { const recencyScore = scoreTertile(recencyValues[index], recencyValues, false); const frequencyScore = scoreTertile(client.orderCount, frequencyValues); const monetaryScore = scoreTertile(client.totalSpent, monetaryValues); const valueScore = Math.min(3, Math.max(1, Math.round((frequencyScore + monetaryScore) / 2))); const rfmPriority = (recencyScore * 100) + (valueScore * 10) + monetaryScore; return { ...client, clientToken: client.clientToken || client.customerKey, averageTicket: client.orderCount ? client.totalSpent / client.orderCount : 0, clientType: getClientType(recencyScore, valueScore), rfmScore: `${recencyScore}${frequencyScore}${monetaryScore}`, rfmPriority }; }); }; export const buildClientsSummary = ( ordersData: OrderData[], dateRange: DateRange, searchTerm: string, sortBy: ClientSortOption ): ClientSummary[] => { const orders = filterOrdersByDateRange(ordersData, dateRange); const clientMap: Record; lastPurchase: number; phone: string; }> = {}; orders.forEach(order => { const clientName = getClientDisplayName(order); const customerKey = getOrderCustomerKey(order); if (!clientMap[customerKey]) { clientMap[customerKey] = { name: clientName, totalSpent: 0, totalItems: 0, uniqueOrders: new Set(), lastPurchase: 0, phone: '' }; } if (order.Fone_Cliente) { clientMap[customerKey].phone = order.Fone_Cliente; } clientMap[customerKey].totalSpent += getOrderItemRevenue(order); clientMap[customerKey].totalItems += order.Quantidade; clientMap[customerKey].uniqueOrders.add(`${order.Data_Pedido}_${order.Valor_Pedido}`); const orderTime = parseOrderDate(order.Data_Pedido).getTime(); if (orderTime > clientMap[customerKey].lastPurchase) { clientMap[customerKey].lastPurchase = orderTime; } }); const normalizedSearch = searchTerm.trim().toLowerCase(); const clients = enrichClientsWithRfmType(Object.keys(clientMap).map(customerKey => ({ customerKey, clientToken: customerKey, name: clientMap[customerKey].name, phone: clientMap[customerKey].phone, totalSpent: clientMap[customerKey].totalSpent, totalItems: clientMap[customerKey].totalItems, orderCount: clientMap[customerKey].uniqueOrders.size, lastPurchase: clientMap[customerKey].lastPurchase })), dateRange); const filteredClients = normalizedSearch ? clients.filter(client => client.name.toLowerCase().includes(normalizedSearch)) : clients; return filteredClients.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; } }); }; export const buildClientDetailsMetrics = (ordersData: OrderData[], customerKey: string, dateRange: DateRange): ClientDetailsMetrics => { const keyedOrders = ordersData.filter(order => getOrderCustomerKey(order) === customerKey); const legacyNameOrders = keyedOrders.length ? [] : ordersData.filter(order => getClientDisplayName(order) === customerKey); const legacyCustomerKeys = new Set(legacyNameOrders.map(getOrderCustomerKey)); const resolvedLegacyKey = legacyCustomerKeys.size === 1 ? [...legacyCustomerKeys][0] : ''; const clientOrders = keyedOrders.length ? keyedOrders : resolvedLegacyKey ? ordersData.filter(order => getOrderCustomerKey(order) === resolvedLegacyKey) : []; const periodOrders = filterOrdersByDateRange(clientOrders, dateRange); const groupedOrdersMap: Record = {}; const spentByDate: Record = {}; let clientPhone = ''; let clientName = ''; let periodSpent = 0; let periodItems = 0; clientOrders.forEach(order => { if (order.Fone_Cliente && !clientPhone) clientPhone = order.Fone_Cliente; if (!clientName) clientName = getClientDisplayName(order); }); periodOrders.forEach(order => { periodSpent += getOrderItemRevenue(order); periodItems += order.Quantidade; spentByDate[order.Data_Pedido] = (spentByDate[order.Data_Pedido] || 0) + getOrderItemRevenue(order); const key = order.ID_Pedido || `${order.Data_Pedido}_${order.Valor_Pedido}`; if (!groupedOrdersMap[key]) { groupedOrdersMap[key] = { date: order.Data_Pedido, orderId: order.ID_Pedido || key, orderTotal: 0, items: [] }; } groupedOrdersMap[key].items.push(order); groupedOrdersMap[key].orderTotal += getOrderItemRevenue(order); }); const groupedOrders = Object.values(groupedOrdersMap).sort((a, b) => { return parseOrderDate(b.date).getTime() - parseOrderDate(a.date).getTime(); }); const allTimeOrderIds = new Set(clientOrders.map(order => order.ID_Pedido || `${order.Data_Pedido}_${order.Valor_Pedido}`)); const periodOrderCount = groupedOrders.length; const chartData = Object.keys(spentByDate).map(date => ({ date, value: spentByDate[date] })).sort((a, b) => parseOrderDate(a.date).getTime() - parseOrderDate(b.date).getTime()); const weekdayCounts = WEEKDAY_LABELS.map(label => ({ label, value: 0 })); const hourCounts = Array.from({ length: 24 }, (_, hour) => ({ label: `${String(hour).padStart(2, '0')}h`, value: 0 })); const patternOrders = new Map(); clientOrders.forEach(order => { const key = order.ID_Pedido || `${order.Data_Pedido}_${order.Valor_Pedido}`; if (!patternOrders.has(key)) { patternOrders.set(key, order); } }); patternOrders.forEach(order => { const orderDate = parseOrderDate(order.Data_Pedido); if (!Number.isNaN(orderDate.getTime())) { weekdayCounts[orderDate.getDay()].value += 1; } const orderHour = getOrderHour(order); if (orderHour !== null) { hourCounts[orderHour].value += 1; } }); return { chartData, purchaseWeekdays: weekdayCounts, purchaseHours: hourCounts, groupedOrders, allTimeOrderCount: allTimeOrderIds.size, clientName, clientPhone, hasClient: clientOrders.length > 0, periodAverageTicket: periodOrderCount ? periodSpent / periodOrderCount : 0, periodOrderCount, periodSpent, periodItems }; };