Refine dashboard and RFV visuals
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m18s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m18s
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link, useOutletContext } from 'react-router-dom';
|
||||
import { ChevronLeft, ChevronRight, Download, Filter, Loader2, Search, Users } from 'lucide-react';
|
||||
import DateRangePicker from '../components/DateRangePicker';
|
||||
@@ -8,16 +8,23 @@ 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' }
|
||||
type SegmentStyle = {
|
||||
accent: string;
|
||||
bg: string;
|
||||
border: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
const segmentStyles: Record<string, SegmentStyle> = {
|
||||
champions: { accent: '#18D6B5', bg: 'rgba(24, 214, 181, 0.10)', border: 'rgba(24, 214, 181, 0.34)', text: '#18D6B5' },
|
||||
potential_loyalists: { accent: '#A3E635', bg: 'rgba(163, 230, 53, 0.10)', border: 'rgba(163, 230, 53, 0.34)', text: '#A3E635' },
|
||||
new_customers: { accent: '#25C2FF', bg: 'rgba(37, 194, 255, 0.10)', border: 'rgba(37, 194, 255, 0.34)', text: '#25C2FF' },
|
||||
loyal_customers: { accent: '#6EA5FF', bg: 'rgba(110, 165, 255, 0.10)', border: 'rgba(110, 165, 255, 0.34)', text: '#6EA5FF' },
|
||||
need_attention: { accent: '#FFC247', bg: 'rgba(255, 194, 71, 0.11)', border: 'rgba(255, 194, 71, 0.34)', text: '#FFC247' },
|
||||
about_to_sleep: { accent: '#FF8A3D', bg: 'rgba(255, 138, 61, 0.11)', border: 'rgba(255, 138, 61, 0.34)', text: '#FF8A3D' },
|
||||
at_risk: { accent: '#FF5D7D', bg: 'rgba(255, 93, 125, 0.11)', border: 'rgba(255, 93, 125, 0.34)', text: '#FF5D7D' },
|
||||
hibernating: { accent: '#C77DFF', bg: 'rgba(199, 125, 255, 0.11)', border: 'rgba(199, 125, 255, 0.34)', text: '#C77DFF' },
|
||||
lost: { accent: '#8c9298', bg: 'rgba(140, 146, 152, 0.08)', border: 'rgba(140, 146, 152, 0.24)', text: '#b3b7bb' }
|
||||
};
|
||||
|
||||
const rfmSegmentDefinitions: Array<Pick<RfmSegment, 'key' | 'label'>> = [
|
||||
@@ -72,6 +79,23 @@ const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
|
||||
};
|
||||
|
||||
const segmentPanelStyle = (style: SegmentStyle, isActive = false) => ({
|
||||
backgroundColor: style.bg,
|
||||
borderColor: isActive ? style.text : style.border,
|
||||
boxShadow: isActive ? `0 0 0 1px ${style.border}, 0 0 14px ${style.accent}22` : undefined
|
||||
});
|
||||
|
||||
const segmentPillStyle = (style: SegmentStyle) => ({
|
||||
backgroundColor: style.bg,
|
||||
color: style.text,
|
||||
borderColor: style.border
|
||||
});
|
||||
|
||||
const segmentDotStyle = (style: SegmentStyle) => ({
|
||||
backgroundColor: style.accent,
|
||||
boxShadow: `0 0 6px ${style.accent}55`
|
||||
});
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const parseLocalDate = (value: string) => {
|
||||
@@ -135,6 +159,7 @@ const Rfm = () => {
|
||||
const [selectedSegmentKey, setSelectedSegmentKey] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(20);
|
||||
const segmentFocusRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const loadRfm = useCallback(async (range: DateRange, options?: { force?: boolean }) => {
|
||||
const cachedAnalytics = options?.force ? undefined : getCachedRfmAnalytics(range);
|
||||
@@ -166,6 +191,47 @@ const Rfm = () => {
|
||||
return () => clearInterval(intervalId);
|
||||
}, [dateRange, loadRfm, refreshInterval]);
|
||||
|
||||
const clearSegmentFocus = useCallback(() => {
|
||||
setSegmentFilter('all');
|
||||
setSelectedSegmentKey('');
|
||||
setCurrentPage(1);
|
||||
}, []);
|
||||
|
||||
const selectSegment = useCallback((segmentKey: string) => {
|
||||
if (segmentFilter === segmentKey) {
|
||||
clearSegmentFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
setSegmentFilter(segmentKey);
|
||||
setSelectedSegmentKey(segmentKey);
|
||||
setCurrentPage(1);
|
||||
}, [clearSegmentFocus, segmentFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (segmentFilter === 'all' && !selectedSegmentKey) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
clearSegmentFocus();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
const target = event.target;
|
||||
if (target instanceof Node && segmentFocusRef.current?.contains(target)) return;
|
||||
clearSegmentFocus();
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener('pointerdown', handlePointerDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.removeEventListener('pointerdown', handlePointerDown);
|
||||
};
|
||||
}, [clearSegmentFocus, segmentFilter, selectedSegmentKey]);
|
||||
|
||||
const visibleAnalytics = analytics;
|
||||
const clients = visibleAnalytics?.clients || emptyClients;
|
||||
const segments = useMemo(() => {
|
||||
@@ -315,167 +381,165 @@ const Rfm = () => {
|
||||
</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 RFV</h2>
|
||||
<p className="text-sm font-medium text-dark-muted">Recência fixa, frequência por número de pedidos e valor relativo ao histórico.</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 / Perfil</p>
|
||||
</div>
|
||||
<div ref={segmentFocusRef} className="space-y-6">
|
||||
<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 RFV</h2>
|
||||
<p className="text-sm font-medium text-dark-muted">Recência fixa, frequência por número de pedidos e valor relativo ao histórico.</p>
|
||||
</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">Perfil {valueScore}</p>
|
||||
<p className="text-xs font-bold text-dark-text">{scoreLabel(valueScore)}</p>
|
||||
<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"
|
||||
style={{ background: 'linear-gradient(90deg, #FF6B8A, #FFC247, #18D6B5)' }}
|
||||
/>
|
||||
<span>Maior prioridade</span>
|
||||
</div>
|
||||
))}
|
||||
</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 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">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 ? '0 a 7 dias' : recencyScore === 2 ? '8 a 15 dias' : '16+ dias'}
|
||||
</p>
|
||||
<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 / Perfil</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">Perfil {valueScore}</p>
|
||||
<p className="text-xs font-bold text-dark-text">{scoreLabel(valueScore)}</p>
|
||||
</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 || '');
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
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} P{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 período</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);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
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>
|
||||
{[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 ? '0 a 7 dias' : recencyScore === 2 ? '8 a 15 dias' : '16+ dias'}
|
||||
</p>
|
||||
</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)} no período
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{[1, 2, 3].map(valueScore => {
|
||||
const segment = segmentForCell(segments, recencyScore, valueScore);
|
||||
const segmentKey = segment?.key || 'lost';
|
||||
const style = segmentStyles[segmentKey];
|
||||
const isActive = segmentFilter === segmentKey;
|
||||
const intensity = segment?.count ? 'opacity-100' : 'opacity-75';
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`${recencyScore}-${valueScore}`}
|
||||
onClick={() => selectSegment(segmentKey)}
|
||||
className={`group min-h-28 rounded-xl border p-3 text-left transition-all hover:-translate-y-0.5 cursor-pointer ${intensity}`}
|
||||
style={segmentPanelStyle(style, isActive)}
|
||||
>
|
||||
<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={segmentDotStyle(style)} />
|
||||
<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} P{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[segmentKey]}
|
||||
</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={{ color: style.text }}>{segment?.count || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Receita período</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={() => selectSegment(segment.key)}
|
||||
className={`w-full rounded-xl border px-4 py-3 text-left transition-colors cursor-pointer ${
|
||||
segmentFilter === segment.key ? '' : 'hover:border-brand-primary'
|
||||
}`}
|
||||
style={segmentFilter === segment.key ? segmentPanelStyle(style, true) : { borderColor: '#282828' }}
|
||||
>
|
||||
<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={segmentDotStyle(style)} />
|
||||
<span className="truncate text-sm font-bold text-dark-text">{segment.label}</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold" style={{ color: 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)} no período
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{selectedSegment && selectedSegmentInsights && (
|
||||
<section className="rounded-2xl border p-4 shadow-sm" style={segmentPanelStyle(selectedSegmentStyle, true)}>
|
||||
<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" style={segmentDotStyle(selectedSegmentStyle)} />
|
||||
<p className="text-xs font-bold uppercase tracking-widest" style={{ color: 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 período</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 período</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">Compra período</p>
|
||||
<p className="mt-1 text-base font-bold text-dark-text">{selectedSegmentInsights.averageRecencyDays.toFixed(0)} dias</p>
|
||||
</div>
|
||||
</div>
|
||||
</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 período</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 período</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">Compra período</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">
|
||||
@@ -552,8 +616,8 @@ const Rfm = () => {
|
||||
</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}`} />
|
||||
<span className="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-bold" style={segmentPillStyle(style)}>
|
||||
<span className="h-2 w-2 rounded-full" style={segmentDotStyle(style)} />
|
||||
{client.segmentLabel}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user