From ba27a342f8da8ae2a399a2de18bbbae15e419b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Fri, 12 Jun 2026 12:29:40 -0300 Subject: [PATCH] Refine RFM page table UX --- src/pages/Rfm.tsx | 296 +++++++++++----------------------------------- 1 file changed, 71 insertions(+), 225 deletions(-) diff --git a/src/pages/Rfm.tsx b/src/pages/Rfm.tsx index d860944..450e25f 100644 --- a/src/pages/Rfm.tsx +++ b/src/pages/Rfm.tsx @@ -1,6 +1,6 @@ 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 { ChevronLeft, ChevronRight, Download, Filter, Loader2, Search, Users } from 'lucide-react'; import DateRangePicker from '../components/DateRangePicker'; import { exportToCSV, fetchRfmAnalytics } from '../dataService'; import type { DateRange, RfmAnalytics, RfmClient, RfmSegment } from '../types'; @@ -68,178 +68,6 @@ const segmentByScore: Record = { '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); }; @@ -283,7 +111,8 @@ const Rfm = () => { const [searchTerm, setSearchTerm] = useState(''); const [segmentFilter, setSegmentFilter] = useState('all'); const [selectedSegmentKey, setSelectedSegmentKey] = useState(''); - const [isDemoMode, setIsDemoMode] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(20); const loadRfm = useCallback(async (range: DateRange) => { setIsLoading(true); @@ -308,7 +137,7 @@ const Rfm = () => { return () => clearInterval(intervalId); }, [dateRange, loadRfm, refreshInterval]); - const visibleAnalytics = isDemoMode ? demoAnalytics : analytics; + const visibleAnalytics = analytics; const clients = visibleAnalytics?.clients || emptyClients; const segments = useMemo(() => { const sourceSegments = visibleAnalytics?.segments || emptySegments; @@ -323,13 +152,6 @@ const Rfm = () => { }); }, [visibleAnalytics]); - const toggleDemoMode = () => { - setIsDemoMode(current => !current); - setSegmentFilter('all'); - setSearchTerm(''); - setSelectedSegmentKey(''); - }; - const filteredClients = useMemo(() => { const normalizedSearch = searchTerm.trim().toLowerCase(); @@ -343,6 +165,10 @@ const Rfm = () => { }); }, [clients, searchTerm, segmentFilter]); + const totalPages = Math.ceil(filteredClients.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const paginatedClients = filteredClients.slice(startIndex, startIndex + itemsPerPage); + const totals = useMemo(() => { const totalRevenue = clients.reduce((sum, client) => sum + client.monetary, 0); const activeSegments = segments.filter(segment => segment.count > 0).length; @@ -407,18 +233,6 @@ const Rfm = () => {
- - {
- {isDemoMode && ( -
- Visualização com dados fictícios. Nenhum cliente real ou dado do banco foi alterado. -
- )} - - {!isDemoMode && isLoading && ( + {isLoading && (
Atualizando RFM @@ -513,6 +321,7 @@ const Rfm = () => { 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' @@ -563,6 +372,7 @@ const Rfm = () => { 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' @@ -626,7 +436,10 @@ const Rfm = () => { { + setItemsPerPage(Number(event.target.value)); + setCurrentPage(1); + }} + className="bg-dark-card border border-dark-border rounded-lg px-2 py-1 focus:outline-none focus:border-brand-primary cursor-pointer text-dark-text" + > + + + + + + clientes por página +
+ +
+ + Mostrando {filteredClients.length > 0 ? startIndex + 1 : 0} a {Math.min(startIndex + itemsPerPage, filteredClients.length)} de {filteredClients.length} clientes + +
+ + +
+
+ );