Fix RFM period date display
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user