Merge pull request #30 from m1guelmcf/Stage

Stage
This commit is contained in:
DaniloSts 2025-11-27 16:22:00 -03:00 committed by GitHub
commit ed862da502
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 358 additions and 400 deletions

View File

@ -551,7 +551,7 @@ export default function AvailabilityPage() {
<Link href="/doctor/dashboard" className="w-full sm:w-auto"> <Link href="/doctor/dashboard" className="w-full sm:w-auto">
<Button variant="outline" className="w-full sm:w-auto">Cancelar</Button> <Button variant="outline" className="w-full sm:w-auto">Cancelar</Button>
</Link> </Link>
<Button type="submit" className="bg-green-600 hover:bg-green-700 w-full sm:w-auto"> <Button type="submit" className="bg-blue-600 hover:bg-blue-700 w-full sm:w-auto">
Salvar Disponibilidade Salvar Disponibilidade
</Button> </Button>
</div> </div>

View File

@ -10,9 +10,6 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
import { patientsService } from "@/services/patientsApi.mjs"; import { patientsService } from "@/services/patientsApi.mjs";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar";
// Defina o tamanho da página.
const PAGE_SIZE = 5;
export default function PacientesPage() { export default function PacientesPage() {
// --- ESTADOS DE DADOS E GERAL --- // --- ESTADOS DE DADOS E GERAL ---
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@ -30,10 +27,14 @@ export default function PacientesPage() {
// --- ESTADOS DE PAGINAÇÃO --- // --- ESTADOS DE PAGINAÇÃO ---
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
// PADRONIZAÇÃO: Iniciar com 10 itens por página
const [pageSize, setPageSize] = useState(10);
// CÁLCULO DA PAGINAÇÃO // CÁLCULO DA PAGINAÇÃO
const totalPages = Math.ceil(filteredPatients.length / PAGE_SIZE); const totalPages = Math.ceil(filteredPatients.length / pageSize);
const startIndex = (page - 1) * PAGE_SIZE; const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + PAGE_SIZE; const endIndex = startIndex + pageSize;
// Pacientes a serem exibidos na tabela (aplicando a paginação) // Pacientes a serem exibidos na tabela (aplicando a paginação)
const currentPatients = filteredPatients.slice(startIndex, endIndex); const currentPatients = filteredPatients.slice(startIndex, endIndex);
@ -51,7 +52,6 @@ export default function PacientesPage() {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
// Como o backend retorna um array, chamamos sem paginação
const res = await patientsService.list(); const res = await patientsService.list();
const mapped = res.map((p: any) => ({ const mapped = res.map((p: any) => ({
@ -60,11 +60,10 @@ export default function PacientesPage() {
telefone: p.phone_mobile ?? p.phone1 ?? "—", telefone: p.phone_mobile ?? p.phone1 ?? "—",
cidade: p.city ?? "—", cidade: p.city ?? "—",
estado: p.state ?? "—", estado: p.state ?? "—",
// Formate as datas se necessário, aqui usamos como string
ultimoAtendimento: p.last_visit_at?.split('T')[0] ?? "—", ultimoAtendimento: p.last_visit_at?.split('T')[0] ?? "—",
proximoAtendimento: p.next_appointment_at?.split('T')[0] ?? "—", proximoAtendimento: p.next_appointment_at?.split('T')[0] ?? "—",
vip: Boolean(p.vip ?? false), vip: Boolean(p.vip ?? false),
convenio: p.convenio ?? "Particular", // Define um valor padrão convenio: p.convenio ?? "Particular",
status: p.status ?? undefined, status: p.status ?? undefined,
})); }));
@ -77,20 +76,17 @@ export default function PacientesPage() {
} }
}, []); }, []);
// 2. Efeito para aplicar filtros e calcular a lista filtrada (chama-se quando allPatients ou filtros mudam) // 2. Efeito para aplicar filtros
useEffect(() => { useEffect(() => {
const filtered = allPatients.filter((patient) => { const filtered = allPatients.filter((patient) => {
// Filtro por termo de busca (Nome ou Telefone)
const matchesSearch = const matchesSearch =
patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) || patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) ||
patient.telefone?.includes(searchTerm); patient.telefone?.includes(searchTerm);
// Filtro por Convênio
const matchesConvenio = const matchesConvenio =
convenioFilter === "all" || convenioFilter === "all" ||
patient.convenio === convenioFilter; patient.convenio === convenioFilter;
// Filtro por VIP
const matchesVip = const matchesVip =
vipFilter === "all" || vipFilter === "all" ||
(vipFilter === "vip" && patient.vip) || (vipFilter === "vip" && patient.vip) ||
@ -100,24 +96,23 @@ export default function PacientesPage() {
}); });
setFilteredPatients(filtered); setFilteredPatients(filtered);
// Garante que a página atual seja válida após a filtragem setPage(1); // Reseta a página ao filtrar
setPage(1);
}, [allPatients, searchTerm, convenioFilter, vipFilter]); }, [allPatients, searchTerm, convenioFilter, vipFilter]);
// 3. Efeito inicial para buscar os pacientes // 3. Efeito inicial
useEffect(() => { useEffect(() => {
fetchAllPacientes(); fetchAllPacientes();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// --- LÓGICA DE AÇÕES (DELETAR / VER DETALHES) --- // --- LÓGICA DE AÇÕES ---
const openDetailsDialog = async (patientId: string) => { const openDetailsDialog = async (patientId: string) => {
setDetailsDialogOpen(true); setDetailsDialogOpen(true);
setPatientDetails(null); setPatientDetails(null);
try { try {
const res = await patientsService.getById(patientId); const res = await patientsService.getById(patientId);
setPatientDetails(Array.isArray(res) ? res[0] : res); // Supondo que retorne um array com um item setPatientDetails(Array.isArray(res) ? res[0] : res);
} catch (e: any) { } catch (e: any) {
setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" }); setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" });
} }
@ -126,7 +121,6 @@ export default function PacientesPage() {
const handleDeletePatient = async (patientId: string) => { const handleDeletePatient = async (patientId: string) => {
try { try {
await patientsService.delete(patientId); await patientsService.delete(patientId);
// Atualiza a lista completa para refletir a exclusão
setAllPatients((prev) => setAllPatients((prev) =>
prev.filter((p) => String(p.id) !== String(patientId)) prev.filter((p) => String(p.id) !== String(patientId))
); );
@ -145,7 +139,7 @@ export default function PacientesPage() {
return ( return (
<Sidebar> <Sidebar>
<div className="space-y-6 px-2 sm:px-4 md:px-8"> <div className="space-y-6 px-2 sm:px-4 md:px-8">
{/* Header (Responsividade OK) */} {/* Header */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div> <div>
<h1 className="text-xl md:text-2xl font-bold text-foreground"> <h1 className="text-xl md:text-2xl font-bold text-foreground">
@ -157,30 +151,26 @@ export default function PacientesPage() {
</div> </div>
</div> </div>
{/* Bloco de Filtros (Responsividade APLICADA) */} {/* Filtros */}
{/* Adicionado flex-wrap para permitir que os itens quebrem para a linha de baixo */}
<div className="flex flex-wrap items-center gap-4 bg-card p-4 rounded-lg border border-border"> <div className="flex flex-wrap items-center gap-4 bg-card p-4 rounded-lg border border-border">
<Filter className="w-5 h-5 text-gray-400" /> <Filter className="w-5 h-5 text-gray-400" />
{/* Busca - Ocupa 100% no mobile, depois cresce */} {/* Busca */}
<input <input
type="text" type="text"
placeholder="Buscar por nome ou telefone..." placeholder="Buscar por nome ou telefone..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
// w-full no mobile, depois flex-grow para ocupar o espaço disponível
className="w-full sm:flex-grow sm:max-w-[300px] p-2 border rounded-md text-sm" className="w-full sm:flex-grow sm:max-w-[300px] p-2 border rounded-md text-sm"
/> />
{/* Convênio - Ocupa a largura total em telas pequenas, depois se ajusta */} {/* Convênio */}
<div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[200px]"> <div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[200px]">
<span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block"> <span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">
Convênio Convênio
</span> </span>
<Select value={convenioFilter} onValueChange={setConvenioFilter}> <Select value={convenioFilter} onValueChange={setConvenioFilter}>
<SelectTrigger className="w-full sm:w-40"> <SelectTrigger className="w-full sm:w-40">
{" "}
{/* w-full para mobile, w-40 para sm+ */}
<SelectValue placeholder="Convênio" /> <SelectValue placeholder="Convênio" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -188,16 +178,15 @@ export default function PacientesPage() {
<SelectItem value="Particular">Particular</SelectItem> <SelectItem value="Particular">Particular</SelectItem>
<SelectItem value="SUS">SUS</SelectItem> <SelectItem value="SUS">SUS</SelectItem>
<SelectItem value="Unimed">Unimed</SelectItem> <SelectItem value="Unimed">Unimed</SelectItem>
{/* Adicione outros convênios conforme necessário */}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */} {/* VIP */}
<div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[150px]"> <div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[150px]">
<span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">VIP</span> <span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">VIP</span>
<Select value={vipFilter} onValueChange={setVipFilter}> <Select value={vipFilter} onValueChange={setVipFilter}>
<SelectTrigger className="w-full sm:w-32"> {/* w-full para mobile, w-32 para sm+ */} <SelectTrigger className="w-full sm:w-32">
<SelectValue placeholder="VIP" /> <SelectValue placeholder="VIP" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -208,14 +197,30 @@ export default function PacientesPage() {
</Select> </Select>
</div> </div>
{/* Seletor de Itens por Página (Inicia com 10) */}
<div className="flex items-center gap-2 w-full sm:w-auto ml-auto sm:ml-0">
<Select
value={String(pageSize)}
onValueChange={(value) => {
setPageSize(Number(value));
setPage(1); // Resetar para página 1 ao mudar o tamanho
}}
>
<SelectTrigger className="w-full sm:w-[70px]">
<SelectValue placeholder="10" />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
</SelectContent>
</Select>
</div>
</div> </div>
{/* --- SEÇÃO DE TABELA (VISÍVEL EM TELAS MAIORES OU IGUAIS A MD) --- */} {/* Tabela */}
{/* Garantir que a tabela se esconda em telas menores e apareça em MD+ */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md hidden md:block"> <div className="bg-white rounded-lg border border-gray-200 shadow-md hidden md:block">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
{" "}
{/* Permite rolagem horizontal se a tabela for muito larga */}
{error ? ( {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>
) : loading ? ( ) : loading ? (
@ -225,32 +230,15 @@ export default function PacientesPage() {
</div> </div>
) : ( ) : (
<table className="w-full min-w-[650px]"> <table className="w-full min-w-[650px]">
{" "}
{/* min-w para evitar que a tabela se contraia demais */}
<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-4 font-medium text-gray-700 w-[20%]"> <th className="text-left p-4 font-medium text-gray-700 w-[20%]">Nome</th>
Nome <th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">Telefone</th>
</th> <th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden md:table-cell">Cidade / Estado</th>
{/* Ajustes de visibilidade de colunas para diferentes breakpoints */} <th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">Convênio</th>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell"> <th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden lg:table-cell">Último atendimento</th>
Telefone <th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden lg:table-cell">Próximo atendimento</th>
</th> <th className="text-left p-4 font-medium text-gray-700 w-[5%]">Ações</th>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden md:table-cell">
Cidade / Estado
</th>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">
Convênio
</th>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden lg:table-cell">
Último atendimento
</th>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden lg:table-cell">
Próximo atendimento
</th>
<th className="text-left p-4 font-medium text-gray-700 w-[5%]">
Ações
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -264,10 +252,7 @@ export default function PacientesPage() {
</tr> </tr>
) : ( ) : (
currentPatients.map((patient) => ( currentPatients.map((patient) => (
<tr <tr key={patient.id} className="border-b border-gray-100 hover:bg-gray-50">
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">
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
@ -285,20 +270,11 @@ export default function PacientesPage() {
</span> </span>
</div> </div>
</td> </td>
<td className="p-4 text-gray-600 hidden sm:table-cell"> <td className="p-4 text-gray-600 hidden sm:table-cell">{patient.telefone}</td>
{patient.telefone}
</td>
<td className="p-4 text-gray-600 hidden md:table-cell">{`${patient.cidade} / ${patient.estado}`}</td> <td className="p-4 text-gray-600 hidden md:table-cell">{`${patient.cidade} / ${patient.estado}`}</td>
<td className="p-4 text-gray-600 hidden sm:table-cell"> <td className="p-4 text-gray-600 hidden sm:table-cell">{patient.convenio}</td>
{patient.convenio} <td className="p-4 text-gray-600 hidden lg:table-cell">{patient.ultimoAtendimento}</td>
</td> <td className="p-4 text-gray-600 hidden lg:table-cell">{patient.proximoAtendimento}</td>
<td className="p-4 text-gray-600 hidden lg:table-cell">
{patient.ultimoAtendimento}
</td>
<td className="p-4 text-gray-600 hidden lg:table-cell">
{patient.proximoAtendimento}
</td>
<td className="p-4"> <td className="p-4">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -307,34 +283,21 @@ export default function PacientesPage() {
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem <DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}>
onClick={() =>
openDetailsDialog(String(patient.id))
}
>
<Eye className="w-4 h-4 mr-2" /> <Eye className="w-4 h-4 mr-2" />
Ver detalhes Ver detalhes
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link <Link href={`/secretary/pacientes/${patient.id}/editar`} className="flex items-center w-full">
href={`/secretary/pacientes/${patient.id}/editar`}
className="flex items-center w-full"
>
<Edit className="w-4 h-4 mr-2" /> <Edit className="w-4 h-4 mr-2" />
Editar Editar
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem> <DropdownMenuItem>
<Calendar className="w-4 h-4 mr-2" /> <Calendar className="w-4 h-4 mr-2" />
Marcar consulta Marcar consulta
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(String(patient.id))}>
className="text-red-600"
onClick={() =>
openDeleteDialog(String(patient.id))
}
>
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
Excluir Excluir
</DropdownMenuItem> </DropdownMenuItem>
@ -354,8 +317,6 @@ export default function PacientesPage() {
{totalPages > 1 && !loading && ( {totalPages > 1 && !loading && (
<div className="flex flex-col sm:flex-row items-center justify-center p-4 border-t border-gray-200"> <div className="flex flex-col sm:flex-row items-center justify-center p-4 border-t border-gray-200">
<div className="flex space-x-2 flex-wrap justify-center"> <div className="flex space-x-2 flex-wrap justify-center">
{" "}
{/* Adicionado flex-wrap e justify-center para botões da paginação */}
<Button <Button
onClick={() => setPage((prev) => Math.max(1, prev - 1))} onClick={() => setPage((prev) => Math.max(1, prev - 1))}
disabled={page === 1} disabled={page === 1}
@ -382,9 +343,7 @@ export default function PacientesPage() {
</Button> </Button>
))} ))}
<Button <Button
onClick={() => onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
setPage((prev) => Math.min(totalPages, prev + 1))
}
disabled={page === totalPages} disabled={page === totalPages}
variant="outline" variant="outline"
size="lg" size="lg"
@ -395,22 +354,19 @@ export default function PacientesPage() {
</div> </div>
)} )}
{/* AlertDialogs (Permanecem os mesmos) */} {/* Dialog de Exclusão */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle> <AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Tem certeza que deseja excluir este paciente? Esta ação não pode Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita.
ser desfeita.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel> <AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={() => onClick={() => patientToDelete && handleDeletePatient(patientToDelete)}
patientToDelete && handleDeletePatient(patientToDelete)
}
className="bg-red-600 hover:bg-red-700" className="bg-red-600 hover:bg-red-700"
> >
Excluir Excluir
@ -419,10 +375,8 @@ export default function PacientesPage() {
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
<AlertDialog {/* Dialog de Detalhes */}
open={detailsDialogOpen} <AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
onOpenChange={setDetailsDialogOpen}
>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle> <AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle>

View File

@ -80,7 +80,7 @@ export default function PatientDashboard() {
<Link href="/patient/appointments"> <Link href="/patient/appointments">
<Button <Button
variant="outline" variant="outline"
className="w-full justify-start bg-transparent" className="w-full justify-start bg-transparent bg-blue-600 hover:bg-blue-700 text-white"
> >
<Calendar className="mr-2 h-4 w-4" /> <Calendar className="mr-2 h-4 w-4" />
Ver Minhas Consultas Ver Minhas Consultas

View File

@ -460,7 +460,7 @@ export default function PacientesPage() {
onClick={() => setPage(pageNumber)} onClick={() => setPage(pageNumber)}
variant={pageNumber === page ? "default" : "outline"} variant={pageNumber === page ? "default" : "outline"}
size="lg" size="lg"
className={pageNumber === page ? "bg-green-600 hover:bg-green-700 text-white" : "text-gray-700"} className={pageNumber === page ? "bg-blue-600 hover:bg-blue-700 text-white" : "text-gray-700"}
> >
{pageNumber} {pageNumber}
</Button> </Button>

View File

@ -400,13 +400,17 @@ export default function ScheduleForm() {
Carregando... Carregando...
</SelectItem> </SelectItem>
) : ( ) : (
doctors.map((d) => ( doctors
.slice() // evita mutar o state original
.sort((a, b) => a.full_name.localeCompare(b.full_name, "pt-BR"))
.map((d) => (
<SelectItem key={d.id} value={d.id}> <SelectItem key={d.id} value={d.id}>
{d.full_name} {d.specialty} {d.full_name} {d.specialty}
</SelectItem> </SelectItem>
)) ))
)} )}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</CardContent> </CardContent>