Fix RFM period date display
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s

This commit is contained in:
Cauê Faleiros
2026-06-15 14:46:14 -03:00
parent 73033cac3d
commit ae73a543b6

View File

@@ -72,9 +72,30 @@ const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value); return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
}; };
const DAY_MS = 24 * 60 * 60 * 1000;
const parseLocalDate = (value: string) => {
if (!value) return null;
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (match) {
const [, year, month, day] = match;
return new Date(Number(year), Number(month) - 1, Number(day));
}
const parsedDate = new Date(value);
return Number.isNaN(parsedDate.getTime()) ? null : parsedDate;
};
const startOfDay = (date: Date) => {
const nextDate = new Date(date);
nextDate.setHours(0, 0, 0, 0);
return nextDate;
};
const formatDate = (value: string) => { const formatDate = (value: string) => {
if (!value) return 'N/A'; if (!value) return 'N/A';
return new Date(value).toLocaleDateString('pt-BR'); const date = parseLocalDate(value);
return date ? date.toLocaleDateString('pt-BR') : 'N/A';
}; };
const scoreLabel = (score: number) => { const scoreLabel = (score: number) => {
@@ -152,6 +173,15 @@ const Rfm = () => {
}); });
}, [visibleAnalytics]); }, [visibleAnalytics]);
const getPeriodRecencyDays = useCallback((lastPurchaseDate: string) => {
const purchaseDate = parseLocalDate(lastPurchaseDate);
if (!purchaseDate) return 0;
const purchaseDay = startOfDay(purchaseDate).getTime();
const rangeEndDay = startOfDay(dateRange.end).getTime();
return Math.max(0, Math.floor((rangeEndDay - purchaseDay) / DAY_MS));
}, [dateRange.end]);
const filteredClients = useMemo(() => { const filteredClients = useMemo(() => {
const normalizedSearch = searchTerm.trim().toLowerCase(); const normalizedSearch = searchTerm.trim().toLowerCase();
@@ -188,7 +218,7 @@ const Rfm = () => {
}>>((insights, segment) => { }>>((insights, segment) => {
const segmentClients = clients.filter(client => client.segmentKey === segment.key); const segmentClients = clients.filter(client => client.segmentKey === segment.key);
const totalOrders = segmentClients.reduce((sum, client) => sum + client.frequency, 0); const totalOrders = segmentClients.reduce((sum, client) => sum + client.frequency, 0);
const totalRecency = segmentClients.reduce((sum, client) => sum + client.recencyDays, 0); const totalRecency = segmentClients.reduce((sum, client) => sum + getPeriodRecencyDays(client.lastPurchaseDate), 0);
insights[segment.key] = { insights[segment.key] = {
averageRecencyDays: segmentClients.length ? totalRecency / segmentClients.length : 0, averageRecencyDays: segmentClients.length ? totalRecency / segmentClients.length : 0,
@@ -200,7 +230,7 @@ const Rfm = () => {
return insights; return insights;
}, {}); }, {});
}, [clients, segments, totals.totalRevenue]); }, [clients, segments, getPeriodRecencyDays, totals.totalRevenue]);
const selectedSegment = selectedSegmentKey ? getSegment(segments, selectedSegmentKey) : undefined; const selectedSegment = selectedSegmentKey ? getSegment(segments, selectedSegmentKey) : undefined;
const selectedSegmentStyle = segmentStyles[selectedSegment?.key || 'lost'] || segmentStyles.lost; const selectedSegmentStyle = segmentStyles[selectedSegment?.key || 'lost'] || segmentStyles.lost;
@@ -215,7 +245,7 @@ const Rfm = () => {
Recencia: client.recencyScore, Recencia: client.recencyScore,
Frequencia: client.frequencyScore, Frequencia: client.frequencyScore,
Monetario: client.monetaryScore, Monetario: client.monetaryScore,
'Dias desde ultima compra': client.recencyDays, 'Dias desde compra no periodo': getPeriodRecencyDays(client.lastPurchaseDate),
'Pedidos no periodo': client.frequency, 'Pedidos no periodo': client.frequency,
'Ticket medio no periodo (R$)': (client.frequency ? client.monetary / client.frequency : 0).toFixed(2).replace('.', ','), 'Ticket medio no periodo (R$)': (client.frequency ? client.monetary / client.frequency : 0).toFixed(2).replace('.', ','),
'Receita no periodo (R$)': client.monetary.toFixed(2).replace('.', ','), 'Receita no periodo (R$)': client.monetary.toFixed(2).replace('.', ','),
@@ -253,16 +283,16 @@ const Rfm = () => {
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm"> <div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
<p className="text-dark-muted text-sm font-medium mb-1">Base RFM</p> <p className="text-dark-muted text-sm font-medium mb-1">Clientes no Período</p>
<h3 className="text-3xl font-bold text-dark-text">{clients.length}</h3> <h3 className="text-3xl font-bold text-dark-text">{clients.length}</h3>
<p className="mt-1 text-xs font-semibold text-dark-muted">{totals.periodClientCount} com compra no período</p> <p className="mt-1 text-xs font-semibold text-dark-muted">Agrupados pela tag RFM anterior</p>
</div> </div>
<div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm"> <div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
<p className="text-dark-muted text-sm font-medium mb-1">Receita no Período</p> <p className="text-dark-muted text-sm font-medium mb-1">Receita no Período</p>
<h3 className="text-3xl font-bold text-dark-text">{formatCurrency(totals.totalRevenue)}</h3> <h3 className="text-3xl font-bold text-dark-text">{formatCurrency(totals.totalRevenue)}</h3>
</div> </div>
<div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm"> <div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
<p className="text-dark-muted text-sm font-medium mb-1">Segmentos na Base</p> <p className="text-dark-muted text-sm font-medium mb-1">Segmentos com Compra</p>
<h3 className="text-3xl font-bold text-dark-text">{totals.activeSegments}</h3> <h3 className="text-3xl font-bold text-dark-text">{totals.activeSegments}</h3>
<p className="mt-1 text-xs font-semibold text-dark-muted"> <p className="mt-1 text-xs font-semibold text-dark-muted">
Maior receita no período: {totals.topSegment?.label || 'N/A'} Maior receita no período: {totals.topSegment?.label || 'N/A'}
@@ -275,7 +305,7 @@ const Rfm = () => {
<div className="mb-4 flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between"> <div className="mb-4 flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div> <div>
<h2 className="text-lg font-bold text-dark-text">Matriz RFM</h2> <h2 className="text-lg font-bold text-dark-text">Matriz RFM</h2>
<p className="text-sm font-medium text-dark-muted">Base classificada até o fim do período; valores mostram apenas o período selecionado.</p> <p className="text-sm font-medium text-dark-muted">Compradores do período agrupados pela tag RFM anterior ao período.</p>
</div> </div>
<div className="flex items-center gap-2 text-xs font-semibold text-dark-muted"> <div className="flex items-center gap-2 text-xs font-semibold text-dark-muted">
<span>Menor prioridade</span> <span>Menor prioridade</span>
@@ -423,7 +453,7 @@ const Rfm = () => {
<p className="mt-1 text-base font-bold text-dark-text">{formatCurrency(selectedSegmentInsights.averageTicket)}</p> <p className="mt-1 text-base font-bold text-dark-text">{formatCurrency(selectedSegmentInsights.averageTicket)}</p>
</div> </div>
<div className="rounded-xl border border-dark-border bg-black/10 p-3"> <div className="rounded-xl border border-dark-border bg-black/10 p-3">
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Recência</p> <p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Compra período</p>
<p className="mt-1 text-base font-bold text-dark-text">{selectedSegmentInsights.averageRecencyDays.toFixed(0)} dias</p> <p className="mt-1 text-base font-bold text-dark-text">{selectedSegmentInsights.averageRecencyDays.toFixed(0)} dias</p>
</div> </div>
</div> </div>
@@ -521,7 +551,7 @@ const Rfm = () => {
</td> </td>
<td className="px-6 py-3 text-dark-muted font-medium"> <td className="px-6 py-3 text-dark-muted font-medium">
{formatDate(client.lastPurchaseDate)} {formatDate(client.lastPurchaseDate)}
<span className="block text-xs">{client.recencyDays} dias</span> <span className="block text-xs">{getPeriodRecencyDays(client.lastPurchaseDate)} dias</span>
</td> </td>
<td className="px-6 py-3 text-dark-text font-bold">{client.frequency}</td> <td className="px-6 py-3 text-dark-text font-bold">{client.frequency}</td>
<td className="px-6 py-3 text-dark-text font-bold">{formatCurrency(client.frequency ? client.monetary / client.frequency : 0)}</td> <td className="px-6 py-3 text-dark-text font-bold">{formatCurrency(client.frequency ? client.monetary / client.frequency : 0)}</td>