feat(protocols): implement ProtocolsDashboard and ProtocolsDialog with pagination and search functionality
parent
d0db593171
commit
6b997c4c8c
58
src/App.tsx
58
src/App.tsx
|
|
@ -11,48 +11,17 @@ import {
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from '@/views/components/ui/chart';
|
} from '@/views/components/ui/chart';
|
||||||
import { Progress } from '@/views/components/ui/progress';
|
|
||||||
import { TrendingUp } from 'lucide-react';
|
import { TrendingUp } from 'lucide-react';
|
||||||
import { Bar, BarChart, CartesianGrid, Pie, PieChart, XAxis } from 'recharts';
|
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';
|
||||||
export const description = 'A stacked bar chart with a legend';
|
export const description = 'A stacked bar chart with a legend';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const stats = [
|
// dashboard now uses ProtocolsDashboard for protocol status cards
|
||||||
{
|
|
||||||
title: 'Protocolos criados',
|
|
||||||
value: 'R$ 0,00',
|
|
||||||
description: '',
|
|
||||||
progress: 44.6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Protocolos tramitados',
|
|
||||||
value: '0 KM',
|
|
||||||
description: '',
|
|
||||||
progress: 30.6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Protocolos recebidos',
|
|
||||||
value: '0',
|
|
||||||
description: '',
|
|
||||||
progress: 24.8,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Protocolos finalizados',
|
|
||||||
value: '0',
|
|
||||||
description: '',
|
|
||||||
progress: 67.2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Protocolos cancelados',
|
|
||||||
value: '0',
|
|
||||||
description: '',
|
|
||||||
progress: 67.2,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const bestPricesConfig = {
|
const bestPricesConfig = {
|
||||||
price: {
|
price: {
|
||||||
|
|
@ -98,26 +67,7 @@ export function App() {
|
||||||
<div className="p-[18px] flex flex-col gap-6">
|
<div className="p-[18px] flex flex-col gap-6">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-5 lg:grid-cols-2 xl:grid-cols-5">
|
<ProtocolsDashboard />
|
||||||
{stats.map((stat) => (
|
|
||||||
<Card
|
|
||||||
key={stat.title}
|
|
||||||
className="hover:shadow-md transition-shadow duration-300">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
||||||
{stat.title}
|
|
||||||
</CardTitle>
|
|
||||||
<div className="text-4xl font-bold">{stat.value}</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-xs text-muted-foreground mb-4">
|
|
||||||
{stat.description}
|
|
||||||
</p>
|
|
||||||
<Progress value={stat.progress} className="h-2" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-6 grid-cols-1 md:grid-cols-2">
|
<div className="grid gap-6 grid-cols-1 md:grid-cols-2">
|
||||||
<Card>
|
<Card>
|
||||||
|
|
@ -157,7 +107,7 @@ export function App() {
|
||||||
<CardContent className="flex-1 pb-0">
|
<CardContent className="flex-1 pb-0">
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={vehicleStatusConfig}
|
config={vehicleStatusConfig}
|
||||||
className="[&_.recharts-pie-label-text]:fill-foreground mx-auto aspect-square max-h-[320px] pb-0">
|
className="[&_.recharts-pie-label-text]:fill-foreground mx-auto aspect-square max-h-80 pb-0">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
||||||
<Pie
|
<Pie
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
export type ProtocolStatus =
|
||||||
|
| 'Criado'
|
||||||
|
| 'Tramitado'
|
||||||
|
| 'Recebido'
|
||||||
|
| 'Finalizado'
|
||||||
|
| 'Cancelado'
|
||||||
|
| 'Arquivado'
|
||||||
|
| 'Enviado'
|
||||||
|
| 'Aguardando';
|
||||||
|
|
||||||
|
export type Protocol = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
date: string;
|
||||||
|
sender: string;
|
||||||
|
location: string;
|
||||||
|
status: ProtocolStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
const statuses: ProtocolStatus[] = [
|
||||||
|
'Criado',
|
||||||
|
'Tramitado',
|
||||||
|
'Recebido',
|
||||||
|
'Finalizado',
|
||||||
|
'Cancelado',
|
||||||
|
'Arquivado',
|
||||||
|
'Enviado',
|
||||||
|
'Aguardando',
|
||||||
|
];
|
||||||
|
|
||||||
|
// generate mock data
|
||||||
|
const protocols: Protocol[] = Array.from({ length: 120 }).map((_, i) => {
|
||||||
|
const status = statuses[i % statuses.length];
|
||||||
|
return {
|
||||||
|
id: String(1000 + i),
|
||||||
|
title: `NOTA FISCAL ${1000 + i}`,
|
||||||
|
date: new Date(Date.now() - i * 1000 * 60 * 60 * 24)
|
||||||
|
.toISOString()
|
||||||
|
.split('T')[0],
|
||||||
|
sender: `PREFEITURA ${i % 10}`,
|
||||||
|
location: ['Sala do financeiro', 'Protocolo Central', 'Expediente'][i % 3],
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ProtocolsPage = {
|
||||||
|
items: Protocol[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getProtocols({
|
||||||
|
status,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10,
|
||||||
|
search = '',
|
||||||
|
}: {
|
||||||
|
status?: ProtocolStatus | 'Todos';
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
search?: string;
|
||||||
|
}): Promise<ProtocolsPage> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
let items = protocols.slice();
|
||||||
|
if (status && status !== 'Todos') {
|
||||||
|
items = items.filter((p) => p.status === status);
|
||||||
|
}
|
||||||
|
if (search) {
|
||||||
|
const q = search.toLowerCase();
|
||||||
|
items = items.filter(
|
||||||
|
(p) =>
|
||||||
|
p.title.toLowerCase().includes(q) ||
|
||||||
|
p.sender.toLowerCase().includes(q),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const total = items.length;
|
||||||
|
const start = (page - 1) * pageSize;
|
||||||
|
const paged = items.slice(start, start + pageSize);
|
||||||
|
resolve({ items: paged, total });
|
||||||
|
}, 250);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCounts() {
|
||||||
|
const counts: Record<string, number> = { Todos: protocols.length };
|
||||||
|
for (const s of statuses) {
|
||||||
|
counts[s] = protocols.filter((p) => p.status === s).length;
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { cn } from '@/app/utils';
|
||||||
|
import { Button, buttonVariants } from '@/views/components/ui/button';
|
||||||
|
import { CaretLeftIcon, CaretRightIcon } from '@phosphor-icons/react';
|
||||||
|
import { MoreHorizontalIcon } from 'lucide-react';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
data-slot="pagination"
|
||||||
|
className={cn('mx-auto flex w-full justify-center', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'ul'>) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="pagination-content"
|
||||||
|
className={cn('flex flex-row items-center gap-1', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
||||||
|
return <li data-slot="pagination-item" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
||||||
|
React.ComponentProps<'a'>;
|
||||||
|
|
||||||
|
function PaginationLink({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
size = 'icon',
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
|
data-slot="pagination-link"
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? 'outline' : 'ghost',
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationPrevious({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
size="default"
|
||||||
|
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
||||||
|
{...props}>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
<span className="hidden sm:block">Anterior</span>
|
||||||
|
</PaginationLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationNext({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to next page"
|
||||||
|
size="default"
|
||||||
|
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
|
||||||
|
{...props}>
|
||||||
|
<span className="hidden sm:block">Próxima</span>
|
||||||
|
<CaretRightIcon />
|
||||||
|
</PaginationLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'span'>) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot="pagination-ellipsis"
|
||||||
|
className={cn('flex size-9 items-center justify-center', className)}
|
||||||
|
{...props}>
|
||||||
|
<MoreHorizontalIcon className="size-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/app/utils"
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
'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;
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
/* 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;
|
||||||
Loading…
Reference in New Issue