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,
|
||||
ChartTooltipContent,
|
||||
} from '@/views/components/ui/chart';
|
||||
import { Progress } from '@/views/components/ui/progress';
|
||||
import { TrendingUp } from 'lucide-react';
|
||||
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';
|
||||
export const description = 'A stacked bar chart with a legend';
|
||||
|
||||
export function App() {
|
||||
const stats = [
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
// dashboard now uses ProtocolsDashboard for protocol status cards
|
||||
|
||||
const bestPricesConfig = {
|
||||
price: {
|
||||
|
|
@ -98,26 +67,7 @@ export function App() {
|
|||
<div className="p-[18px] flex flex-col gap-6">
|
||||
<Header />
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-5 lg:grid-cols-2 xl:grid-cols-5">
|
||||
{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>
|
||||
<ProtocolsDashboard />
|
||||
|
||||
<div className="grid gap-6 grid-cols-1 md:grid-cols-2">
|
||||
<Card>
|
||||
|
|
@ -157,7 +107,7 @@ export function App() {
|
|||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer
|
||||
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>
|
||||
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
||||
<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