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);
|
||||
};
|
||||
|
||||
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) => {
|
||||
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) => {
|
||||
@@ -152,6 +173,15 @@ const Rfm = () => {
|
||||
});
|
||||
}, [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 normalizedSearch = searchTerm.trim().toLowerCase();
|
||||
|
||||
@@ -188,7 +218,7 @@ const Rfm = () => {
|
||||
}>>((insights, segment) => {
|
||||
const segmentClients = clients.filter(client => client.segmentKey === segment.key);
|
||||
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] = {
|
||||
averageRecencyDays: segmentClients.length ? totalRecency / segmentClients.length : 0,
|
||||
@@ -200,7 +230,7 @@ const Rfm = () => {
|
||||
|
||||
return insights;
|
||||
}, {});
|
||||
}, [clients, segments, totals.totalRevenue]);
|
||||
}, [clients, segments, getPeriodRecencyDays, totals.totalRevenue]);
|
||||
|
||||
const selectedSegment = selectedSegmentKey ? getSegment(segments, selectedSegmentKey) : undefined;
|
||||
const selectedSegmentStyle = segmentStyles[selectedSegment?.key || 'lost'] || segmentStyles.lost;
|
||||
@@ -215,7 +245,7 @@ const Rfm = () => {
|
||||
Recencia: client.recencyScore,
|
||||
Frequencia: client.frequencyScore,
|
||||
Monetario: client.monetaryScore,
|
||||
'Dias desde ultima compra': client.recencyDays,
|
||||
'Dias desde compra no periodo': getPeriodRecencyDays(client.lastPurchaseDate),
|
||||
'Pedidos no periodo': client.frequency,
|
||||
'Ticket medio no periodo (R$)': (client.frequency ? client.monetary / client.frequency : 0).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="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>
|
||||
<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 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>
|
||||
<h3 className="text-3xl font-bold text-dark-text">{formatCurrency(totals.totalRevenue)}</h3>
|
||||
</div>
|
||||
<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>
|
||||
<p className="mt-1 text-xs font-semibold text-dark-muted">
|
||||
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>
|
||||
<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 className="flex items-center gap-2 text-xs font-semibold text-dark-muted">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -521,7 +551,7 @@ const Rfm = () => {
|
||||
</td>
|
||||
<td className="px-6 py-3 text-dark-muted font-medium">
|
||||
{formatDate(client.lastPurchaseDate)}
|
||||
<span className="block text-xs">{client.recencyDays} dias</span>
|
||||
<span className="block text-xs">{getPeriodRecencyDays(client.lastPurchaseDate)} dias</span>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user