feat(main-routes): add pagination
This commit is contained in:
parent
26d4077784
commit
90dc9823b7
@ -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>
|
||||
|
||||
@ -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); } }}>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user