From e2b2163c83e09bcd6e9e3e7ae00822cba1ba0318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Wed, 1 Jul 2026 11:26:09 -0300 Subject: [PATCH] Add full pagination controls --- src/analytics/products.ts | 2 - src/components/PaginationControls.tsx | 133 ++++++++++++++++++++++++++ src/pages/ClientDetails.tsx | 61 ++++-------- src/pages/Clients.tsx | 64 ++++--------- src/pages/Products.tsx | 133 +++++--------------------- src/pages/Rfm.tsx | 64 ++++--------- 6 files changed, 219 insertions(+), 238 deletions(-) create mode 100644 src/components/PaginationControls.tsx diff --git a/src/analytics/products.ts b/src/analytics/products.ts index 9d8c2b8..e6090b5 100644 --- a/src/analytics/products.ts +++ b/src/analytics/products.ts @@ -8,8 +8,6 @@ export interface ProductSummary { revenue: number; lastPrice: number; stock: number; - firstSaleDate?: string | null; - lastSaleDate?: string | null; } export interface ProductDetailsMetrics { diff --git a/src/components/PaginationControls.tsx b/src/components/PaginationControls.tsx new file mode 100644 index 0000000..64b8d58 --- /dev/null +++ b/src/components/PaginationControls.tsx @@ -0,0 +1,133 @@ +import { ChevronsLeft, ChevronLeft, ChevronRight, ChevronsRight } from 'lucide-react'; + +type PaginationControlsProps = { + totalItems: number; + currentPage: number; + totalPages: number; + pageSize: number; + pageSizeOptions: number[]; + itemLabel: string; + pageSizeLabel: string; + startIndex: number; + endIndex: number; + onPageChange: (page: number) => void; + onPageSizeChange: (pageSize: number) => void; + className?: string; +}; + +const clampPage = (page: number, totalPages: number) => { + if (!Number.isFinite(page)) return 1; + return Math.min(Math.max(1, Math.trunc(page)), Math.max(1, totalPages)); +}; + +const PaginationControls = ({ + totalItems, + currentPage, + totalPages, + pageSize, + pageSizeOptions, + itemLabel, + pageSizeLabel, + startIndex, + endIndex, + onPageChange, + onPageSizeChange, + className = 'px-6 py-4 border-t border-zinc-100 dark:border-dark-border' +}: PaginationControlsProps) => { + const safeTotalPages = Math.max(1, totalPages); + const safeCurrentPage = clampPage(currentPage, safeTotalPages); + const isFirstPage = safeCurrentPage <= 1; + const isLastPage = safeCurrentPage >= safeTotalPages || totalItems === 0; + + const goToPage = (page: number) => { + onPageChange(clampPage(page, safeTotalPages)); + }; + + return ( +
+
+ Mostrar + + {pageSizeLabel} +
+ +
+ + Mostrando {totalItems > 0 ? startIndex + 1 : 0} a {endIndex} de {totalItems} {itemLabel} + + +
+
+ + +
+ + + +
+ + +
+
+
+
+ ); +}; + +export default PaginationControls; diff --git a/src/pages/ClientDetails.tsx b/src/pages/ClientDetails.tsx index 9e72f2a..ce94313 100644 --- a/src/pages/ClientDetails.tsx +++ b/src/pages/ClientDetails.tsx @@ -1,8 +1,9 @@ import { useEffect, useState } from 'react'; import { useParams, Link, useOutletContext } from 'react-router-dom'; -import { ArrowLeft, User, Tag, Package, DollarSign, Clock, Phone, ChevronDown, ChevronLeft, ChevronRight, ShoppingBag, ReceiptText } from 'lucide-react'; +import { ArrowLeft, User, Tag, Package, DollarSign, Clock, Phone, ChevronDown, ShoppingBag, ReceiptText } from 'lucide-react'; import { AreaChart, Area, BarChart, Bar, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import DateRangePicker from '../components/DateRangePicker'; +import PaginationControls from '../components/PaginationControls'; import RefreshStatus from '../components/RefreshStatus'; import type { ClientDetailsAnalytics, DateRange, OrderData } from '../types'; import { fetchClientDetailsAnalytics } from '../dataService'; @@ -556,47 +557,23 @@ const ClientDetails = () => { })} -
-
- Mostrar - - pedidos por página -
- -
- - Mostrando {groupedOrders.length > 0 ? startIndex + 1 : 0} a {Math.min(startIndex + ordersPerPage, groupedOrders.length)} de {groupedOrders.length} pedidos - -
- - -
-
-
+ { + setOrdersPerPage(pageSize); + setCurrentPage(1); + }} + className="px-6 py-4 border border-zinc-200 dark:border-dark-border rounded-2xl bg-white dark:bg-dark-card" + /> ); diff --git a/src/pages/Clients.tsx b/src/pages/Clients.tsx index 56099d1..d74ce35 100644 --- a/src/pages/Clients.tsx +++ b/src/pages/Clients.tsx @@ -1,7 +1,8 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { Link, useOutletContext } from 'react-router-dom'; -import { Search, ChevronRight, Filter, ChevronLeft, X } from 'lucide-react'; +import { Search, ChevronRight, Filter, X } from 'lucide-react'; import { BarChart, Bar, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; +import PaginationControls from '../components/PaginationControls'; import RefreshStatus from '../components/RefreshStatus'; import type { ClientAnalyticsItem, ClientFilterOptions, ClientMetadataFilters, ClientPurchasePatternAnalytics, DateRange, RfmAnalytics, RfmClient } from '../types'; import { fetchClientAnalytics, fetchClientFilterOptions, fetchClientPurchasePatternAnalytics, fetchRfmAnalytics, getCachedClientAnalytics, getCachedClientFilterOptions, getCachedRfmAnalytics } from '../dataService'; @@ -390,7 +391,8 @@ const Clients = () => { // Pagination logic const totalPages = Math.ceil(clientsData.length / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; + const safeCurrentPage = Math.min(currentPage, totalPages || 1); + const startIndex = (safeCurrentPage - 1) * itemsPerPage; const paginatedData = clientsData.slice(startIndex, startIndex + itemsPerPage); const formatCurrency = (value: number) => { @@ -803,48 +805,22 @@ const Clients = () => { - {/* Pagination Controls */} -
-
- Mostrar - - itens por página -
- -
- - Mostrando {clientsData.length > 0 ? startIndex + 1 : 0} a {Math.min(startIndex + itemsPerPage, clientsData.length)} de {clientsData.length} clientes - -
- - -
-
-
+ { + setItemsPerPage(pageSize); + setCurrentPage(1); + }} + /> )} diff --git a/src/pages/Products.tsx b/src/pages/Products.tsx index 97e9837..f544071 100644 --- a/src/pages/Products.tsx +++ b/src/pages/Products.tsx @@ -1,74 +1,31 @@ import { useEffect, useMemo, useState } from 'react'; import { Link, useOutletContext } from 'react-router-dom'; -import { Search, Package, TrendingUp, ChevronLeft, ChevronRight, Download } from 'lucide-react'; +import { Search, Package, TrendingUp, Download } from 'lucide-react'; import DateRangePicker from '../components/DateRangePicker'; +import PaginationControls from '../components/PaginationControls'; import RefreshStatus from '../components/RefreshStatus'; import type { DateRange, ProductAnalyticsItem } from '../types'; import { exportToCSV, fetchProductAnalytics } from '../dataService'; import type { ProductSummary } from '../analytics/products'; -type ProductHealth = { - label: string; - className: string; -}; - -const getDateOnlyTime = (value?: string | null) => { - if (!value) return 0; - const date = new Date(`${String(value).slice(0, 10)}T00:00:00`); - return Number.isNaN(date.getTime()) ? 0 : date.getTime(); -}; - -const getProductHealth = (product: ProductSummary, dateRange: DateRange): ProductHealth => { - const endTime = new Date(dateRange.end.getFullYear(), dateRange.end.getMonth(), dateRange.end.getDate()).getTime(); - const startTime = new Date(dateRange.start.getFullYear(), dateRange.start.getMonth(), dateRange.start.getDate()).getTime(); - const rangeDays = Math.max(1, Math.round((endTime - startTime) / 86400000) + 1); - const lastSaleTime = getDateOnlyTime(product.lastSaleDate); - const firstSaleTime = getDateOnlyTime(product.firstSaleDate); - const daysSinceLastSale = lastSaleTime ? Math.max(0, Math.round((endTime - lastSaleTime) / 86400000)) : Infinity; - const daysSinceFirstSale = firstSaleTime ? Math.max(0, Math.round((endTime - firstSaleTime) / 86400000)) : Infinity; - - if (product.totalSold > 0 && product.stock > 0 && product.stock <= Math.max(3, product.totalSold * 0.15)) { - return { label: 'Estoque baixo', className: 'border-amber-500/35 bg-amber-500/10 text-amber-700 dark:text-amber-300' }; - } - - if (!product.totalSold) { - return { label: 'Sem venda', className: 'border-zinc-500/25 bg-zinc-500/10 text-zinc-600 dark:text-zinc-400' }; - } - - if (daysSinceFirstSale <= Math.min(14, rangeDays)) { - return { label: 'Novo', className: 'border-cyan-500/35 bg-cyan-500/10 text-cyan-700 dark:text-cyan-300' }; - } - - if (daysSinceLastSale <= Math.max(1, Math.min(7, Math.ceil(rangeDays * 0.2)))) { - return { label: 'Quente', className: 'border-emerald-500/35 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300' }; - } - - if (daysSinceLastSale > Math.max(14, Math.ceil(rangeDays * 0.55))) { - return { label: 'Esfriando', className: 'border-orange-500/35 bg-orange-500/10 text-orange-700 dark:text-orange-300' }; - } - - return { label: 'Estável', className: 'border-sky-500/35 bg-sky-500/10 text-sky-700 dark:text-sky-300' }; -}; - const ProductTableSkeleton = () => (
-
- {[0, 1, 2, 3, 4, 5, 6].map(item => ( +
+ {[0, 1, 2, 3, 4, 5].map(item => (
))}
{[0, 1, 2, 3, 4, 5, 6, 7].map(row => ( -
+
-
@@ -123,9 +80,7 @@ const Products = () => { totalSold: product.quantitySold, revenue: product.revenue, lastPrice: product.lastPrice, - stock: product.stock, - firstSaleDate: product.firstSaleDate, - lastSaleDate: product.lastSaleDate + stock: product.stock })); const filteredProducts = normalizedSearch ? products.filter(product => @@ -139,7 +94,8 @@ const Products = () => { // Pagination logic const totalPages = Math.ceil(productsData.length / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; + const safeCurrentPage = Math.min(currentPage, totalPages || 1); + const startIndex = (safeCurrentPage - 1) * itemsPerPage; const paginatedData = productsData.slice(startIndex, startIndex + itemsPerPage); const isRefreshing = isLoading && productAnalytics.length > 0; @@ -185,7 +141,6 @@ const Products = () => { 'Descrição': product.name, 'Preço Atual (R$)': product.lastPrice.toFixed(2).replace('.', ','), 'Total Vendido (un.)': product.totalSold, - 'Status': getProductHealth(product, dateRange).label, 'Estoque': product.stock, 'Receita Gerada (R$)': product.revenue.toFixed(2).replace('.', ',') })); @@ -213,17 +168,13 @@ const Products = () => { ID Produto Descrição Total Vendido - Status Estoque Receita Gerada Ações - {paginatedData.map((product) => { - const health = getProductHealth(product, dateRange); - - return ( + {paginatedData.map((product) => ( #{product.id} @@ -236,11 +187,6 @@ const Products = () => { {product.totalSold} un.
- - - {health.label} - - {product.stock} un. @@ -257,54 +203,27 @@ const Products = () => { - ); - })} + ))}
- {/* Pagination Controls */} -
-
- Mostrar - - itens por página -
- -
- - Mostrando {productsData.length > 0 ? startIndex + 1 : 0} a {Math.min(startIndex + itemsPerPage, productsData.length)} de {productsData.length} produtos - -
- - -
-
-
+ { + setItemsPerPage(pageSize); + setCurrentPage(1); + }} + />
)}
diff --git a/src/pages/Rfm.tsx b/src/pages/Rfm.tsx index 754b763..4840931 100644 --- a/src/pages/Rfm.tsx +++ b/src/pages/Rfm.tsx @@ -1,7 +1,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { Link, useOutletContext } from 'react-router-dom'; -import { ChevronLeft, ChevronRight, Download, Filter, Search, Users } from 'lucide-react'; +import { Download, Filter, Search, Users } from 'lucide-react'; import DateRangePicker from '../components/DateRangePicker'; +import PaginationControls from '../components/PaginationControls'; import RefreshStatus from '../components/RefreshStatus'; import { exportToCSV, fetchRfmAnalytics, getCachedRfmAnalytics } from '../dataService'; import type { DateRange, RfmAnalytics, RfmClient, RfmSegment } from '../types'; @@ -393,7 +394,8 @@ const Rfm = () => { }, [clients, searchTerm, segmentFilter]); const totalPages = Math.ceil(filteredClients.length / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; + const safeCurrentPage = Math.min(currentPage, totalPages || 1); + const startIndex = (safeCurrentPage - 1) * itemsPerPage; const paginatedClients = filteredClients.slice(startIndex, startIndex + itemsPerPage); const totals = useMemo(() => { @@ -766,47 +768,23 @@ const Rfm = () => {
)} -
-
- Mostrar - - clientes por página -
- -
- - Mostrando {filteredClients.length > 0 ? startIndex + 1 : 0} a {Math.min(startIndex + itemsPerPage, filteredClients.length)} de {filteredClients.length} clientes - -
- - -
-
-
+ { + setItemsPerPage(pageSize); + setCurrentPage(1); + }} + className="px-6 py-4 border-t border-dark-border" + />
)}