Add RFM segmentation analytics
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m12s

This commit is contained in:
Cauê Faleiros
2026-06-12 11:22:57 -03:00
parent 25ab6cd448
commit fe8d0ff105
10 changed files with 1231 additions and 14 deletions

740
src/pages/Rfm.tsx Normal file
View File

@@ -0,0 +1,740 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useOutletContext } from 'react-router-dom';
import { Database, Download, Filter, Loader2, Search, Sparkles, Users } from 'lucide-react';
import DateRangePicker from '../components/DateRangePicker';
import { exportToCSV, fetchRfmAnalytics } from '../dataService';
import type { DateRange, RfmAnalytics, RfmClient, RfmSegment } from '../types';
const emptyClients: RfmClient[] = [];
const emptySegments: RfmSegment[] = [];
const segmentStyles: Record<string, { accent: string; bg: string; text: string }> = {
champions: { accent: 'bg-emerald-500', bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
potential_loyalists: { accent: 'bg-sky-500', bg: 'bg-sky-500/10', text: 'text-sky-400' },
new_customers: { accent: 'bg-cyan-500', bg: 'bg-cyan-500/10', text: 'text-cyan-400' },
loyal_customers: { accent: 'bg-blue-500', bg: 'bg-blue-500/10', text: 'text-blue-400' },
need_attention: { accent: 'bg-amber-500', bg: 'bg-amber-500/10', text: 'text-amber-400' },
about_to_sleep: { accent: 'bg-orange-500', bg: 'bg-orange-500/10', text: 'text-orange-400' },
at_risk: { accent: 'bg-rose-500', bg: 'bg-rose-500/10', text: 'text-rose-400' },
hibernating: { accent: 'bg-fuchsia-500', bg: 'bg-fuchsia-500/10', text: 'text-fuchsia-400' },
lost: { accent: 'bg-zinc-500', bg: 'bg-zinc-500/10', text: 'text-zinc-400' }
};
const rfmSegmentDefinitions: Array<Pick<RfmSegment, 'key' | 'label'>> = [
{ key: 'champions', label: 'Champions' },
{ key: 'potential_loyalists', label: 'Potenciais Leais' },
{ key: 'new_customers', label: 'Novos Clientes' },
{ key: 'loyal_customers', label: 'Clientes Leais' },
{ key: 'need_attention', label: 'Precisam de Atenção' },
{ key: 'about_to_sleep', label: 'Quase Dormindo' },
{ key: 'at_risk', label: 'Em Risco' },
{ key: 'hibernating', label: 'Hibernando' },
{ key: 'lost', label: 'Perdidos' }
];
const segmentDescriptions: Record<string, string> = {
champions: 'Recentes, frequentes e valiosos',
potential_loyalists: 'Recentes com bom potencial',
new_customers: 'Primeira compra recente',
loyal_customers: 'Valiosos, mas menos recentes',
need_attention: 'Base intermediária para nutrir',
about_to_sleep: 'Baixo valor começando a esfriar',
at_risk: 'Valiosos sem compra recente',
hibernating: 'Sem compra recente e valor médio',
lost: 'Baixa atividade e distante'
};
const segmentActions: Record<string, string> = {
champions: 'Oferecer acesso antecipado, benefícios VIP e lançamentos.',
potential_loyalists: 'Estimular a próxima compra com recomendações personalizadas.',
new_customers: 'Enviar boas-vindas e incentivo para a segunda compra.',
loyal_customers: 'Manter relacionamento com ofertas relevantes e recorrentes.',
need_attention: 'Reativar interesse com campanha leve e produtos recentes.',
about_to_sleep: 'Enviar lembrete antes que o cliente esfrie completamente.',
at_risk: 'Priorizar recuperação com oferta forte ou contato direto.',
hibernating: 'Testar reativação de baixo custo com mensagem objetiva.',
lost: 'Evitar alto investimento; usar apenas campanhas ocasionais.'
};
const segmentByScore: 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 demoClients: RfmClient[] = [
{
name: 'Ana Paula Ribeiro',
phone: '+55 11 90000-0001',
monetary: 4820,
frequency: 12,
quantityPurchased: 38,
lastPurchaseDate: '2026-06-10',
recencyDays: 1,
recencyScore: 3,
frequencyScore: 3,
monetaryScore: 3,
valueScore: 3,
rfmScore: '333',
segmentKey: 'champions',
segmentLabel: 'Champions'
},
{
name: 'Bruno Ferreira',
phone: '+55 11 90000-0002',
monetary: 1680,
frequency: 5,
quantityPurchased: 14,
lastPurchaseDate: '2026-06-09',
recencyDays: 2,
recencyScore: 3,
frequencyScore: 2,
monetaryScore: 2,
valueScore: 2,
rfmScore: '322',
segmentKey: 'potential_loyalists',
segmentLabel: 'Potenciais Leais'
},
{
name: 'Carla Mendes',
phone: '+55 11 90000-0003',
monetary: 249,
frequency: 1,
quantityPurchased: 2,
lastPurchaseDate: '2026-06-08',
recencyDays: 3,
recencyScore: 3,
frequencyScore: 1,
monetaryScore: 1,
valueScore: 1,
rfmScore: '311',
segmentKey: 'new_customers',
segmentLabel: 'Novos Clientes'
},
{
name: 'Diego Nascimento',
phone: '+55 11 90000-0004',
monetary: 3920,
frequency: 10,
quantityPurchased: 31,
lastPurchaseDate: '2026-05-24',
recencyDays: 18,
recencyScore: 2,
frequencyScore: 3,
monetaryScore: 3,
valueScore: 3,
rfmScore: '233',
segmentKey: 'loyal_customers',
segmentLabel: 'Clientes Leais'
},
{
name: 'Elisa Costa',
phone: '+55 11 90000-0005',
monetary: 1210,
frequency: 4,
quantityPurchased: 9,
lastPurchaseDate: '2026-05-18',
recencyDays: 24,
recencyScore: 2,
frequencyScore: 2,
monetaryScore: 2,
valueScore: 2,
rfmScore: '222',
segmentKey: 'need_attention',
segmentLabel: 'Precisam de Atenção'
},
{
name: 'Felipe Azevedo',
phone: '+55 11 90000-0006',
monetary: 390,
frequency: 2,
quantityPurchased: 3,
lastPurchaseDate: '2026-05-12',
recencyDays: 30,
recencyScore: 2,
frequencyScore: 1,
monetaryScore: 1,
valueScore: 1,
rfmScore: '211',
segmentKey: 'about_to_sleep',
segmentLabel: 'Quase Dormindo'
},
{
name: 'Gabriela Martins',
phone: '+55 11 90000-0007',
monetary: 5350,
frequency: 14,
quantityPurchased: 44,
lastPurchaseDate: '2026-03-28',
recencyDays: 75,
recencyScore: 1,
frequencyScore: 3,
monetaryScore: 3,
valueScore: 3,
rfmScore: '133',
segmentKey: 'at_risk',
segmentLabel: 'Em Risco'
},
{
name: 'Henrique Lima',
phone: '+55 11 90000-0008',
monetary: 980,
frequency: 4,
quantityPurchased: 8,
lastPurchaseDate: '2026-03-06',
recencyDays: 97,
recencyScore: 1,
frequencyScore: 2,
monetaryScore: 2,
valueScore: 2,
rfmScore: '122',
segmentKey: 'hibernating',
segmentLabel: 'Hibernando'
},
{
name: 'Isabela Rocha',
phone: '+55 11 90000-0009',
monetary: 120,
frequency: 1,
quantityPurchased: 1,
lastPurchaseDate: '2026-01-29',
recencyDays: 133,
recencyScore: 1,
frequencyScore: 1,
monetaryScore: 1,
valueScore: 1,
rfmScore: '111',
segmentKey: 'lost',
segmentLabel: 'Perdidos'
}
];
const demoSegments: RfmSegment[] = rfmSegmentDefinitions.map(segment => {
const segmentClients = demoClients.filter(client => client.segmentKey === segment.key);
const totalRevenue = segmentClients.reduce((sum, client) => sum + client.monetary, 0);
return {
...segment,
count: segmentClients.length,
totalRevenue,
averageRevenue: segmentClients.length ? totalRevenue / segmentClients.length : 0
};
});
const demoAnalytics: RfmAnalytics = {
range: {
start: '2026-01-01',
end: '2026-06-11'
},
clients: demoClients,
segments: demoSegments,
matrix: {
recencyScores: [3, 2, 1],
valueScores: [1, 2, 3]
}
};
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
};
const formatDate = (value: string) => {
if (!value) return 'N/A';
return new Date(value).toLocaleDateString('pt-BR');
};
const scoreLabel = (score: number) => {
if (score === 3) return 'Alto';
if (score === 2) return 'Médio';
return 'Baixo';
};
const getSegment = (segments: RfmSegment[], key: string) => {
return segments.find(segment => segment.key === key);
};
const segmentForCell = (segments: RfmSegment[], recencyScore: number, valueScore: number) => {
const matrixKey = `${recencyScore}-${valueScore}`;
return getSegment(segments, segmentByScore[matrixKey]);
};
const ScoreBadge = ({ value }: { value: number }) => (
<span className="inline-flex h-7 w-7 items-center justify-center rounded-lg border border-dark-border bg-dark-input text-xs font-bold text-dark-text">
{value}
</span>
);
const Rfm = () => {
const { dateRange, setDateRange, refreshInterval, setRefreshInterval } = useOutletContext<{
dateRange: DateRange;
setDateRange: (range: DateRange) => void;
refreshInterval: number;
setRefreshInterval: (interval: number) => void;
}>();
const [analytics, setAnalytics] = useState<RfmAnalytics | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [segmentFilter, setSegmentFilter] = useState('all');
const [selectedSegmentKey, setSelectedSegmentKey] = useState('');
const [isDemoMode, setIsDemoMode] = useState(false);
const loadRfm = useCallback(async (range: DateRange) => {
setIsLoading(true);
const data = await fetchRfmAnalytics(range);
setAnalytics(data);
setIsLoading(false);
}, []);
useEffect(() => {
// RFM is calculated server-side from the selected date range.
// eslint-disable-next-line react-hooks/set-state-in-effect
void loadRfm(dateRange);
}, [dateRange, loadRfm]);
useEffect(() => {
if (refreshInterval === 0) return;
const intervalId = setInterval(() => {
void loadRfm(dateRange);
}, refreshInterval);
return () => clearInterval(intervalId);
}, [dateRange, loadRfm, refreshInterval]);
const visibleAnalytics = isDemoMode ? demoAnalytics : analytics;
const clients = visibleAnalytics?.clients || emptyClients;
const segments = useMemo(() => {
const sourceSegments = visibleAnalytics?.segments || emptySegments;
return rfmSegmentDefinitions.map(definition => {
const segment = sourceSegments.find(item => item.key === definition.key);
return segment || {
...definition,
count: 0,
totalRevenue: 0,
averageRevenue: 0
};
});
}, [visibleAnalytics]);
const toggleDemoMode = () => {
setIsDemoMode(current => !current);
setSegmentFilter('all');
setSearchTerm('');
setSelectedSegmentKey('');
};
const filteredClients = useMemo(() => {
const normalizedSearch = searchTerm.trim().toLowerCase();
return clients.filter(client => {
const matchesSegment = segmentFilter === 'all' || client.segmentKey === segmentFilter;
const matchesSearch = !normalizedSearch ||
client.name.toLowerCase().includes(normalizedSearch) ||
client.phone.toLowerCase().includes(normalizedSearch);
return matchesSegment && matchesSearch;
});
}, [clients, searchTerm, segmentFilter]);
const totals = useMemo(() => {
const totalRevenue = clients.reduce((sum, client) => sum + client.monetary, 0);
const activeSegments = segments.filter(segment => segment.count > 0).length;
const topSegment = [...segments].sort((a, b) => b.totalRevenue - a.totalRevenue)[0];
return { totalRevenue, activeSegments, topSegment };
}, [clients, segments]);
const segmentInsights = useMemo(() => {
return segments.reduce<Record<string, {
averageRecencyDays: number;
averageTicket: number;
customerPercent: number;
revenuePercent: number;
topClients: RfmClient[];
}>>((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);
insights[segment.key] = {
averageRecencyDays: segmentClients.length ? totalRecency / segmentClients.length : 0,
averageTicket: totalOrders ? segment.totalRevenue / totalOrders : 0,
customerPercent: clients.length ? (segment.count / clients.length) * 100 : 0,
revenuePercent: totals.totalRevenue ? (segment.totalRevenue / totals.totalRevenue) * 100 : 0,
topClients: [...segmentClients].sort((a, b) => b.monetary - a.monetary).slice(0, 3)
};
return insights;
}, {});
}, [clients, segments, totals.totalRevenue]);
const selectedSegment = selectedSegmentKey ? getSegment(segments, selectedSegmentKey) : undefined;
const selectedSegmentStyle = segmentStyles[selectedSegment?.key || 'lost'] || segmentStyles.lost;
const selectedSegmentInsights = selectedSegment ? segmentInsights[selectedSegment.key] : null;
const handleExport = () => {
const exportData = filteredClients.map(client => ({
Cliente: client.name,
Telefone: client.phone,
Segmento: client.segmentLabel,
RFM: client.rfmScore,
Recencia: client.recencyScore,
Frequencia: client.frequencyScore,
Monetario: client.monetaryScore,
'Dias desde ultima compra': client.recencyDays,
Pedidos: client.frequency,
'Ticket medio (R$)': (client.frequency ? client.monetary / client.frequency : 0).toFixed(2).replace('.', ','),
'Total gasto (R$)': client.monetary.toFixed(2).replace('.', ','),
'Ultima compra': formatDate(client.lastPurchaseDate)
}));
exportToCSV(exportData, `rfm_${new Date().toISOString().split('T')[0]}.csv`);
};
return (
<div className="space-y-6">
<div className="flex flex-col xl:flex-row xl:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold mb-2 text-dark-text">RFM</h1>
<p className="text-dark-muted font-medium">Segmentação de clientes por recência, frequência e valor monetário.</p>
</div>
<div className="flex flex-col sm:flex-row flex-wrap gap-3 items-start sm:items-center">
<button
onClick={toggleDemoMode}
className={`inline-flex items-center justify-center gap-2 rounded-xl border px-4 py-2.5 text-sm font-bold transition-colors cursor-pointer ${
isDemoMode
? 'border-amber-500/40 bg-amber-500/10 text-amber-300 hover:bg-amber-500/15'
: 'border-dark-border bg-dark-card text-dark-text hover:border-brand-primary'
}`}
>
{isDemoMode ? <Database className="h-4 w-4" /> : <Sparkles className="h-4 w-4" />}
{isDemoMode ? 'Voltar para dados reais' : 'Ver dados demo'}
</button>
<DateRangePicker
dateRange={dateRange}
onChange={setDateRange}
refreshInterval={refreshInterval}
setRefreshInterval={setRefreshInterval}
onManualRefresh={() => void loadRfm(dateRange)}
/>
</div>
</div>
{isDemoMode && (
<div className="rounded-2xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm font-semibold text-amber-200">
Visualização com dados fictícios. Nenhum cliente real ou dado do banco foi alterado.
</div>
)}
{!isDemoMode && isLoading && (
<div className="inline-flex items-center gap-2 rounded-xl border border-dark-border bg-dark-card px-3 py-2 text-sm font-semibold text-dark-muted">
<Loader2 className="h-4 w-4 animate-spin text-brand-primary" />
Atualizando RFM
</div>
)}
<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">Clientes Classificados</p>
<h3 className="text-3xl font-bold text-dark-text">{clients.length}</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">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 Ativos</p>
<h3 className="text-3xl font-bold text-dark-text">{totals.activeSegments}</h3>
<p className="mt-1 text-xs font-semibold text-dark-muted">
Principal: {totals.topSegment?.label || 'N/A'}
</p>
</div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_340px] gap-6">
<section className="bg-dark-card p-5 rounded-2xl border border-dark-border shadow-sm">
<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">Distribuição dos clientes por recência e valor.</p>
</div>
<div className="flex items-center gap-2 text-xs font-semibold text-dark-muted">
<span>Menor prioridade</span>
<span className="h-2 w-12 rounded-full bg-gradient-to-r from-rose-500 via-amber-400 to-emerald-500" />
<span>Maior prioridade</span>
</div>
</div>
<div className="overflow-x-auto">
<div className="grid min-w-[720px] grid-cols-[104px_repeat(3,minmax(0,1fr))] gap-2">
<div className="flex items-center justify-center rounded-xl border border-dark-border bg-dark-input/60 px-3 py-2.5 text-center">
<div>
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Eixos</p>
<p className="text-xs font-semibold text-dark-text">Recência / Valor</p>
</div>
</div>
{[1, 2, 3].map(valueScore => (
<div key={valueScore} className="rounded-xl border border-dark-border bg-dark-input/70 px-3 py-2.5 text-center">
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Valor {valueScore}</p>
<p className="text-xs font-bold text-dark-text">{scoreLabel(valueScore)}</p>
</div>
))}
{[3, 2, 1].map(recencyScore => (
<div key={recencyScore} className="contents">
<div className="flex min-h-28 items-center justify-center rounded-xl border border-dark-border bg-dark-input/70 px-2.5 text-center">
<div>
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Rec. {recencyScore}</p>
<p className="text-xs font-bold text-dark-text">{scoreLabel(recencyScore)}</p>
<p className="mt-1 text-[10px] font-semibold text-dark-muted">
{recencyScore === 3 ? 'Compra recente' : recencyScore === 2 ? 'Intermediário' : 'Distante'}
</p>
</div>
</div>
{[1, 2, 3].map(valueScore => {
const segment = segmentForCell(segments, recencyScore, valueScore);
const style = segmentStyles[segment?.key || 'lost'];
const isActive = segmentFilter === segment?.key;
const intensity = segment?.count ? 'opacity-100' : 'opacity-75';
return (
<button
key={`${recencyScore}-${valueScore}`}
onClick={() => {
setSegmentFilter(segment?.key || 'all');
setSelectedSegmentKey(segment?.key || '');
}}
className={`group min-h-28 rounded-xl border p-3 text-left transition-all hover:-translate-y-0.5 hover:border-brand-primary cursor-pointer ${
isActive ? 'border-brand-primary ring-1 ring-brand-primary/50' : 'border-dark-border'
} ${style.bg} ${intensity}`}
>
<div className="flex h-full flex-col justify-between gap-3">
<div>
<div className="mb-2 flex items-start justify-between gap-2">
<span className={`h-2 w-8 rounded-full ${style.accent}`} />
<span className="rounded-md border border-dark-border bg-black/10 px-1.5 py-0.5 text-[10px] font-bold text-dark-muted">
R{recencyScore} V{valueScore}
</span>
</div>
<p className="text-sm font-bold text-dark-text">{segment?.label}</p>
<p className="mt-1 truncate text-xs font-medium text-dark-muted">
{segmentDescriptions[segment?.key || 'lost']}
</p>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Clientes</p>
<p className={`text-xl font-bold leading-none ${style.text}`}>{segment?.count || 0}</p>
</div>
<div>
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Receita</p>
<p className="truncate text-xs font-bold text-dark-text">{formatCurrency(segment?.totalRevenue || 0)}</p>
</div>
</div>
</div>
</button>
);
})}
</div>
))}
</div>
</div>
</section>
<section className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
<h2 className="mb-5 text-lg font-bold text-dark-text">Segmentos</h2>
<div className="space-y-2">
{segments.map(segment => {
const style = segmentStyles[segment.key] || segmentStyles.lost;
return (
<button
key={segment.key}
onClick={() => {
setSegmentFilter(segment.key);
setSelectedSegmentKey(segment.key);
}}
className={`w-full rounded-xl border px-4 py-3 text-left transition-colors cursor-pointer ${
segmentFilter === segment.key ? 'border-brand-primary bg-dark-input' : 'border-dark-border bg-transparent hover:border-brand-primary'
}`}
>
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<span className={`h-2.5 w-2.5 shrink-0 rounded-full ${style.accent}`} />
<span className="truncate text-sm font-bold text-dark-text">{segment.label}</span>
</div>
<span className={`text-sm font-bold ${style.text}`}>{segment.count}</span>
</div>
<p className="mt-1 text-xs font-semibold text-dark-muted">
{(segmentInsights[segment.key]?.customerPercent || 0).toFixed(1)}% clientes · {formatCurrency(segment.totalRevenue)}
</p>
</button>
);
})}
</div>
</section>
</div>
{selectedSegment && selectedSegmentInsights && (
<section className={`rounded-2xl border border-dark-border ${selectedSegmentStyle.bg} p-4 shadow-sm`}>
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<div className="min-w-0">
<div className="mb-1 flex items-center gap-2">
<span className={`h-2.5 w-2.5 rounded-full ${selectedSegmentStyle.accent}`} />
<p className={`text-xs font-bold uppercase tracking-widest ${selectedSegmentStyle.text}`}>Segmento selecionado</p>
</div>
<h3 className="text-lg font-bold text-dark-text">{selectedSegment.label}</h3>
<p className="mt-1 text-sm font-semibold text-dark-muted">{segmentActions[selectedSegment.key]}</p>
</div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 xl:w-[560px]">
<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">Clientes</p>
<p className="mt-1 text-base font-bold text-dark-text">{selectedSegmentInsights.customerPercent.toFixed(1)}%</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">Receita</p>
<p className="mt-1 text-base font-bold text-dark-text">{selectedSegmentInsights.revenuePercent.toFixed(1)}%</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">Ticket</p>
<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="mt-1 text-base font-bold text-dark-text">{selectedSegmentInsights.averageRecencyDays.toFixed(0)} dias</p>
</div>
</div>
</div>
</section>
)}
<section className="bg-dark-card border border-dark-border rounded-2xl overflow-hidden shadow-sm">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 border-b border-dark-border p-4">
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative">
<Filter className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-dark-muted" />
<select
value={segmentFilter}
onChange={(event) => setSegmentFilter(event.target.value)}
className="appearance-none bg-dark-input border border-dark-border text-dark-text text-sm rounded-xl pl-9 pr-8 py-2.5 focus:outline-none focus:border-brand-primary transition-colors shadow-sm cursor-pointer"
>
<option value="all">Todos os segmentos</option>
{segments.map(segment => (
<option key={segment.key} value={segment.key}>{segment.label}</option>
))}
</select>
</div>
<div className="relative">
<Search className="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-dark-muted" />
<input
type="text"
placeholder="Buscar cliente..."
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
className="w-full sm:w-72 bg-dark-input border border-dark-border text-dark-text rounded-xl pl-10 pr-4 py-2.5 focus:outline-none focus:border-brand-primary hover:border-brand-primary transition-colors shadow-sm"
/>
</div>
</div>
<button
onClick={handleExport}
disabled={!filteredClients.length}
className="flex items-center justify-center gap-2 bg-dark-input border border-dark-border px-4 py-2.5 rounded-xl shadow-sm hover:border-brand-primary transition-colors text-sm font-medium text-dark-text cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
title="Exportar para CSV"
>
<Download size={16} className="text-brand-primary" />
Exportar
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="bg-dark-header border-b border-dark-border text-dark-muted">
<tr>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Cliente</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Segmento</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">RFM</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Última Compra</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Pedidos</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Ticket Médio</th>
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Total Gasto</th>
</tr>
</thead>
<tbody className="divide-y divide-dark-border">
{filteredClients.map((client: RfmClient) => {
const style = segmentStyles[client.segmentKey] || segmentStyles.lost;
return (
<tr key={client.phone} className="hover:bg-dark-input/50 transition-colors">
<td className="px-6 py-3">
{isDemoMode ? (
<div className="flex items-center gap-3">
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-dark-input text-dark-muted">
<Users className="h-4 w-4" />
</span>
<span>
<span className="block font-bold text-dark-text">{client.name}</span>
<span className="block text-xs font-medium text-dark-muted">{client.phone || 'N/A'}</span>
</span>
</div>
) : (
<Link to={`/clients/${encodeURIComponent(client.name)}`} className="flex items-center gap-3 hover:text-brand-primary transition-colors">
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-dark-input text-dark-muted">
<Users className="h-4 w-4" />
</span>
<span>
<span className="block font-bold text-dark-text">{client.name}</span>
<span className="block text-xs font-medium text-dark-muted">{client.phone || 'N/A'}</span>
</span>
</Link>
)}
</td>
<td className="px-6 py-3">
<span className={`inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-bold ${style.bg} ${style.text}`}>
<span className={`h-2 w-2 rounded-full ${style.accent}`} />
{client.segmentLabel}
</span>
</td>
<td className="px-6 py-3">
<div className="flex items-center gap-2">
<ScoreBadge value={client.recencyScore} />
<ScoreBadge value={client.frequencyScore} />
<ScoreBadge value={client.monetaryScore} />
</div>
</td>
<td className="px-6 py-3 text-dark-muted font-medium">
{formatDate(client.lastPurchaseDate)}
<span className="block text-xs">{client.recencyDays} 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>
<td className="px-6 py-3 text-brand-primary font-bold">{formatCurrency(client.monetary)}</td>
</tr>
);
})}
</tbody>
</table>
</div>
{!filteredClients.length && (
<div className="p-8 text-center text-sm font-semibold text-dark-muted">
Nenhum cliente encontrado.
</div>
)}
</section>
</div>
);
};
export default Rfm;