From a6fe2ccc856a09a7a06d65e1145e674299fb6c89 Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 26 Nov 2025 09:28:29 -0300 Subject: [PATCH] feat(protocols): implement ProtocolsDashboard and ProtocolsDialog with pagination and search functionality --- package-lock.json | 58 +++++ package.json | 1 + src/App.tsx | 84 +++---- src/main.tsx | 8 +- src/views/components/protocols-table.tsx | 171 +++++++++++++ src/views/components/ui/badge.tsx | 45 ++++ src/views/protocols/ProtocolsDashboard.tsx | 17 -- src/views/protocols/ProtocolsDialog.tsx | 260 -------------------- src/views/protocols/protocols-dashboard.tsx | 56 +++++ 9 files changed, 378 insertions(+), 322 deletions(-) create mode 100644 src/views/components/protocols-table.tsx create mode 100644 src/views/components/ui/badge.tsx delete mode 100644 src/views/protocols/ProtocolsDashboard.tsx delete mode 100644 src/views/protocols/ProtocolsDialog.tsx create mode 100644 src/views/protocols/protocols-dashboard.tsx diff --git a/package-lock.json b/package-lock.json index 7130dcc..ddf032f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "react": "^19.2.0", "react-day-picker": "^9.11.1", "react-dom": "^19.2.0", + "react-router-dom": "^7.9.6", "recharts": "^2.15.4", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.17", @@ -3207,6 +3208,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.0.tgz", + "integrity": "sha512-vXiThu1/rlos7EGu8TuNZQEg2e9TvhH9dmS4T4ZVzB7Ao1agEZ6EG3sn5n+hZRYUgduISd1HpngFzAZiDGm5vQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4721,6 +4735,44 @@ } } }, + "node_modules/react-router": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", + "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz", + "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.6" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-smooth": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", @@ -4873,6 +4925,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 08dfad1..ccb4725 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react": "^19.2.0", "react-day-picker": "^9.11.1", "react-dom": "^19.2.0", + "react-router-dom": "^7.9.6", "recharts": "^2.15.4", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.17", diff --git a/src/App.tsx b/src/App.tsx index 133c02e..fda3b72 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,52 +17,50 @@ import { Bar, BarChart, CartesianGrid, Pie, PieChart, XAxis } from 'recharts'; import { Header } from './views/components/header'; import type { ChartConfig } from './views/components/ui/chart'; -import ProtocolsDashboard from './views/protocols/ProtocolsDashboard'; +import { ProtocolsDashboard } from './views/protocols/protocols-dashboard'; export const description = 'A stacked bar chart with a legend'; +const bestPricesConfig = { + price: { + label: 'Preço', + color: '#007cb8', + }, +} satisfies ChartConfig; + +const bestPricesData = [ + { station: 'Shell', price: 5.42 }, + { station: 'Ipiranga', price: 5.35 }, + { station: 'BR', price: 5.38 }, + { station: 'Ale', price: 5.3 }, +]; + +const vehicleStatusConfig = { + active: { + label: 'Ativos', + color: '#007cb8', + }, + maintenance: { + label: 'Manutenção', + color: '#0ca9eb', + }, + inactive: { + label: 'Inativos', + color: '#36c1fa', + }, + reserved: { + label: 'Reservados', + color: '#7cd5fd', + }, +} satisfies ChartConfig; + +const vehicleStatusData = [ + { status: 'active', count: 45, fill: '#007cb8' }, + { status: 'maintenance', count: 12, fill: '#0ca9eb' }, + { status: 'inactive', count: 8, fill: '#36c1fa' }, + { status: 'reserved', count: 15, fill: '#7cd5fd' }, +]; + export function App() { - // dashboard now uses ProtocolsDashboard for protocol status cards - - const bestPricesConfig = { - price: { - label: 'Preço', - color: '#007cb8', - }, - } satisfies ChartConfig; - - const bestPricesData = [ - { station: 'Shell', price: 5.42 }, - { station: 'Ipiranga', price: 5.35 }, - { station: 'BR', price: 5.38 }, - { station: 'Ale', price: 5.3 }, - ]; - - const vehicleStatusConfig = { - active: { - label: 'Ativos', - color: '#007cb8', - }, - maintenance: { - label: 'Manutenção', - color: '#0ca9eb', - }, - inactive: { - label: 'Inativos', - color: '#36c1fa', - }, - reserved: { - label: 'Reservados', - color: '#7cd5fd', - }, - } satisfies ChartConfig; - - const vehicleStatusData = [ - { status: 'active', count: 45, fill: '#007cb8' }, - { status: 'maintenance', count: 12, fill: '#0ca9eb' }, - { status: 'inactive', count: 8, fill: '#36c1fa' }, - { status: 'reserved', count: 15, fill: '#7cd5fd' }, - ]; - return (
diff --git a/src/main.tsx b/src/main.tsx index c95dcec..6799d44 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,12 +1,16 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + import './styles/index.css'; import { App } from './App.tsx'; createRoot(document.getElementById('root')!).render( - - + + + + , ); diff --git a/src/views/components/protocols-table.tsx b/src/views/components/protocols-table.tsx new file mode 100644 index 0000000..4d82345 --- /dev/null +++ b/src/views/components/protocols-table.tsx @@ -0,0 +1,171 @@ +/* eslint-disable react-hooks/set-state-in-effect */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Badge } from '@/views/components/ui/badge'; +import { Button } from '@/views/components/ui/button'; +import { + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/views/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/views/components/ui/select'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/views/components/ui/table'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; + +interface ProtocolsTableProps { + tasks: any[]; + title: string; + description: string; +} + +export function ProtocolsTable({ + tasks, + title, + description, +}: ProtocolsTableProps) { + const [page, setPage] = useState(0); + const [size, setSize] = useState(10); + const [loading, setLoading] = useState(false); + + const total = tasks.length; + const totalPages = Math.ceil(total / size); + const startIndex = page * size; + const endIndex = Math.min(startIndex + size, total); + + useEffect(() => { + setLoading(true); + const timer = setTimeout(() => { + setLoading(false); + }, 500); + return () => clearTimeout(timer); + }, [page, size]); + + const protocols = useMemo(() => { + return tasks.slice(startIndex, endIndex); + }, [tasks, startIndex, endIndex]); + + function handlePreviousPage() { + setPage((prev) => Math.max(prev - 1, 0)); + } + + function handleNextPage() { + setPage((prev) => Math.min(prev + 1, totalPages - 1)); + } + + return ( + + +
+ {title} + {description} +
+
+ +
+
+

Detalhes dos Protocolos

+ Total: {total} +
+ + {protocols.length > 0 ? ( + + + + Código + Assunto + Data + Solicitante + Local + + + + {protocols.map((protocol) => ( + + {protocol.id} + {protocol.title} + {protocol.date} + {protocol.sender} + {protocol.location} + + ))} + {protocols.length === 0 && ( + + + {loading ? 'Carregando...' : 'Nenhum protocolo encontrado.'} + + + )} + +
+ ) : ( +
+ Nenhuma tarefa encontrada +
+ )} + +
+
+ Mostrando {startIndex + 1} - {endIndex} de {total} tarefas +
+
+
+

Tarefas por página

+ +
+
+ +
+ Página {page + 1} de {totalPages || 1} +
+ +
+
+
+
+
+ ); +} diff --git a/src/views/components/ui/badge.tsx b/src/views/components/ui/badge.tsx new file mode 100644 index 0000000..9be393c --- /dev/null +++ b/src/views/components/ui/badge.tsx @@ -0,0 +1,45 @@ +/* eslint-disable react-refresh/only-export-components */ +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from 'react'; + +import { cn } from '@/app/utils'; + +const badgeVariants = cva( + 'inline-flex items-center rounded-md border border-gray-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2 dark:border-gray-800 dark:focus:ring-gray-300', + { + variants: { + variant: { + default: + 'border-transparent bg-agprimary text-gray-50 shadow hover:bg-agprimary/80 dark:bg-gray-50 dark:text-agprimary dark:hover:bg-gray-50/80', + secondary: + 'border-transparent bg-gray-100 text-agprimary hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80', + destructive: + 'border-transparent bg-red-500 text-gray-50 shadow hover:bg-red-500/80 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/80', + outline: 'text-gray-950 dark:text-gray-50', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span'; + + return ( + + ); +} + +export { Badge, badgeVariants }; diff --git a/src/views/protocols/ProtocolsDashboard.tsx b/src/views/protocols/ProtocolsDashboard.tsx deleted file mode 100644 index 98b6a50..0000000 --- a/src/views/protocols/ProtocolsDashboard.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client'; - -import ProtocolsDialog from './ProtocolsDialog'; - -const statuses = ['Criado', 'Tramitado', 'Recebido', 'Finalizado', 'Cancelado']; - -export function ProtocolsDashboard() { - return ( -
- {statuses.map((s) => ( - - ))} -
- ); -} - -export default ProtocolsDashboard; diff --git a/src/views/protocols/ProtocolsDialog.tsx b/src/views/protocols/ProtocolsDialog.tsx deleted file mode 100644 index 4a27228..0000000 --- a/src/views/protocols/ProtocolsDialog.tsx +++ /dev/null @@ -1,260 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable react-hooks/set-state-in-effect */ -'use client'; - -import { getCounts, getProtocols } from '@/services/protocolService'; -import { Button } from '@/views/components/ui/button'; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@/views/components/ui/dialog'; -import { - Pagination, - PaginationContent, - PaginationEllipsis, - PaginationItem, - PaginationLink, - PaginationNext, - PaginationPrevious, -} from '@/views/components/ui/pagination'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/views/components/ui/table'; -import { useEffect, useState } from 'react'; - -import type { Protocol } from '@/services/protocolService'; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from '../components/ui/card'; - -type Props = { - status: string; -}; - -export function ProtocolsDialog({ status }: Props) { - const [open, setOpen] = useState(false); - const [search, setSearch] = useState(''); - const [page, setPage] = useState(1); - const [pageSize] = useState(10); - const [allItems, setAllItems] = useState([]); - const [paginatedItems, setPaginatedItems] = useState([]); - const [total, setTotal] = useState(0); - const [loading, setLoading] = useState(false); - const [counts, setCounts] = useState>({ Todos: 0 }); - - const label = `${status} (${counts[status] ?? 0})`; - - useEffect(() => { - setCounts(getCounts()); - }, []); - - useEffect(() => { - if (!open) return; - - setLoading(true); - getProtocols({ - status: status as any, - page: 1, - pageSize: 1000, - search, - }).then((r) => { - setAllItems(r.items); - setTotal(r.total); - setLoading(false); - setPage(1); // Reset para primeira página quando buscar - }); - }, [open, search, status]); - - // Efeito para aplicar a paginação local - useEffect(() => { - if (allItems.length === 0) { - setPaginatedItems([]); - return; - } - - const startIndex = (page - 1) * pageSize; - const endIndex = startIndex + pageSize; - const itemsForCurrentPage = allItems.slice(startIndex, endIndex); - - setPaginatedItems(itemsForCurrentPage); - }, [allItems, page, pageSize]); - - const totalPages = Math.max(1, Math.ceil(total / pageSize)); - - function getPageItems(totalPages: number, current: number) { - const pages: Array = []; - if (totalPages <= 7) { - for (let i = 1; i <= totalPages; i++) pages.push(i); - return pages; - } - - pages.push(1); - const left = Math.max(2, current - 1); - const right = Math.min(totalPages - 1, current + 1); - - if (left > 2) pages.push('ellipsis'); - - for (let i = left; i <= right; i++) pages.push(i); - - if (right < totalPages - 1) pages.push('ellipsis'); - - pages.push(totalPages); - return pages; - } - - const handlePreviousPage = () => { - if (page > 1) { - setPage(page - 1); - } - }; - - const handleNextPage = () => { - if (page < totalPages) { - setPage(page + 1); - } - }; - - const handlePageClick = (pageNumber: number) => { - setPage(pageNumber); - }; - - const pageItems = getPageItems(totalPages, page); - - return ( - { - setOpen(v); - if (!v) { - // Resetar estado quando o dialog fechar - setSearch(''); - setPage(1); - } - }}> - - - - {`Protocolos ${status.toLowerCase()}`} - - -
{counts[status] ?? 0}
-
-
-
- - - - {label} - - -
- { - setSearch(e.target.value); - setPage(1); - }} - /> -
- {loading ? 'Carregando...' : `${total} encontrados`} -
-
- -
- - - - Código - Assunto - Data - Solicitante - Local - - - - {paginatedItems.map((protocol) => ( - - {protocol.id} - {protocol.title} - {protocol.date} - {protocol.sender} - {protocol.location} - - ))} - {paginatedItems.length === 0 && ( - - - {loading ? 'Carregando...' : 'Nenhum protocolo encontrado.'} - - - )} - -
-
- -
-
- Página {page} de {totalPages} -
- - - - - - - - {pageItems.map((pageItem, index) => ( - - {pageItem === 'ellipsis' ? ( - - ) : ( - handlePageClick(pageItem)} - isActive={pageItem === page}> - {pageItem} - - )} - - ))} - - - = totalPages ? 'pointer-events-none opacity-50' : '' - } - /> - - - -
- - - - -
-
- ); -} - -export default ProtocolsDialog; diff --git a/src/views/protocols/protocols-dashboard.tsx b/src/views/protocols/protocols-dashboard.tsx new file mode 100644 index 0000000..094191a --- /dev/null +++ b/src/views/protocols/protocols-dashboard.tsx @@ -0,0 +1,56 @@ +import { Dialog, DialogTrigger } from '@/views/components/ui/dialog'; +import { useState } from 'react'; + +import type { Protocol } from '@/services/protocolService'; +import { InfoIcon } from '@phosphor-icons/react'; +import { ProtocolsTable } from '../components/protocols-table'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '../components/ui/card'; + +const statuses = ['Criado', 'Tramitado', 'Recebido', 'Finalizado', 'Cancelado']; + +export function ProtocolsDashboard() { + const [open, setOpen] = useState(false); + const [paginatedItems] = useState([]); + const [counts] = useState>({ Todos: 0 }); + + return ( +
+ {statuses.map((status) => ( + { + setOpen(v); + }}> + + + + + {`Protocolos ${status.toLowerCase()}`} + + + + + + {counts[status] ?? 0} + + + + + + + + + ))} +
+ ); +}