Compare commits

..

7 Commits

Author SHA1 Message Date
Cauê Faleiros
f88ae2d491 Keep client data visible when RFV fails
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m24s
2026-06-17 11:23:46 -03:00
Cauê Faleiros
4b3d65587e Reapply "Optimize RFV all-period analytics"
This reverts commit 19a29956d2.
2026-06-17 11:16:27 -03:00
Cauê Faleiros
bb69e79af6 Reapply "Fix RFV period classification"
This reverts commit 28af45c315.
2026-06-17 11:16:27 -03:00
Cauê Faleiros
28af45c315 Revert "Fix RFV period classification"
This reverts commit 1889c172d0.
2026-06-17 11:08:59 -03:00
Cauê Faleiros
19a29956d2 Revert "Optimize RFV all-period analytics"
This reverts commit 7fb2507f53.
2026-06-17 11:08:59 -03:00
Cauê Faleiros
ab4d7a3bd7 Revert "Normalize super admin login env values"
This reverts commit de49dd50d9.
2026-06-17 11:07:39 -03:00
Cauê Faleiros
de49dd50d9 Normalize super admin login env values 2026-06-17 11:02:30 -03:00
2 changed files with 133 additions and 14 deletions

View File

@@ -109,6 +109,128 @@ const authFetch = async (path: string, options: RequestInit = {}): Promise<Respo
return response; return response;
}; };
const rfmSegmentLabels: Record<string, string> = {
champions: 'Champions',
potential_loyalists: 'Potenciais Leais',
new_customers: 'Novos Clientes',
loyal_customers: 'Clientes Leais',
need_attention: 'Precisam de Atenção',
about_to_sleep: 'Quase Dormindo',
at_risk: 'Em Risco',
hibernating: 'Hibernando',
lost: 'Perdidos'
};
const rfmSegmentByScore: Record<string, string> = {
'3-3': 'champions',
'3-2': 'potential_loyalists',
'3-1': 'new_customers',
'2-3': 'loyal_customers',
'2-2': 'need_attention',
'2-1': 'about_to_sleep',
'1-3': 'at_risk',
'1-2': 'hibernating',
'1-1': 'lost'
};
const scoreTertile = (value: number, values: number[], higherIsBetter = true): 1 | 2 | 3 => {
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 higherIsBetter ? 2 : 3;
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)) as 1 | 2 | 3;
};
const fetchClientAnalyticsForRange = async (dateRange: DateRange): Promise<ClientAnalyticsItem[]> => {
const params = new URLSearchParams({
start: formatDateParam(dateRange.start),
end: formatDateParam(dateRange.end)
});
const response = await authFetch(`/analytics/clients?${params.toString()}`);
if (!response.ok) return [];
return await response.json();
};
const buildFallbackRfmAnalytics = async (dateRange: DateRange): Promise<RfmAnalytics | null> => {
let clientRows: ClientAnalyticsItem[];
try {
clientRows = await fetchClientAnalyticsForRange(dateRange);
} catch (error) {
console.error('Fetch fallback RFM analytics failed', error);
return null;
}
if (!clientRows.length) return null;
const rangeEndTime = dateRange.end.getTime();
const recencyValues = clientRows.map(client => {
const lastPurchaseTime = client.lastPurchaseDate ? new Date(client.lastPurchaseDate).getTime() : rangeEndTime;
return Math.max(0, Math.floor((rangeEndTime - lastPurchaseTime) / 86400000));
});
const frequencyValues = clientRows.map(client => client.orderCount);
const monetaryValues = clientRows.map(client => client.totalSpent);
const clients = clientRows.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))) as 1 | 2 | 3;
const segmentKey = rfmSegmentByScore[`${recencyScore}-${valueScore}`] || 'lost';
return {
customerKey: client.customerKey,
name: client.name,
phone: client.phone,
monetary: client.totalSpent,
frequency: client.orderCount,
quantityPurchased: client.quantityPurchased,
lastPurchaseDate: client.lastPurchaseDate,
recencyDays: recencyValues[index],
recencyScore,
frequencyScore,
monetaryScore,
valueScore,
rfmScore: `${recencyScore}${frequencyScore}${monetaryScore}`,
segmentKey,
segmentLabel: rfmSegmentLabels[segmentKey] || 'Perdidos'
};
});
const segments = Object.entries(rfmSegmentLabels).map(([key, label]) => {
const segmentClients = clients.filter(client => client.segmentKey === key);
const totalRevenue = segmentClients.reduce((sum, client) => sum + client.monetary, 0);
return {
key,
label,
count: segmentClients.length,
totalRevenue,
averageRevenue: segmentClients.length ? totalRevenue / segmentClients.length : 0
};
});
return {
range: {
start: formatDateParam(dateRange.start),
end: formatDateParam(dateRange.end)
},
clients,
segments,
matrix: {
recencyScores: [3, 2, 1],
valueScores: [1, 2, 3]
}
};
};
export const fetchDashboardAnalytics = async (dateRange: DateRange): Promise<DashboardAnalytics | null> => { export const fetchDashboardAnalytics = async (dateRange: DateRange): Promise<DashboardAnalytics | null> => {
try { try {
const params = new URLSearchParams({ const params = new URLSearchParams({
@@ -131,23 +253,17 @@ export const fetchRfmAnalytics = async (dateRange: DateRange): Promise<RfmAnalyt
end: formatDateParam(dateRange.end) end: formatDateParam(dateRange.end)
}); });
const response = await authFetch(`/analytics/rfm?${params.toString()}`); const response = await authFetch(`/analytics/rfm?${params.toString()}`);
if (!response.ok) return null; if (!response.ok) return await buildFallbackRfmAnalytics(dateRange);
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error('Fetch RFM analytics failed', error); console.error('Fetch RFM analytics failed', error);
return null; return await buildFallbackRfmAnalytics(dateRange);
} }
}; };
export const fetchClientAnalytics = async (dateRange: DateRange): Promise<ClientAnalyticsItem[]> => { export const fetchClientAnalytics = async (dateRange: DateRange): Promise<ClientAnalyticsItem[]> => {
try { try {
const params = new URLSearchParams({ return await fetchClientAnalyticsForRange(dateRange);
start: formatDateParam(dateRange.start),
end: formatDateParam(dateRange.end)
});
const response = await authFetch(`/analytics/clients?${params.toString()}`);
if (!response.ok) return [];
return await response.json();
} catch (error) { } catch (error) {
console.error('Fetch client analytics failed', error); console.error('Fetch client analytics failed', error);
return []; return [];

View File

@@ -7,6 +7,7 @@ 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-500/30 bg-zinc-500/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',
@@ -19,6 +20,7 @@ const clientTypeStyles: Record<string, string> = {
}; };
const clientTypes = [ const clientTypes = [
'Sem análise',
'Campeão', 'Campeão',
'Potencial Leal', 'Potencial Leal',
'Novo Cliente', 'Novo Cliente',
@@ -81,12 +83,13 @@ const Clients = () => {
let isMounted = true; let isMounted = true;
const loadClients = async () => { const loadClients = async () => {
const [clientsData, rfmData] = await Promise.all([ const clientsData = await fetchClientAnalytics(dateRange);
fetchClientAnalytics(dateRange),
fetchRfmAnalytics(dateRange)
]);
if (isMounted) { if (isMounted) {
setClientAnalytics(clientsData); setClientAnalytics(clientsData);
}
const rfmData = await fetchRfmAnalytics(dateRange);
if (isMounted) {
setRfmAnalytics(rfmData); setRfmAnalytics(rfmData);
} }
}; };
@@ -111,7 +114,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) : 'Perdido', clientType: rfmClient ? (backendSegmentToClientType[rfmClient.segmentKey] || rfmClient.segmentLabel) : 'Sem análise',
rfmScore: rfmClient?.rfmScore || '000', rfmScore: rfmClient?.rfmScore || '000',
rfmPriority: rfmClient ? getRfmPriority(rfmClient) : 0 rfmPriority: rfmClient ? getRfmPriority(rfmClient) : 0
}; };