feat(protocols): implement ProtocolsDashboard and ProtocolsDialog with pagination and search functionality
parent
6b997c4c8c
commit
a6fe2ccc85
|
|
@ -25,6 +25,7 @@
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.9.6",
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
|
|
@ -3207,6 +3208,19 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"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": {
|
"node_modules/react-smooth": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||||
|
|
@ -4873,6 +4925,12 @@
|
||||||
"semver": "bin/semver.js"
|
"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": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.9.6",
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
|
|
|
||||||
84
src/App.tsx
84
src/App.tsx
|
|
@ -17,52 +17,50 @@ import { Bar, BarChart, CartesianGrid, Pie, PieChart, XAxis } from 'recharts';
|
||||||
import { Header } from './views/components/header';
|
import { Header } from './views/components/header';
|
||||||
|
|
||||||
import type { ChartConfig } from './views/components/ui/chart';
|
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';
|
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() {
|
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 (
|
return (
|
||||||
<div className="p-[18px] flex flex-col gap-6">
|
<div className="p-[18px] flex flex-col gap-6">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import './styles/index.css';
|
import './styles/index.css';
|
||||||
|
|
||||||
import { App } from './App.tsx';
|
import { App } from './App.tsx';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<BrowserRouter>
|
||||||
</StrictMode>
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<DialogContent className="min-w-7xl max-w-7xl h-full max-h-[90vh] flex flex-col overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="mb-4">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="mt-4 h-full flex flex-col">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="font-bold">Detalhes dos Protocolos</h2>
|
||||||
|
<Badge variant="outline">Total: {total}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{protocols.length > 0 ? (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[100px]">Código</TableHead>
|
||||||
|
<TableHead>Assunto</TableHead>
|
||||||
|
<TableHead className="w-[120px]">Data</TableHead>
|
||||||
|
<TableHead>Solicitante</TableHead>
|
||||||
|
<TableHead>Local</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{protocols.map((protocol) => (
|
||||||
|
<TableRow key={protocol.id}>
|
||||||
|
<TableCell className="font-medium">{protocol.id}</TableCell>
|
||||||
|
<TableCell>{protocol.title}</TableCell>
|
||||||
|
<TableCell>{protocol.date}</TableCell>
|
||||||
|
<TableCell>{protocol.sender}</TableCell>
|
||||||
|
<TableCell>{protocol.location}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{protocols.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="h-24 text-center">
|
||||||
|
{loading ? 'Carregando...' : 'Nenhum protocolo encontrado.'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-4 my-auto text-muted-foreground">
|
||||||
|
Nenhuma tarefa encontrada
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between px-2 mt-auto pb-4">
|
||||||
|
<div className="flex-1 text-sm text-muted-foreground">
|
||||||
|
Mostrando {startIndex + 1} - {endIndex} de {total} tarefas
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-6 lg:space-x-8">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<p className="text-sm font-medium">Tarefas por página</p>
|
||||||
|
<Select
|
||||||
|
value={`${size}`}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newSize = Number(value);
|
||||||
|
setSize(newSize);
|
||||||
|
const newPage = Math.floor(startIndex / newSize);
|
||||||
|
setPage(newPage);
|
||||||
|
}}>
|
||||||
|
<SelectTrigger className="h-8 w-[70px]">
|
||||||
|
<SelectValue placeholder={size} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent side="top">
|
||||||
|
{[5, 10, 20, 30, 40, 50].map((pageSize) => (
|
||||||
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||||
|
{pageSize}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={handlePreviousPage}
|
||||||
|
disabled={page === 0}>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div className="text-sm">
|
||||||
|
Página {page + 1} de {totalPages || 1}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={handleNextPage}
|
||||||
|
disabled={page >= totalPages - 1 || totalPages === 0}>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : 'span';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants };
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import ProtocolsDialog from './ProtocolsDialog';
|
|
||||||
|
|
||||||
const statuses = ['Criado', 'Tramitado', 'Recebido', 'Finalizado', 'Cancelado'];
|
|
||||||
|
|
||||||
export function ProtocolsDashboard() {
|
|
||||||
return (
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-5 lg:grid-cols-5">
|
|
||||||
{statuses.map((s) => (
|
|
||||||
<ProtocolsDialog status={s} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProtocolsDashboard;
|
|
||||||
|
|
@ -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<Protocol[]>([]);
|
|
||||||
const [paginatedItems, setPaginatedItems] = useState<Protocol[]>([]);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [counts, setCounts] = useState<Record<string, number>>({ 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<number | 'ellipsis'> = [];
|
|
||||||
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 (
|
|
||||||
<Dialog
|
|
||||||
open={open}
|
|
||||||
onOpenChange={(v) => {
|
|
||||||
setOpen(v);
|
|
||||||
if (!v) {
|
|
||||||
// Resetar estado quando o dialog fechar
|
|
||||||
setSearch('');
|
|
||||||
setPage(1);
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Card className="hover:shadow-md transition-shadow duration-200 cursor-pointer">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-sm">{`Protocolos ${status.toLowerCase()}`}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold mb-3">{counts[status] ?? 0}</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent className="min-w-[80vw] max-w-[80vw] h-full max-h-[80vh] overflow-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{label}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="flex gap-2 items-center mb-4">
|
|
||||||
<input
|
|
||||||
className="flex-1 rounded-sm border px-3 py-2"
|
|
||||||
placeholder={`Pesquisar em ${label.toLowerCase()}...`}
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSearch(e.target.value);
|
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{loading ? 'Carregando...' : `${total} encontrados`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="overflow-auto">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead className="w-[100px]">Código</TableHead>
|
|
||||||
<TableHead>Assunto</TableHead>
|
|
||||||
<TableHead className="w-[120px]">Data</TableHead>
|
|
||||||
<TableHead>Solicitante</TableHead>
|
|
||||||
<TableHead>Local</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{paginatedItems.map((protocol) => (
|
|
||||||
<TableRow key={protocol.id}>
|
|
||||||
<TableCell className="font-medium">{protocol.id}</TableCell>
|
|
||||||
<TableCell>{protocol.title}</TableCell>
|
|
||||||
<TableCell>{protocol.date}</TableCell>
|
|
||||||
<TableCell>{protocol.sender}</TableCell>
|
|
||||||
<TableCell>{protocol.location}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
{paginatedItems.length === 0 && (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={5} className="h-24 text-center">
|
|
||||||
{loading ? 'Carregando...' : 'Nenhum protocolo encontrado.'}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-2 mt-4">
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
Página {page} de {totalPages}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Pagination className='w-fit m-0'>
|
|
||||||
<PaginationContent>
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationPrevious
|
|
||||||
href="#"
|
|
||||||
onClick={handlePreviousPage}
|
|
||||||
className={page <= 1 ? 'pointer-events-none opacity-50' : ''}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
|
|
||||||
{pageItems.map((pageItem, index) => (
|
|
||||||
<PaginationItem key={index}>
|
|
||||||
{pageItem === 'ellipsis' ? (
|
|
||||||
<PaginationEllipsis />
|
|
||||||
) : (
|
|
||||||
<PaginationLink
|
|
||||||
href="#"
|
|
||||||
onClick={() => handlePageClick(pageItem)}
|
|
||||||
isActive={pageItem === page}>
|
|
||||||
{pageItem}
|
|
||||||
</PaginationLink>
|
|
||||||
)}
|
|
||||||
</PaginationItem>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationNext
|
|
||||||
href="#"
|
|
||||||
onClick={handleNextPage}
|
|
||||||
className={
|
|
||||||
page >= totalPages ? 'pointer-events-none opacity-50' : ''
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button onClick={() => setOpen(false)}>Fechar</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProtocolsDialog;
|
|
||||||
|
|
@ -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<Protocol[]>([]);
|
||||||
|
const [counts] = useState<Record<string, number>>({ Todos: 0 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-5 lg:grid-cols-5">
|
||||||
|
{statuses.map((status) => (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(v) => {
|
||||||
|
setOpen(v);
|
||||||
|
}}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Card className="cursor-pointer hover:shadow-md transition-shadow bg-muted/50">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardDescription className="flex items-center gap-1">
|
||||||
|
{`Protocolos ${status.toLowerCase()}`}
|
||||||
|
<span title="Informações sobre os protocolos">
|
||||||
|
<InfoIcon size={14} />
|
||||||
|
</span>
|
||||||
|
</CardDescription>
|
||||||
|
<CardTitle className="text-3xl">
|
||||||
|
{counts[status] ?? 0}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent></CardContent>
|
||||||
|
</Card>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<ProtocolsTable
|
||||||
|
tasks={paginatedItems}
|
||||||
|
title={`Protocolos ${status.toLowerCase()}`}
|
||||||
|
description={`Lista de protocolos com status ${status.toLowerCase()}.`}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue