Score RFV clients without phone
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m46s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m46s
This commit is contained in:
@@ -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')), '')
|
ELSE NULLIF(TRIM(regexp_replace(split_part(COALESCE(produto_descricao, 'Unknown'), ' TAMANHO', 1), '${SIZE_SUFFIX_SQL_PATTERN}', '', 'i')), '')
|
||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
|
const CUSTOMER_KEY_SQL = "COALESCE(NULLIF(cliente_fone, ''), 'name:' || COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido'))";
|
||||||
|
|
||||||
const normalizeDateParam = (value) => {
|
const normalizeDateParam = (value) => {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
@@ -235,7 +236,8 @@ const getClientAnalytics = async (range = {}) => {
|
|||||||
const { params, whereClause } = buildDateFilter(range);
|
const { params, whereClause } = buildDateFilter(range);
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
SELECT
|
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,
|
MAX(NULLIF(cliente_fone, '')) as phone,
|
||||||
COALESCE(SUM(quantidade), 0) as quantity_purchased,
|
COALESCE(SUM(quantidade), 0) as quantity_purchased,
|
||||||
COALESCE(SUM(quantidade * valor_unitario), 0) as total_spent,
|
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
|
MAX(data_pedido_date) as last_purchase_date
|
||||||
FROM orders
|
FROM orders
|
||||||
${whereClause}
|
${whereClause}
|
||||||
GROUP BY COALESCE(NULLIF(cliente_nome, ''), 'Cliente Desconhecido')
|
GROUP BY customer_key
|
||||||
ORDER BY total_spent DESC;
|
ORDER BY total_spent DESC;
|
||||||
`, params);
|
`, params);
|
||||||
|
|
||||||
return result.rows.map(row => ({
|
return result.rows.map(row => ({
|
||||||
|
customerKey: row.customer_key,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
phone: row.phone || '',
|
phone: row.phone || '',
|
||||||
quantityPurchased: toNumber(row.quantity_purchased),
|
quantityPurchased: toNumber(row.quantity_purchased),
|
||||||
@@ -268,23 +271,23 @@ const getRfmAnalytics = async (range = {}) => {
|
|||||||
const [periodResult, historyResult] = await Promise.all([
|
const [periodResult, historyResult] = await Promise.all([
|
||||||
pool.query(`
|
pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
|
${CUSTOMER_KEY_SQL} as customer_key,
|
||||||
MAX(cliente_nome) as name,
|
MAX(cliente_nome) as name,
|
||||||
cliente_fone as phone,
|
MAX(NULLIF(cliente_fone, '')) as phone,
|
||||||
COALESCE(SUM(quantidade * valor_unitario), 0) as monetary,
|
COALESCE(SUM(quantidade * valor_unitario), 0) as monetary,
|
||||||
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as frequency,
|
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as frequency,
|
||||||
COALESCE(SUM(quantidade), 0) as quantity_purchased,
|
COALESCE(SUM(quantidade), 0) as quantity_purchased,
|
||||||
MAX(data_pedido_date) as last_purchase_date
|
MAX(data_pedido_date) as last_purchase_date
|
||||||
FROM orders
|
FROM orders
|
||||||
${whereClause}
|
${whereClause}
|
||||||
AND cliente_fone IS NOT NULL
|
GROUP BY customer_key
|
||||||
AND cliente_fone != ''
|
|
||||||
GROUP BY cliente_fone
|
|
||||||
ORDER BY monetary DESC;
|
ORDER BY monetary DESC;
|
||||||
`, params),
|
`, params),
|
||||||
pool.query(`
|
pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
|
${CUSTOMER_KEY_SQL} as customer_key,
|
||||||
MAX(cliente_nome) as name,
|
MAX(cliente_nome) as name,
|
||||||
cliente_fone as phone,
|
MAX(NULLIF(cliente_fone, '')) as phone,
|
||||||
COALESCE(SUM(quantidade * valor_unitario), 0) as monetary,
|
COALESCE(SUM(quantidade * valor_unitario), 0) as monetary,
|
||||||
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as frequency,
|
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as frequency,
|
||||||
COALESCE(SUM(quantidade), 0) as quantity_purchased,
|
COALESCE(SUM(quantidade), 0) as quantity_purchased,
|
||||||
@@ -293,30 +296,30 @@ const getRfmAnalytics = async (range = {}) => {
|
|||||||
FROM orders
|
FROM orders
|
||||||
WHERE data_pedido_date IS NOT NULL
|
WHERE data_pedido_date IS NOT NULL
|
||||||
AND data_pedido_date <= ${recencyReferenceDate}
|
AND data_pedido_date <= ${recencyReferenceDate}
|
||||||
AND cliente_fone IS NOT NULL
|
GROUP BY customer_key;
|
||||||
AND cliente_fone != ''
|
|
||||||
GROUP BY cliente_fone;
|
|
||||||
`, historyParams)
|
`, historyParams)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const historyClients = buildRfmClients(historyResult.rows.map(row => ({
|
const historyClients = buildRfmClients(historyResult.rows.map(row => ({
|
||||||
|
customerKey: row.customer_key,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
phone: row.phone,
|
phone: row.phone || '',
|
||||||
monetary: toNumber(row.monetary),
|
monetary: toNumber(row.monetary),
|
||||||
frequency: toNumber(row.frequency),
|
frequency: toNumber(row.frequency),
|
||||||
quantityPurchased: toNumber(row.quantity_purchased),
|
quantityPurchased: toNumber(row.quantity_purchased),
|
||||||
lastPurchaseDate: row.last_purchase_date,
|
lastPurchaseDate: row.last_purchase_date,
|
||||||
recencyDays: toNumber(row.recency_days)
|
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 clients = periodResult.rows.map(row => {
|
||||||
const taggedClient = tagsByPhone.get(row.phone);
|
const taggedClient = tagsByCustomerKey.get(row.customer_key);
|
||||||
if (!taggedClient) {
|
if (!taggedClient) {
|
||||||
const newCustomerSegment = getRfmSegment(3, 1);
|
const newCustomerSegment = getRfmSegment(3, 1);
|
||||||
return {
|
return {
|
||||||
|
customerKey: row.customer_key,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
phone: row.phone,
|
phone: row.phone || '',
|
||||||
monetary: toNumber(row.monetary),
|
monetary: toNumber(row.monetary),
|
||||||
frequency: toNumber(row.frequency),
|
frequency: toNumber(row.frequency),
|
||||||
quantityPurchased: toNumber(row.quantity_purchased),
|
quantityPurchased: toNumber(row.quantity_purchased),
|
||||||
@@ -334,8 +337,9 @@ const getRfmAnalytics = async (range = {}) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...taggedClient,
|
...taggedClient,
|
||||||
|
customerKey: row.customer_key,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
phone: row.phone,
|
phone: row.phone || '',
|
||||||
monetary: toNumber(row.monetary),
|
monetary: toNumber(row.monetary),
|
||||||
frequency: toNumber(row.frequency),
|
frequency: toNumber(row.frequency),
|
||||||
quantityPurchased: toNumber(row.quantity_purchased),
|
quantityPurchased: toNumber(row.quantity_purchased),
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected
|
|||||||
return {
|
return {
|
||||||
rows: [
|
rows: [
|
||||||
{
|
{
|
||||||
|
customer_key: '1',
|
||||||
name: 'Cliente Ontem',
|
name: 'Cliente Ontem',
|
||||||
phone: '1',
|
phone: '1',
|
||||||
monetary: 5000,
|
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
|
recency_days: endDate === '2026-06-13' ? 0 : 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
customer_key: '2',
|
||||||
name: 'Cliente Antigo',
|
name: 'Cliente Antigo',
|
||||||
phone: '2',
|
phone: '2',
|
||||||
monetary: 50,
|
monetary: 50,
|
||||||
@@ -222,6 +224,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected
|
|||||||
recency_days: 164
|
recency_days: 164
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
customer_key: '3',
|
||||||
name: 'Cliente Medio',
|
name: 'Cliente Medio',
|
||||||
phone: '3',
|
phone: '3',
|
||||||
monetary: 100,
|
monetary: 100,
|
||||||
@@ -237,6 +240,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected
|
|||||||
return {
|
return {
|
||||||
rows: [
|
rows: [
|
||||||
{
|
{
|
||||||
|
customer_key: '1',
|
||||||
name: 'Cliente Ontem',
|
name: 'Cliente Ontem',
|
||||||
phone: '1',
|
phone: '1',
|
||||||
monetary: 100,
|
monetary: 100,
|
||||||
@@ -245,6 +249,7 @@ test('getRfmAnalytics groups period buyers by their RFM tag before the selected
|
|||||||
last_purchase_date: '2026-06-14'
|
last_purchase_date: '2026-06-14'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
customer_key: '4',
|
||||||
name: 'Cliente Novo no Periodo',
|
name: 'Cliente Novo no Periodo',
|
||||||
phone: '4',
|
phone: '4',
|
||||||
monetary: 25,
|
monetary: 25,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import DateRangePicker from '../components/DateRangePicker';
|
|||||||
import type { ClientSortOption, ClientSummary } from '../analytics/clients';
|
import type { ClientSortOption, ClientSummary } from '../analytics/clients';
|
||||||
|
|
||||||
const clientTypeStyles: Record<string, string> = {
|
const clientTypeStyles: Record<string, string> = {
|
||||||
'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',
|
'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',
|
'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',
|
'Novo Cliente': 'border-cyan-500/30 bg-cyan-500/15 text-cyan-300',
|
||||||
@@ -20,7 +19,6 @@ const clientTypeStyles: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clientTypes = [
|
const clientTypes = [
|
||||||
'Sem análise',
|
|
||||||
'Campeão',
|
'Campeão',
|
||||||
'Potencial Leal',
|
'Potencial Leal',
|
||||||
'Novo Cliente',
|
'Novo Cliente',
|
||||||
@@ -102,9 +100,9 @@ const Clients = () => {
|
|||||||
|
|
||||||
const allClientsData = useMemo(() => {
|
const allClientsData = useMemo(() => {
|
||||||
const normalizedSearch = searchTerm.trim().toLowerCase();
|
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 clients = clientAnalytics.map((client): ClientSummary => {
|
||||||
const rfmClient = client.phone ? rfmByPhone.get(client.phone) : undefined;
|
const rfmClient = rfmByCustomerKey.get(client.customerKey);
|
||||||
return {
|
return {
|
||||||
name: client.name,
|
name: client.name,
|
||||||
phone: client.phone,
|
phone: client.phone,
|
||||||
@@ -113,7 +111,7 @@ const Clients = () => {
|
|||||||
totalItems: client.quantityPurchased,
|
totalItems: client.quantityPurchased,
|
||||||
orderCount: client.orderCount,
|
orderCount: client.orderCount,
|
||||||
lastPurchase: client.lastPurchaseDate ? new Date(client.lastPurchaseDate).getTime() : 0,
|
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',
|
rfmScore: rfmClient?.rfmScore || '000',
|
||||||
rfmPriority: rfmClient ? getRfmPriority(rfmClient) : 0
|
rfmPriority: rfmClient ? getRfmPriority(rfmClient) : 0
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export interface DashboardAnalytics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientAnalyticsItem {
|
export interface ClientAnalyticsItem {
|
||||||
|
customerKey: string;
|
||||||
name: string;
|
name: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
quantityPurchased: number;
|
quantityPurchased: number;
|
||||||
@@ -73,6 +74,7 @@ export interface ClientAnalyticsItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RfmClient {
|
export interface RfmClient {
|
||||||
|
customerKey: string;
|
||||||
name: string;
|
name: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
monetary: number;
|
monetary: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user