re-alteracoes esteticas

This commit is contained in:
pedrosiimoes 2025-11-06 00:18:34 -03:00
parent 1aec6b56d0
commit 1daa664ff4
3 changed files with 442 additions and 449 deletions

View File

@ -7,7 +7,7 @@ import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Plus, Edit, Trash2, Eye, Calendar, Filter, MoreVertical, Loader2 } from "lucide-react" import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react"
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@ -69,24 +69,20 @@ export default function DoctorsPage() {
const [specialtyFilter, setSpecialtyFilter] = useState("all"); const [specialtyFilter, setSpecialtyFilter] = useState("all");
const [statusFilter, setStatusFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all");
// --- Estados para Paginação (ADICIONADOS) --- // --- Estados para Paginação ---
const [itemsPerPage, setItemsPerPage] = useState(10); const [itemsPerPage, setItemsPerPage] = useState(10);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
// ---------------------------------------------
const fetchDoctors = useCallback(async () => { const fetchDoctors = useCallback(async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const data: Doctor[] = await doctorsService.list(); const data: Doctor[] = await doctorsService.list();
// Exemplo: Adicionando um status fake
const dataWithStatus = data.map((doc, index) => ({ const dataWithStatus = data.map((doc, index) => ({
...doc, ...doc,
status: index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo" status: index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo"
})); }));
setDoctors(dataWithStatus || []); setDoctors(dataWithStatus || []);
// IMPORTANTE: Resetar a página ao carregar novos dados
setCurrentPage(1); setCurrentPage(1);
} catch (e: any) { } catch (e: any) {
console.error("Erro ao carregar lista de médicos:", e); console.error("Erro ao carregar lista de médicos:", e);
@ -141,37 +137,23 @@ export default function DoctorsPage() {
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
}; };
const handleEdit = (doctorId: number) => {
router.push(`/manager/home/${doctorId}/editar`);
};
// Gera a lista de especialidades dinamicamente
const uniqueSpecialties = useMemo(() => { const uniqueSpecialties = useMemo(() => {
const specialties = doctors.map(doctor => doctor.specialty).filter(Boolean); const specialties = doctors.map(doctor => doctor.specialty).filter(Boolean);
return [...new Set(specialties)]; return [...new Set(specialties)];
}, [doctors]); }, [doctors]);
// Lógica de filtragem
const filteredDoctors = doctors.filter(doctor => { const filteredDoctors = doctors.filter(doctor => {
const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter; const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter;
const statusMatch = statusFilter === "all" || doctor.status === statusFilter; const statusMatch = statusFilter === "all" || doctor.status === statusFilter;
return specialtyMatch && statusMatch; return specialtyMatch && statusMatch;
}); });
// --- Lógica de Paginação (ADICIONADA) ---
// 1. Definição do total de páginas com base nos itens FILTRADOS
const totalPages = Math.ceil(filteredDoctors.length / itemsPerPage); const totalPages = Math.ceil(filteredDoctors.length / itemsPerPage);
// 2. Cálculo dos itens a serem exibidos na página atual (dos itens FILTRADOS)
const indexOfLastItem = currentPage * itemsPerPage; const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage; const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem); const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem);
// 3. Função para mudar de página
const paginate = (pageNumber: number) => setCurrentPage(pageNumber); const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
// 4. Funções para Navegação
const goToPrevPage = () => { const goToPrevPage = () => {
setCurrentPage((prev) => Math.max(1, prev - 1)); setCurrentPage((prev) => Math.max(1, prev - 1));
}; };
@ -180,7 +162,6 @@ export default function DoctorsPage() {
setCurrentPage((prev) => Math.min(totalPages, prev + 1)); setCurrentPage((prev) => Math.min(totalPages, prev + 1));
}; };
// 5. Lógica para gerar os números das páginas visíveis
const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { const getVisiblePageNumbers = (totalPages: number, currentPage: number) => {
const pages: number[] = []; const pages: number[] = [];
const maxVisiblePages = 5; const maxVisiblePages = 5;
@ -205,12 +186,10 @@ export default function DoctorsPage() {
const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage);
// Lógica para mudar itens por página, resetando para a página 1
const handleItemsPerPageChange = (value: string) => { const handleItemsPerPageChange = (value: string) => {
setItemsPerPage(Number(value)); setItemsPerPage(Number(value));
setCurrentPage(1); // Resetar para a primeira página setCurrentPage(1);
}; };
// ----------------------------------------------------
return ( return (
@ -232,11 +211,12 @@ export default function DoctorsPage() {
</div> </div>
{/* Filtros e Itens por Página (ATUALIZADO) */} {/* Filtros e Itens por Página */}
<div className="flex flex-wrap items-center gap-3 bg-white p-3 sm:p-4 rounded-lg border border-gray-200"> <div className="flex flex-wrap items-center gap-3 bg-white p-3 sm:p-4 rounded-lg border border-gray-200">
<Filter className="w-5 h-5 text-gray-400" /> <div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground">
{/* Filtro de Especialidade */} Especialidade
</span>
<Select value={specialtyFilter} onValueChange={setSpecialtyFilter}> <Select value={specialtyFilter} onValueChange={setSpecialtyFilter}>
<SelectTrigger className="w-[160px] sm:w-[180px]"> <SelectTrigger className="w-[160px] sm:w-[180px]">
<SelectValue placeholder="Especialidade" /> <SelectValue placeholder="Especialidade" />
@ -248,8 +228,11 @@ export default function DoctorsPage() {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div>
{/* Filtro de Status */} <div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground">
Status
</span>
<Select value={statusFilter} onValueChange={setStatusFilter}> <Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[160px] sm:w-[180px]"> <SelectTrigger className="w-[160px] sm:w-[180px]">
<SelectValue placeholder="Status" /> <SelectValue placeholder="Status" />
@ -261,12 +244,14 @@ export default function DoctorsPage() {
<SelectItem value="Inativo">Inativo</SelectItem> <SelectItem value="Inativo">Inativo</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div>
{/* Select de Itens por Página (ADICIONADO) */} <div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground">
Itens por página
</span>
<Select <Select
onValueChange={handleItemsPerPageChange} onValueChange={handleItemsPerPageChange}
defaultValue={String(itemsPerPage)} defaultValue={String(itemsPerPage)}>
>
<SelectTrigger className="w-[140px]"> <SelectTrigger className="w-[140px]">
<SelectValue placeholder="Itens por pág." /> <SelectValue placeholder="Itens por pág." />
</SelectTrigger> </SelectTrigger>
@ -276,7 +261,7 @@ export default function DoctorsPage() {
<SelectItem value="20">20 por página</SelectItem> <SelectItem value="20">20 por página</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div>
<Button variant="outline" className="ml-auto w-full md:w-auto"> <Button variant="outline" className="ml-auto w-full md:w-auto">
<Filter className="w-4 h-4 mr-2" /> <Filter className="w-4 h-4 mr-2" />
Filtro avançado Filtro avançado
@ -302,19 +287,18 @@ export default function DoctorsPage() {
</div> </div>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-[700px] w-full divide-y divide-gray-200 text-sm sm:text-base"> <table className="w-full min-w-[600px]">
<thead className="bg-gray-50"> <thead className="bg-gray-50 border-b border-gray-200">
<tr> <tr>
<th className="px-4 py-3 text-left font-medium text-gray-500 uppercase tracking-wider">Nome</th> <th className="text-left p-2 md:p-4 font-medium text-gray-700">Nome</th>
<th className="px-4 py-3 text-left font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">CRM</th> <th className="text-left p-2 md:p-4 font-medium text-gray-700">CRM</th>
<th className="px-4 py-3 text-left font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">Especialidade</th> <th className="text-left p-2 md:p-4 font-medium text-gray-700">Especialidade</th>
<th className="px-4 py-3 text-left font-medium text-gray-500 uppercase tracking-wider hidden lg:table-cell">Status</th> <th className="text-left p-2 md:p-4 font-medium text-gray-700">Status</th>
<th className="px-4 py-3 text-left font-medium text-gray-500 uppercase tracking-wider hidden xl:table-cell">Cidade/Estado</th> <th className="text-left p-2 md:p-4 font-medium text-gray-700">Cidade/Estado</th>
<th className="px-4 py-3 text-right font-medium text-gray-500 uppercase tracking-wider">Ações</th> <th className="text-right p-4 font-medium text-gray-700">Ações</th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{/* Mapeia APENAS os itens da página atual (currentItems) */}
{currentItems.map((doctor) => ( {currentItems.map((doctor) => (
<tr key={doctor.id} className="hover:bg-gray-50 transition"> <tr key={doctor.id} className="hover:bg-gray-50 transition">
<td className="px-4 py-3 font-medium text-gray-900">{doctor.full_name}</td> <td className="px-4 py-3 font-medium text-gray-900">{doctor.full_name}</td>
@ -327,19 +311,33 @@ export default function DoctorsPage() {
: "N/A"} : "N/A"}
</td> </td>
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
<div className="flex justify-end gap-2"> {/* ===== INÍCIO DA ALTERAÇÃO ===== */}
<Button variant="outline" size="icon" onClick={() => openDetailsDialog(doctor)} title="Visualizar Detalhes"><Eye className="h-4 w-4" /></Button>
<Button variant="outline" size="icon" onClick={() => handleEdit(doctor.id)} title="Editar"><Edit className="h-4 w-4 text-blue-600" /></Button>
<Button variant="outline" size="icon" onClick={() => openDeleteDialog(doctor.id)} title="Excluir"><Trash2 className="h-4 w-4 text-red-600" /></Button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0" title="Mais Ações"><MoreVertical className="h-4 w-4" /></Button> <div className="text-blue-600 cursor-pointer inline-block">Ações</div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem><Calendar className="mr-2 h-4 w-4" />Agendar Consulta</DropdownMenuItem> <DropdownMenuItem onClick={() => openDetailsDialog(doctor)}>
<Eye className="mr-2 h-4 w-4" />
Ver detalhes
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/manager/home/${doctor.id}/editar`}>
<Edit className="mr-2 h-4 w-4" />
Editar
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Calendar className="mr-2 h-4 w-4" />
Marcar consulta
</DropdownMenuItem>
<DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(doctor.id)}>
<Trash2 className="mr-2 h-4 w-4" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> {/* ===== FIM DA ALTERAÇÃO ===== */}
</td> </td>
</tr> </tr>
))} ))}
@ -349,11 +347,9 @@ export default function DoctorsPage() {
)} )}
</div> </div>
{/* Paginação (ADICIONADA) */} {/* Paginação */}
{totalPages > 1 && ( {totalPages > 1 && (
<div className="flex flex-wrap justify-center items-center gap-2 mt-4 p-4 bg-white rounded-lg border border-gray-200 shadow-md"> <div className="flex flex-wrap justify-center items-center gap-2 mt-4 p-4 bg-white rounded-lg border border-gray-200 shadow-md">
{/* Botão Anterior */}
<button <button
onClick={goToPrevPage} onClick={goToPrevPage}
disabled={currentPage === 1} disabled={currentPage === 1}
@ -362,13 +358,11 @@ export default function DoctorsPage() {
{"< Anterior"} {"< Anterior"}
</button> </button>
{/* Números das Páginas */}
{visiblePageNumbers.map((number) => ( {visiblePageNumbers.map((number) => (
<button <button
key={number} key={number}
onClick={() => paginate(number)} onClick={() => paginate(number)}
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-gray-300 ${ className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-gray-300 ${currentPage === number
currentPage === number
? "bg-green-600 text-white shadow-md border-green-600" ? "bg-green-600 text-white shadow-md border-green-600"
: "bg-gray-100 text-gray-700 hover:bg-gray-200" : "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`} }`}
@ -377,7 +371,6 @@ export default function DoctorsPage() {
</button> </button>
))} ))}
{/* Botão Próximo */}
<button <button
onClick={goToNextPage} onClick={goToNextPage}
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
@ -385,11 +378,10 @@ export default function DoctorsPage() {
> >
{"Próximo >"} {"Próximo >"}
</button> </button>
</div> </div>
)} )}
{/* Dialogs de Exclusão e Detalhes (Sem alterações) */} {/* Dialogs de Exclusão e Detalhes */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect, useRef, useCallback } from "react"; import React, { useState, useEffect, useCallback, useMemo } from "react";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -16,7 +16,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Plus, Edit, Trash2, Eye, Calendar, Filter } from "lucide-react"; import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@ -30,29 +30,22 @@ import {
import ManagerLayout from "@/components/manager-layout"; import ManagerLayout from "@/components/manager-layout";
import { patientsService } from "@/services/patientsApi.mjs"; import { patientsService } from "@/services/patientsApi.mjs";
// 📅 PASSO 1: Criar uma função para formatar a data
const formatDate = (dateString: string | null | undefined): string => { const formatDate = (dateString: string | null | undefined): string => {
// Se a data não existir, retorna um texto padrão
if (!dateString) { if (!dateString) {
return "N/A"; return "N/A";
} }
try { try {
const date = new Date(dateString); const date = new Date(dateString);
// Verifica se a data é válida após a conversão
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
return "Data inválida"; return "Data inválida";
} }
const day = String(date.getDate()).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0'); // Mês é base 0, então +1 const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear(); const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`; return `${day}/${month}/${year} ${hours}:${minutes}`;
} catch (error) { } catch (error) {
// Se houver qualquer erro na conversão, retorna um texto de erro
return "Data inválida"; return "Data inválida";
} }
}; };
@ -63,16 +56,19 @@ export default function PacientesPage() {
const [convenioFilter, setConvenioFilter] = useState("all"); const [convenioFilter, setConvenioFilter] = useState("all");
const [vipFilter, setVipFilter] = useState("all"); const [vipFilter, setVipFilter] = useState("all");
const [patients, setPatients] = useState<any[]>([]); const [patients, setPatients] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(true); // Alterado para true
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [hasNext, setHasNext] = useState(true); // --- Estados de Paginação (ADICIONADOS) ---
const [isFetching, setIsFetching] = useState(false); const [itemsPerPage, setItemsPerPage] = useState(10);
const observerRef = useRef<HTMLDivElement | null>(null); const [currentPage, setCurrentPage] = useState(1);
// ---------------------------------------------
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [patientToDelete, setPatientToDelete] = useState<string | null>(null); const [patientToDelete, setPatientToDelete] = useState<string | null>(null);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [patientDetails, setPatientDetails] = useState<any | null>(null); const [patientDetails, setPatientDetails] = useState<any | null>(null);
const openDetailsDialog = async (patientId: string) => { const openDetailsDialog = async (patientId: string) => {
setDetailsDialogOpen(true); setDetailsDialogOpen(true);
setPatientDetails(null); setPatientDetails(null);
@ -84,10 +80,9 @@ export default function PacientesPage() {
} }
}; };
const fetchPacientes = useCallback( // --- LÓGICA DE BUSCA DE DADOS (ATUALIZADA) ---
async (pageToFetch: number) => { const fetchPacientes = useCallback(async () => {
if (isFetching || !hasNext) return; setLoading(true);
setIsFetching(true);
setError(null); setError(null);
try { try {
const res = await patientsService.list(); const res = await patientsService.list();
@ -100,59 +95,28 @@ export default function PacientesPage() {
ultimoAtendimento: p.last_visit_at ?? "", ultimoAtendimento: p.last_visit_at ?? "",
proximoAtendimento: p.next_appointment_at ?? "", proximoAtendimento: p.next_appointment_at ?? "",
vip: Boolean(p.vip ?? false), vip: Boolean(p.vip ?? false),
convenio: p.convenio ?? "", // se não existir, fica vazio convenio: p.convenio ?? "",
status: p.status ?? undefined, status: p.status ?? undefined,
})); }));
setPatients(mapped);
setPatients((prev) => { setCurrentPage(1); // Resetar para a primeira página ao carregar
const all = [...prev, ...mapped];
const unique = Array.from(
new Map(all.map((p) => [p.id, p])).values()
);
return unique;
});
if (!mapped.id) setHasNext(false); // parar carregamento
else setPage((prev) => prev + 1);
} catch (e: any) { } catch (e: any) {
setError(e?.message || "Erro ao buscar pacientes"); setError(e?.message || "Erro ao buscar pacientes");
setPatients([]);
} finally { } finally {
setIsFetching(false); setLoading(false);
} }
},
[isFetching, hasNext]
);
useEffect(() => {
fetchPacientes(page);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!observerRef.current || !hasNext) return; fetchPacientes();
const observer = new window.IntersectionObserver((entries) => { }, [fetchPacientes]);
if (entries[0].isIntersecting && !isFetching && hasNext) {
fetchPacientes(page);
}
});
observer.observe(observerRef.current);
return () => {
if (observerRef.current) observer.unobserve(observerRef.current);
};
}, [fetchPacientes, page, hasNext, isFetching]);
const handleDeletePatient = async (patientId: string) => { const handleDeletePatient = async (patientId: string) => {
// Remove from current list (client-side deletion)
try { try {
const res = await patientsService.delete(patientId); await patientsService.delete(patientId);
// Recarrega a lista para refletir a exclusão
if (res) { await fetchPacientes();
alert(`${res.error} ${res.message}`);
}
setPatients((prev) =>
prev.filter((p) => String(p.id) !== String(patientId))
);
} catch (e: any) { } catch (e: any) {
setError(e?.message || "Erro ao deletar paciente"); setError(e?.message || "Erro ao deletar paciente");
} }
@ -179,6 +143,50 @@ export default function PacientesPage() {
return matchesSearch && matchesConvenio && matchesVip; return matchesSearch && matchesConvenio && matchesVip;
}); });
// --- LÓGICA DE PAGINAÇÃO (ADICIONADA) ---
const totalPages = Math.ceil(filteredPatients.length / itemsPerPage);
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = filteredPatients.slice(indexOfFirstItem, indexOfLastItem);
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
const goToPrevPage = () => {
setCurrentPage((prev) => Math.max(1, prev - 1));
};
const goToNextPage = () => {
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
};
const getVisiblePageNumbers = (totalPages: number, currentPage: number) => {
const pages: number[] = [];
const maxVisiblePages = 5;
const halfRange = Math.floor(maxVisiblePages / 2);
let startPage = Math.max(1, currentPage - halfRange);
let endPage = Math.min(totalPages, currentPage + halfRange);
if (endPage - startPage + 1 < maxVisiblePages) {
if (endPage === totalPages) {
startPage = Math.max(1, totalPages - maxVisiblePages + 1);
}
if (startPage === 1) {
endPage = Math.min(totalPages, maxVisiblePages);
}
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
};
const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage);
const handleItemsPerPageChange = (value: string) => {
setItemsPerPage(Number(value));
setCurrentPage(1); // Resetar para a primeira página
};
// ---------------------------------------------
return ( return (
<ManagerLayout> <ManagerLayout>
<div className="space-y-6"> <div className="space-y-6">
@ -193,16 +201,15 @@ export default function PacientesPage() {
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Link href="/manager/pacientes/novo"> <Link href="/manager/pacientes/novo">
<Button className="w-full md:w-auto"> <Button className="w-full md:w-auto bg-green-600 hover:bg-green-700">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Adicionar Adicionar Novo
</Button> </Button>
</Link> </Link>
</div> </div>
</div> </div>
<div className="flex flex-col md:flex-row flex-wrap gap-4 bg-card p-4 rounded-lg border border-border"> <div className="flex flex-col md:flex-row flex-wrap gap-4 bg-card p-4 rounded-lg border border-border">
{/* Convênio */}
<div className="flex items-center gap-2 w-full md:w-auto"> <div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground"> <span className="text-sm font-medium text-foreground">
Convênio Convênio
@ -233,18 +240,18 @@ export default function PacientesPage() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* SELETOR DE ITENS POR PÁGINA (ADICIONADO) */}
<div className="flex items-center gap-2 w-full md:w-auto"> <div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground"> <span className="text-sm font-medium text-foreground">Itens por página</span>
Aniversariantes <Select onValueChange={handleItemsPerPageChange} defaultValue={String(itemsPerPage)}>
</span>
<Select>
<SelectTrigger className="w-full md:w-32"> <SelectTrigger className="w-full md:w-32">
<SelectValue placeholder="Selecione" /> <SelectValue placeholder="Itens por pág." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="today">Hoje</SelectItem> <SelectItem value="5">5 por página</SelectItem>
<SelectItem value="week">Esta semana</SelectItem> <SelectItem value="10">10 por página</SelectItem>
<SelectItem value="month">Este mês</SelectItem> <SelectItem value="20">20 por página</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -257,30 +264,34 @@ export default function PacientesPage() {
<div className="bg-white rounded-lg border border-gray-200"> <div className="bg-white rounded-lg border border-gray-200">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
{error ? ( {loading ? (
<div className="p-8 text-center text-gray-500">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" />
Carregando pacientes...
</div>
) : error ? (
<div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div> <div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div>
) : ( ) : (
<table className="w-full min-w-[600px]"> <table className="w-full min-w-[600px]">
<thead className="bg-gray-50 border-b border-gray-200"> <thead className="bg-gray-50 border-b border-gray-200">
<tr> <tr>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Nome</th> <th className="text-left p-4 font-medium text-gray-700">Nome</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Telefone</th> <th className="text-left p-4 font-medium text-gray-700">Telefone</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Cidade</th> <th className="text-left p-4 font-medium text-gray-700">Cidade</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Estado</th> <th className="text-left p-4 font-medium text-gray-700">Último Atendimento</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Último atendimento</th> <th className="text-right p-4 font-medium text-gray-700">Ações</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Próximo atendimento</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filteredPatients.length === 0 ? ( {filteredPatients.length === 0 ? (
<tr> <tr>
<td colSpan={7} className="p-8 text-center text-gray-500"> <td colSpan={5} className="p-8 text-center text-gray-500">
{patients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"} {patients.length === 0 ? "Nenhum paciente cadastrado." : "Nenhum paciente encontrado com os filtros aplicados."}
</td> </td>
</tr> </tr>
) : ( ) : (
filteredPatients.map((patient) => ( // Mapeando `currentItems` em vez de `filteredPatients`
currentItems.map((patient) => (
<tr key={patient.id} className="border-b border-gray-100 hover:bg-gray-50"> <tr key={patient.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="p-4"> <td className="p-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -292,14 +303,11 @@ export default function PacientesPage() {
</td> </td>
<td className="p-4 text-gray-600">{patient.telefone}</td> <td className="p-4 text-gray-600">{patient.telefone}</td>
<td className="p-4 text-gray-600">{patient.cidade}</td> <td className="p-4 text-gray-600">{patient.cidade}</td>
<td className="p-4 text-gray-600">{patient.estado}</td>
{/* 📅 PASSO 2: Aplicar a formatação de data na tabela */}
<td className="p-4 text-gray-600">{formatDate(patient.ultimoAtendimento)}</td> <td className="p-4 text-gray-600">{formatDate(patient.ultimoAtendimento)}</td>
<td className="p-4 text-gray-600">{formatDate(patient.proximoAtendimento)}</td> <td className="p-4 text-right">
<td className="p-4">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<div className="text-blue-600 cursor-pointer">Ações</div> <div className="text-blue-600 cursor-pointer inline-block">Ações</div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}> <DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}>
@ -329,11 +337,44 @@ export default function PacientesPage() {
</tbody> </tbody>
</table> </table>
)} )}
<div ref={observerRef} style={{ height: 1 }} />
{isFetching && <div className="p-4 text-center text-gray-500">Carregando mais pacientes...</div>}
</div> </div>
</div> </div>
{/* COMPONENTE DE PAGINAÇÃO (ADICIONADO) */}
{totalPages > 1 && (
<div className="flex flex-wrap justify-center items-center gap-2 mt-4 p-4 bg-white rounded-lg border border-gray-200 shadow-md">
<button
onClick={goToPrevPage}
disabled={currentPage === 1}
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300"
>
{"< Anterior"}
</button>
{visiblePageNumbers.map((number) => (
<button
key={number}
onClick={() => paginate(number)}
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-gray-300 ${currentPage === number
? "bg-green-600 text-white shadow-md border-green-600"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
{number}
</button>
))}
<button
onClick={goToNextPage}
disabled={currentPage === totalPages}
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300"
>
{"Próximo >"}
</button>
</div>
)}
{/* MODAIS (SEM ALTERAÇÃO) */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@ -357,7 +398,6 @@ export default function PacientesPage() {
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{/* Modal de detalhes do paciente */}
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}> <AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@ -369,63 +409,12 @@ export default function PacientesPage() {
<div className="text-red-600">{patientDetails.error}</div> <div className="text-red-600">{patientDetails.error}</div>
) : ( ) : (
<div className="space-y-2 text-left"> <div className="space-y-2 text-left">
<p> <p><strong>Nome:</strong> {patientDetails.full_name}</p>
<strong>Nome:</strong> {patientDetails.full_name} <p><strong>CPF:</strong> {patientDetails.cpf}</p>
</p> <p><strong>Email:</strong> {patientDetails.email}</p>
<p> <p><strong>Telefone:</strong> {patientDetails.phone_mobile ?? patientDetails.phone1 ?? patientDetails.phone2 ?? "-"}</p>
<strong>CPF:</strong> {patientDetails.cpf} <p><strong>Endereço:</strong> {`${patientDetails.street ?? "-"}, ${patientDetails.neighborhood ?? "-"}, ${patientDetails.city ?? "-"} - ${patientDetails.state ?? "-"}`}</p>
</p> <p><strong>Criado em:</strong> {formatDate(patientDetails.created_at)}</p>
<p>
<strong>Email:</strong> {patientDetails.email}
</p>
<p>
<strong>Telefone:</strong> {patientDetails.phone_mobile ?? patientDetails.phone1 ?? patientDetails.phone2 ?? "-"}
</p>
<p>
<strong>Nome social:</strong> {patientDetails.social_name ?? "-"}
</p>
<p>
<strong>Sexo:</strong> {patientDetails.sex ?? "-"}
</p>
<p>
<strong>Tipo sanguíneo:</strong> {patientDetails.blood_type ?? "-"}
</p>
<p>
<strong>Peso:</strong> {patientDetails.weight_kg ?? "-"}
{patientDetails.weight_kg ? "kg" : ""}
</p>
<p>
<strong>Altura:</strong> {patientDetails.height_m ?? "-"}
{patientDetails.height_m ? "m" : ""}
</p>
<p>
<strong>IMC:</strong> {patientDetails.bmi ?? "-"}
</p>
<p>
<strong>Endereço:</strong> {patientDetails.street ?? "-"}
</p>
<p>
<strong>Bairro:</strong> {patientDetails.neighborhood ?? "-"}
</p>
<p>
<strong>Cidade:</strong> {patientDetails.city ?? "-"}
</p>
<p>
<strong>Estado:</strong> {patientDetails.state ?? "-"}
</p>
<p>
<strong>CEP:</strong> {patientDetails.cep ?? "-"}
</p>
{/* 📅 PASSO 3: Aplicar a formatação de data no modal */}
<p>
<strong>Criado em:</strong> {formatDate(patientDetails.created_at)}
</p>
<p>
<strong>Atualizado em:</strong> {formatDate(patientDetails.updated_at)}
</p>
<p>
<strong>Id:</strong> {patientDetails.id ?? "-"}
</p>
</div> </div>
)} )}
</AlertDialogDescription> </AlertDialogDescription>

View File

@ -210,16 +210,19 @@ export default function UsersPage() {
{/* Filtro e Itens por Página */} {/* Filtro e Itens por Página */}
<div className="flex flex-wrap items-center gap-3 bg-white p-4 rounded-lg border border-gray-200"> <div className="flex flex-wrap items-center gap-3 bg-white p-4 rounded-lg border border-gray-200">
<Filter className="w-5 h-5 text-gray-400" />
{/* Select de Filtro por Papel - Ajustado para resetar a página */} {/* Select de Filtro por Papel - Ajustado para resetar a página */}
<div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground">
Filtrar por papel
</span>
<Select <Select
onValueChange={(value) => { onValueChange={(value) => {
setSelectedRole(value); setSelectedRole(value);
setCurrentPage(1); // Resetar para a primeira página ao mudar o filtro setCurrentPage(1); // Resetar para a primeira página ao mudar o filtro
}} }}
value={selectedRole} value={selectedRole}>
>
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filtrar por papel" /> <SelectValue placeholder="Filtrar por papel" />
</SelectTrigger> </SelectTrigger>
@ -232,8 +235,13 @@ export default function UsersPage() {
<SelectItem value="user">Usuário</SelectItem> <SelectItem value="user">Usuário</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div>
{/* Select de Itens por Página */} {/* Select de Itens por Página */}
<div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground">
Itens por página
</span>
<Select <Select
onValueChange={handleItemsPerPageChange} onValueChange={handleItemsPerPageChange}
defaultValue={String(itemsPerPage)} defaultValue={String(itemsPerPage)}
@ -248,6 +256,11 @@ export default function UsersPage() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<Button variant="outline" className="ml-auto w-full md:w-auto">
<Filter className="w-4 h-4 mr-2" />
Filtro avançado
</Button>
</div>
{/* Fim do Filtro e Itens por Página */} {/* Fim do Filtro e Itens por Página */}
{/* Tabela */} {/* Tabela */}
@ -331,8 +344,7 @@ export default function UsersPage() {
<button <button
key={number} key={number}
onClick={() => paginate(number)} onClick={() => paginate(number)}
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-gray-300 ${ className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-gray-300 ${currentPage === number
currentPage === number
? "bg-green-600 text-white shadow-md border-green-600" ? "bg-green-600 text-white shadow-md border-green-600"
: "bg-gray-100 text-gray-700 hover:bg-gray-200" : "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`} }`}