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 (
-
- );
-}
-
-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) => (
+
+ ))}
+
+ );
+}