From ce72231560d8756a81c1634c543b38832635ce4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Wed, 17 Jun 2026 09:59:57 -0300 Subject: [PATCH] Score RFV clients without phone --- backend/services/analyticsService.js | 34 +++++++++++++++------------ backend/test/analyticsService.test.js | 5 ++++ src/pages/Clients.tsx | 8 +++---- src/types.ts | 2 ++ 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/backend/services/analyticsService.js b/backend/services/analyticsService.js index 097dace..097e423 100644 --- a/backend/services/analyticsService.js +++ b/backend/services/analyticsService.js @@ -7,6 +7,7 @@ const PRODUCT_NAME_SQL = ` ELSE NULLIF(TRIM(regexp_replace(split_part(COALESCE(produto_descricao, 'Unknown'), ' TAMANHO', 1), '${SIZE_SUFFIX_SQL_PATTERN}', '', 'i')), '') END `; +const CUSTOMER_KEY_SQL = "COALESCE(NULLIF(cliente_fone, ''), 'name:' || COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido'))"; const normalizeDateParam = (value) => { if (!value) return null; @@ -235,7 +236,8 @@ const getClientAnalytics = async (range = {}) => { const { params, whereClause } = buildDateFilter(range); const result = await pool.query(` SELECT - COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido') as name, + ${CUSTOMER_KEY_SQL} as customer_key, + MAX(COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido')) as name, MAX(NULLIF(cliente_fone, '')) as phone, COALESCE(SUM(quantidade), 0) as quantity_purchased, COALESCE(SUM(quantidade * valor_unitario), 0) as total_spent, @@ -243,11 +245,12 @@ const getClientAnalytics = async (range = {}) => { MAX(data_pedido_date) as last_purchase_date FROM orders ${whereClause} - GROUP BY COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido') + GROUP BY customer_key ORDER BY total_spent DESC; `, params); return result.rows.map(row => ({ + customerKey: row.customer_key, name: row.name, phone: row.phone || '', quantityPurchased: toNumber(row.quantity_purchased), @@ -268,23 +271,23 @@ const getRfmAnalytics = async (range = {}) => { const [periodResult, historyResult] = await Promise.all([ pool.query(` SELECT + ${CUSTOMER_KEY_SQL} as customer_key, MAX(cliente_nome) as name, - cliente_fone as phone, + MAX(NULLIF(cliente_fone, '')) as phone, COALESCE(SUM(quantidade * valor_unitario), 0) as monetary, COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as frequency, COALESCE(SUM(quantidade), 0) as quantity_purchased, MAX(data_pedido_date) as last_purchase_date FROM orders ${whereClause} - AND cliente_fone IS NOT NULL - AND cliente_fone != '' - GROUP BY cliente_fone + GROUP BY customer_key ORDER BY monetary DESC; `, params), pool.query(` SELECT + ${CUSTOMER_KEY_SQL} as customer_key, MAX(cliente_nome) as name, - cliente_fone as phone, + MAX(NULLIF(cliente_fone, '')) as phone, COALESCE(SUM(quantidade * valor_unitario), 0) as monetary, COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as frequency, COALESCE(SUM(quantidade), 0) as quantity_purchased, @@ -293,30 +296,30 @@ const getRfmAnalytics = async (range = {}) => { FROM orders WHERE data_pedido_date IS NOT NULL AND data_pedido_date <= ${recencyReferenceDate} - AND cliente_fone IS NOT NULL - AND cliente_fone != '' - GROUP BY cliente_fone; + GROUP BY customer_key; `, historyParams) ]); const historyClients = buildRfmClients(historyResult.rows.map(row => ({ + customerKey: row.customer_key, name: row.name, - phone: row.phone, + phone: row.phone || '', monetary: toNumber(row.monetary), frequency: toNumber(row.frequency), quantityPurchased: toNumber(row.quantity_purchased), lastPurchaseDate: row.last_purchase_date, recencyDays: toNumber(row.recency_days) }))); - const tagsByPhone = new Map(historyClients.map(client => [client.phone, client])); + const tagsByCustomerKey = new Map(historyClients.map(client => [client.customerKey, client])); const clients = periodResult.rows.map(row => { - const taggedClient = tagsByPhone.get(row.phone); + const taggedClient = tagsByCustomerKey.get(row.customer_key); if (!taggedClient) { const newCustomerSegment = getRfmSegment(3, 1); return { + customerKey: row.customer_key, name: row.name, - phone: row.phone, + phone: row.phone || '', monetary: toNumber(row.monetary), frequency: toNumber(row.frequency), quantityPurchased: toNumber(row.quantity_purchased), @@ -334,8 +337,9 @@ const getRfmAnalytics = async (range = {}) => { return { ...taggedClient, + customerKey: row.customer_key, name: row.name, - phone: row.phone, + phone: row.phone || '', monetary: toNumber(row.monetary), frequency: toNumber(row.frequency), quantityPurchased: toNumber(row.quantity_purchased), diff --git a/backend/test/analyticsService.test.js b/backend/test/analyticsService.test.js index a878e8c..803cb41 100644 --- a/backend/test/analyticsService.test.js +++ b/backend/test/analyticsService.test.js @@ -204,6 +204,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected return { rows: [ { + customer_key: '1', name: 'Cliente Ontem', phone: '1', monetary: 5000, @@ -213,6 +214,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected recency_days: endDate === '2026-06-13' ? 0 : 1 }, { + customer_key: '2', name: 'Cliente Antigo', phone: '2', monetary: 50, @@ -222,6 +224,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected recency_days: 164 }, { + customer_key: '3', name: 'Cliente Medio', phone: '3', monetary: 100, @@ -237,6 +240,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected return { rows: [ { + customer_key: '1', name: 'Cliente Ontem', phone: '1', monetary: 100, @@ -245,6 +249,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected last_purchase_date: '2026-06-14' }, { + customer_key: '4', name: 'Cliente Novo no Periodo', phone: '4', monetary: 25, diff --git a/src/pages/Clients.tsx b/src/pages/Clients.tsx index d1219c2..5e16317 100644 --- a/src/pages/Clients.tsx +++ b/src/pages/Clients.tsx @@ -7,7 +7,6 @@ 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', @@ -20,7 +19,6 @@ const clientTypeStyles: Record = { }; const clientTypes = [ - 'Sem análise', 'Campeão', 'Potencial Leal', 'Novo Cliente', @@ -102,9 +100,9 @@ const Clients = () => { const allClientsData = useMemo(() => { const normalizedSearch = searchTerm.trim().toLowerCase(); - const rfmByPhone = new Map((rfmAnalytics?.clients || []).map(client => [client.phone, client])); + const rfmByCustomerKey = new Map((rfmAnalytics?.clients || []).map(client => [client.customerKey, client])); const clients = clientAnalytics.map((client): ClientSummary => { - const rfmClient = client.phone ? rfmByPhone.get(client.phone) : undefined; + const rfmClient = rfmByCustomerKey.get(client.customerKey); return { name: client.name, phone: client.phone, @@ -113,7 +111,7 @@ const Clients = () => { 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', + clientType: rfmClient ? (backendSegmentToClientType[rfmClient.segmentKey] || rfmClient.segmentLabel) : 'Perdido', rfmScore: rfmClient?.rfmScore || '000', rfmPriority: rfmClient ? getRfmPriority(rfmClient) : 0 }; diff --git a/src/types.ts b/src/types.ts index be2e367..3abbb59 100644 --- a/src/types.ts +++ b/src/types.ts @@ -64,6 +64,7 @@ export interface DashboardAnalytics { } export interface ClientAnalyticsItem { + customerKey: string; name: string; phone: string; quantityPurchased: number; @@ -73,6 +74,7 @@ export interface ClientAnalyticsItem { } export interface RfmClient { + customerKey: string; name: string; phone: string; monetary: number;