feat(main-routes): add pagination

This commit is contained in:
M-Gabrielly 2025-10-29 23:41:48 -03:00
parent 26d4077784
commit 90dc9823b7
3 changed files with 239 additions and 11 deletions

View File

@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { useEffect, useState, useCallback } from "react";
import { useEffect, useState, useCallback, useMemo } from "react";
import {
MoreHorizontal,
PlusCircle,
@ -87,6 +87,10 @@ export default function ConsultasPage() {
// Local form state used when editing. Keep hook at top-level to avoid Hooks order changes.
const [localForm, setLocalForm] = useState<any | null>(null);
// Paginação
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const mapAppointmentToFormData = (appointment: any) => {
// prefer scheduled_at (ISO) if available
const scheduledBase = appointment.scheduled_at || appointment.time || appointment.created_at || null;
@ -177,8 +181,8 @@ export default function ConsultasPage() {
let duration_minutes = 30;
try {
if (formData.startTime && formData.endTime) {
const [sh, sm] = String(formData.startTime).split(":").map((n: string) => Number(n));
const [eh, em] = String(formData.endTime).split(":").map((n: string) => Number(n));
const [sh, sm] = String(formData.startTime).split(":").map(Number);
const [eh, em] = String(formData.endTime).split(":").map(Number);
const start = (sh || 0) * 60 + (sm || 0);
const end = (eh || 0) * 60 + (em || 0);
if (!Number.isNaN(start) && !Number.isNaN(end) && end > start) duration_minutes = end - start;
@ -404,12 +408,28 @@ export default function ConsultasPage() {
performSearch(searchValue);
}, 250);
return () => clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue, originalAppointments]);
useEffect(() => {
applyFilters();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedStatus, filterDate, originalAppointments]);
// Dados paginados
const paginatedAppointments = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
return appointments.slice(startIndex, endIndex);
}, [appointments, currentPage, itemsPerPage]);
const totalPages = Math.ceil(appointments.length / itemsPerPage);
// Reset para página 1 quando mudar a busca ou itens por página
useEffect(() => {
setCurrentPage(1);
}, [searchValue, selectedStatus, filterDate, itemsPerPage]);
// Keep localForm synchronized with editingAppointment
useEffect(() => {
if (showForm && editingAppointment) {
@ -515,7 +535,7 @@ export default function ConsultasPage() {
</TableRow>
</TableHeader>
<TableBody>
{appointments.map((appointment) => {
{paginatedAppointments.map((appointment) => {
// appointment.professional may now contain the doctor's name (resolved)
const professionalLookup = mockProfessionals.find((p) => p.id === appointment.professional);
const professionalName = typeof appointment.professional === "string" && appointment.professional && !professionalLookup
@ -574,6 +594,64 @@ export default function ConsultasPage() {
</CardContent>
</Card>
{/* Controles de paginação */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Itens por página:</span>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
>
<option value={10}>10</option>
<option value={15}>15</option>
<option value={20}>20</option>
</select>
<span className="text-sm text-muted-foreground">
Mostrando {paginatedAppointments.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
{Math.min(currentPage * itemsPerPage, appointments.length)} de {appointments.length}
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
>
Primeira
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
>
Anterior
</Button>
<span className="text-sm text-muted-foreground">
Página {currentPage} de {totalPages || 1}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0}
>
Próxima
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
>
Última
</Button>
</div>
</div>
{viewingAppointment && (
<Dialog open={!!viewingAppointment} onOpenChange={() => setViewingAppointment(null)}>
<DialogContent>

View File

@ -141,6 +141,10 @@ export default function DoutoresPage() {
const [searchMode, setSearchMode] = useState(false);
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
// Paginação
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
async function load() {
setLoading(true);
@ -310,6 +314,20 @@ export default function DoutoresPage() {
return filtered;
}, [doctors, search, searchMode, searchResults]);
// Dados paginados
const paginatedDoctors = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
return displayedDoctors.slice(startIndex, endIndex);
}, [displayedDoctors, currentPage, itemsPerPage]);
const totalPages = Math.ceil(displayedDoctors.length / itemsPerPage);
// Reset para página 1 quando mudar a busca ou itens por página
useEffect(() => {
setCurrentPage(1);
}, [search, itemsPerPage, searchMode]);
function handleAdd() {
setEditingId(null);
setShowForm(true);
@ -480,8 +498,8 @@ export default function DoutoresPage() {
Carregando
</TableCell>
</TableRow>
) : displayedDoctors.length > 0 ? (
displayedDoctors.map((doctor) => (
) : paginatedDoctors.length > 0 ? (
paginatedDoctors.map((doctor) => (
<TableRow key={doctor.id}>
<TableCell className="font-medium">{doctor.full_name}</TableCell>
<TableCell>
@ -580,6 +598,64 @@ export default function DoutoresPage() {
</Table>
</div>
{/* Controles de paginação */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Itens por página:</span>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
>
<option value={10}>10</option>
<option value={15}>15</option>
<option value={20}>20</option>
</select>
<span className="text-sm text-muted-foreground">
Mostrando {paginatedDoctors.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
{Math.min(currentPage * itemsPerPage, displayedDoctors.length)} de {displayedDoctors.length}
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
>
Primeira
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
>
Anterior
</Button>
<span className="text-sm text-muted-foreground">
Página {currentPage} de {totalPages || 1}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0}
>
Próxima
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
>
Última
</Button>
</div>
</div>
{viewingDoctor && (
<Dialog open={!!viewingDoctor} onOpenChange={() => setViewingDoctor(null)}>
<DialogContent>
@ -753,7 +829,7 @@ export default function DoutoresPage() {
)}
<div className="text-sm text-muted-foreground">
Mostrando {displayedDoctors.length} {searchMode ? 'resultado(s) da busca' : `de ${doctors.length}`}
{searchMode ? 'Resultado(s) da busca' : `Total de ${doctors.length} médico(s)`}
</div>
{/* Dialog para pacientes atribuídos */}
<Dialog open={assignedDialogOpen} onOpenChange={(open) => { if (!open) { setAssignedDialogOpen(false); setAssignedPatients([]); setAssignedDoctor(null); } }}>

View File

@ -49,6 +49,10 @@ export default function PacientesPage() {
const [viewingPatient, setViewingPatient] = useState<Paciente | null>(null);
const [assignDialogOpen, setAssignDialogOpen] = useState(false);
const [assignPatientId, setAssignPatientId] = useState<string | null>(null);
// Paginação
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
async function loadAll() {
try {
@ -95,6 +99,20 @@ export default function PacientesPage() {
});
}, [patients, search]);
// Dados paginados
const paginatedData = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
return filtered.slice(startIndex, endIndex);
}, [filtered, currentPage, itemsPerPage]);
const totalPages = Math.ceil(filtered.length / itemsPerPage);
// Reset para página 1 quando mudar a busca ou itens por página
useEffect(() => {
setCurrentPage(1);
}, [search, itemsPerPage]);
function handleAdd() {
setEditingId(null);
setShowForm(true);
@ -228,8 +246,8 @@ export default function PacientesPage() {
</TableRow>
</TableHeader>
<TableBody>
{filtered.length > 0 ? (
filtered.map((p) => (
{paginatedData.length > 0 ? (
paginatedData.map((p) => (
<TableRow key={p.id}>
<TableCell className="font-medium">{p.full_name || "(sem nome)"}</TableCell>
<TableCell>{p.cpf || "-"}</TableCell>
@ -277,6 +295,64 @@ export default function PacientesPage() {
</Table>
</div>
{/* Controles de paginação */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Itens por página:</span>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
>
<option value={10}>10</option>
<option value={15}>15</option>
<option value={20}>20</option>
</select>
<span className="text-sm text-muted-foreground">
Mostrando {paginatedData.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
{Math.min(currentPage * itemsPerPage, filtered.length)} de {filtered.length}
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
>
Primeira
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
>
Anterior
</Button>
<span className="text-sm text-muted-foreground">
Página {currentPage} de {totalPages || 1}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0}
>
Próxima
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
>
Última
</Button>
</div>
</div>
{viewingPatient && (
<Dialog open={!!viewingPatient} onOpenChange={() => setViewingPatient(null)}>
<DialogContent className="max-w-2xl">
@ -326,8 +402,6 @@ export default function PacientesPage() {
onSaved={() => { setAssignDialogOpen(false); setAssignPatientId(null); loadAll(); }}
/>
)}
<div className="text-sm text-muted-foreground">Mostrando {filtered.length} de {patients.length}</div>
</div>
);
}