Keep client data visible when RFV fails
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m24s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m24s
This commit is contained in:
@@ -109,6 +109,128 @@ const authFetch = async (path: string, options: RequestInit = {}): Promise<Respo
|
||||
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> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
@@ -131,23 +253,17 @@ export const fetchRfmAnalytics = async (dateRange: DateRange): Promise<RfmAnalyt
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const response = await authFetch(`/analytics/rfm?${params.toString()}`);
|
||||
if (!response.ok) return null;
|
||||
if (!response.ok) return await buildFallbackRfmAnalytics(dateRange);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch RFM analytics failed', error);
|
||||
return null;
|
||||
return await buildFallbackRfmAnalytics(dateRange);
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchClientAnalytics = async (dateRange: DateRange): Promise<ClientAnalyticsItem[]> => {
|
||||
try {
|
||||
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();
|
||||
return await fetchClientAnalyticsForRange(dateRange);
|
||||
} catch (error) {
|
||||
console.error('Fetch client analytics failed', error);
|
||||
return [];
|
||||
|
||||
@@ -7,6 +7,7 @@ import DateRangePicker from '../components/DateRangePicker';
|
||||
import type { ClientSortOption, ClientSummary } from '../analytics/clients';
|
||||
|
||||
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',
|
||||
'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',
|
||||
@@ -19,6 +20,7 @@ const clientTypeStyles: Record<string, string> = {
|
||||
};
|
||||
|
||||
const clientTypes = [
|
||||
'Sem análise',
|
||||
'Campeão',
|
||||
'Potencial Leal',
|
||||
'Novo Cliente',
|
||||
@@ -81,12 +83,13 @@ const Clients = () => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadClients = async () => {
|
||||
const [clientsData, rfmData] = await Promise.all([
|
||||
fetchClientAnalytics(dateRange),
|
||||
fetchRfmAnalytics(dateRange)
|
||||
]);
|
||||
const clientsData = await fetchClientAnalytics(dateRange);
|
||||
if (isMounted) {
|
||||
setClientAnalytics(clientsData);
|
||||
}
|
||||
|
||||
const rfmData = await fetchRfmAnalytics(dateRange);
|
||||
if (isMounted) {
|
||||
setRfmAnalytics(rfmData);
|
||||
}
|
||||
};
|
||||
@@ -111,7 +114,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) : 'Perdido',
|
||||
clientType: rfmClient ? (backendSegmentToClientType[rfmClient.segmentKey] || rfmClient.segmentLabel) : 'Sem análise',
|
||||
rfmScore: rfmClient?.rfmScore || '000',
|
||||
rfmPriority: rfmClient ? getRfmPriority(rfmClient) : 0
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user