ordem alfabetica e visual

This commit is contained in:
m1guelmcf 2025-11-26 22:59:24 -03:00
parent 883411b8a3
commit dbc5a64ccd
2 changed files with 530 additions and 388 deletions

View File

@ -3,10 +3,39 @@
import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2, MoreVertical } from "lucide-react";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Plus,
Edit,
Trash2,
Eye,
Calendar,
Filter,
Loader2,
MoreVertical,
} from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { patientsService } from "@/services/patientsApi.mjs";
import Sidebar from "@/components/Sidebar";
@ -46,8 +75,7 @@ export default function PacientesPage() {
// --- FUNÇÕES DE LÓGICA ---
// 1. Função para carregar TODOS os pacientes da API
const fetchAllPacientes = useCallback(
async () => {
const fetchAllPacientes = useCallback(async () => {
setLoading(true);
setError(null);
try {
@ -61,8 +89,10 @@ export default function PacientesPage() {
cidade: p.city ?? "—",
estado: p.state ?? "—",
// Formate as datas se necessário, aqui usamos como string
ultimoAtendimento: p.last_visit_at?.split('T')[0] ?? "—",
proximoAtendimento: p.next_appointment_at ? p.next_appointment_at.split('T')[0].split('-').reverse().join('-') : "—",
ultimoAtendimento: p.last_visit_at?.split("T")[0] ?? "—",
proximoAtendimento: p.next_appointment_at
? p.next_appointment_at.split("T")[0].split("-").reverse().join("-")
: "—",
vip: Boolean(p.vip ?? false),
convenio: p.convenio ?? "Particular", // Define um valor padrão
status: p.status ?? undefined,
@ -87,8 +117,7 @@ export default function PacientesPage() {
// Filtro por Convênio
const matchesConvenio =
convenioFilter === "all" ||
patient.convenio === convenioFilter;
convenioFilter === "all" || patient.convenio === convenioFilter;
// Filtro por VIP
const matchesVip =
@ -195,9 +224,13 @@ export default function PacientesPage() {
{/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */}
<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}>
<SelectTrigger className="w-full sm:w-32"> {/* w-full para mobile, w-32 para sm+ */}
<SelectTrigger className="w-full sm:w-32">
{" "}
{/* w-full para mobile, w-32 para sm+ */}
<SelectValue placeholder="VIP" />
</SelectTrigger>
<SelectContent>
@ -218,55 +251,91 @@ export default function PacientesPage() {
{/* --- SEÇÃO DE TABELA (VISÍVEL EM TELAS MAIORES OU IGUAIS A MD) --- */}
{/* 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="overflow-x-auto"> {/* Permite rolagem horizontal se a tabela for muito larga */}
<div className="overflow-x-auto">
{" "}
{/* Permite rolagem horizontal se a tabela for muito larga */}
{error ? (
<div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div>
) : loading ? (
<div className="p-6 text-center text-gray-500 flex items-center justify-center">
<Loader2 className="w-6 h-6 mr-2 animate-spin text-green-600" /> Carregando pacientes...
<Loader2 className="w-6 h-6 mr-2 animate-spin text-green-600" />{" "}
Carregando pacientes...
</div>
) : (
<table className="w-full min-w-[650px]"> {/* min-w para evitar que a tabela se contraia demais */}
<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">
<tr>
<th className="text-left p-4 font-medium text-gray-700 w-[20%]">Nome</th>
<th className="text-left p-4 font-medium text-gray-700 w-[20%]">
Nome
</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">Telefone</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>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">
Telefone
</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>
</thead>
<tbody>
{currentPatients.length === 0 ? (
<tr>
<td colSpan={7} className="p-8 text-center text-gray-500">
{allPatients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"}
{allPatients.length === 0
? "Nenhum paciente cadastrado"
: "Nenhum paciente encontrado com os filtros aplicados"}
</td>
</tr>
) : (
currentPatients.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">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<span className="text-green-600 font-medium text-sm">{patient.nome?.charAt(0) || "?"}</span>
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-blue-600 font-medium text-sm">
{patient.nome?.charAt(0) || "?"}
</span>
</div>
<span className="font-medium text-gray-900">
{patient.nome}
{patient.vip && (
<span className="ml-2 px-2 py-0.5 text-xs font-semibold text-purple-600 bg-purple-100 rounded-full">VIP</span>
<span className="ml-2 px-2 py-0.5 text-xs font-semibold text-purple-600 bg-purple-100 rounded-full">
VIP
</span>
)}
</span>
</div>
</td>
<td className="p-4 text-gray-600 hidden sm:table-cell">{patient.telefone}</td>
<td className="p-4 text-gray-600 hidden sm:table-cell">
{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 sm:table-cell">{patient.convenio}</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 text-gray-600 hidden sm:table-cell">
{patient.convenio}
</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">
<DropdownMenu>
@ -276,12 +345,19 @@ export default function PacientesPage() {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}>
<DropdownMenuItem
onClick={() =>
openDetailsDialog(String(patient.id))
}
>
<Eye className="w-4 h-4 mr-2" />
Ver detalhes
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/secretary/pacientes/${patient.id}/editar`} className="flex items-center w-full">
<Link
href={`/secretary/pacientes/${patient.id}/editar`}
className="flex items-center w-full"
>
<Edit className="w-4 h-4 mr-2" />
Editar
</Link>
@ -291,7 +367,12 @@ export default function PacientesPage() {
<Calendar className="w-4 h-4 mr-2" />
Marcar consulta
</DropdownMenuItem>
<DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(String(patient.id))}>
<DropdownMenuItem
className="text-red-600"
onClick={() =>
openDeleteDialog(String(patient.id))
}
>
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
@ -299,7 +380,8 @@ export default function PacientesPage() {
</DropdownMenu>
</td>
</tr>
)))}
))
)}
</tbody>
</table>
)}
@ -309,7 +391,9 @@ export default function PacientesPage() {
{/* Paginação */}
{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 space-x-2 flex-wrap justify-center"> {/* Adicionado flex-wrap e justify-center para botões da paginação */}
<div className="flex space-x-2 flex-wrap justify-center">
{" "}
{/* Adicionado flex-wrap e justify-center para botões da paginação */}
<Button
onClick={() => setPage((prev) => Math.max(1, prev - 1))}
disabled={page === 1}
@ -318,7 +402,6 @@ export default function PacientesPage() {
>
&lt; Anterior
</Button>
{Array.from({ length: totalPages }, (_, index) => index + 1)
.slice(Math.max(0, page - 3), Math.min(totalPages, page + 2))
.map((pageNumber) => (
@ -327,14 +410,19 @@ export default function PacientesPage() {
onClick={() => setPage(pageNumber)}
variant={pageNumber === page ? "default" : "outline"}
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}
</Button>
))}
<Button
onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
onClick={() =>
setPage((prev) => Math.min(totalPages, prev + 1))
}
disabled={page === totalPages}
variant="outline"
size="lg"
@ -350,18 +438,29 @@ export default function PacientesPage() {
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
<AlertDialogDescription>Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita.</AlertDialogDescription>
<AlertDialogDescription>
Tem certeza que deseja excluir este paciente? Esta ação não pode
ser desfeita.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={() => patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700">
<AlertDialogAction
onClick={() =>
patientToDelete && handleDeletePatient(patientToDelete)
}
className="bg-red-600 hover:bg-red-700"
>
Excluir
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<AlertDialog
open={detailsDialogOpen}
onOpenChange={setDetailsDialogOpen}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle>

View File

@ -20,7 +20,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Calendar as CalendarShadcn } from "@/components/ui/calendar";
import { format, addDays } from "date-fns";
import { User, StickyNote, Check, ChevronsUpDown } from "lucide-react";
import { smsService } from "@/services/Sms.mjs";;
import { smsService } from "@/services/Sms.mjs";
import { toast } from "@/hooks/use-toast";
import { cn } from "@/lib/utils";
@ -277,7 +277,10 @@ export default function ScheduleForm() {
const patientId = isSecretaryLike ? selectedPatient : userId;
if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) {
toast({ title: "Campos obrigatórios", description: "Preencha todos os campos." });
toast({
title: "Campos obrigatórios",
description: "Preencha todos os campos.",
});
return;
}
@ -313,8 +316,12 @@ export default function ScheduleForm() {
const me = await usersService.getMe();
const rawPhone =
me?.profile?.phone ||
(typeof me?.profile === "object" && "phone_mobile" in me.profile ? (me.profile as any).phone_mobile : null) ||
(typeof me === "object" && "user_metadata" in me ? (me as any).user_metadata?.phone : null) ||
(typeof me?.profile === "object" && "phone_mobile" in me.profile
? (me.profile as any).phone_mobile
: null) ||
(typeof me === "object" && "user_metadata" in me
? (me as any).user_metadata?.phone
: null) ||
null;
if (rawPhone) phoneNumber = rawPhone;
}
@ -399,14 +406,23 @@ export default function ScheduleForm() {
<CardTitle>Dados da Consulta</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-4"> {/* Ajuste: maior espaçamento vertical geral */}
<form
onSubmit={handleSubmit}
className="grid grid-cols-1 lg:grid-cols-2 gap-6"
>
<div className="space-y-4">
{" "}
{/* Ajuste: maior espaçamento vertical geral */}
{/* Se secretária/gestor/admin → COMBOBOX de Paciente */}
{["secretaria", "gestor", "admin"].includes(role) && (
<div className="flex flex-col gap-2"> {/* Ajuste: gap entre Label e Input */}
<div className="flex flex-col gap-2">
{" "}
{/* Ajuste: gap entre Label e Input */}
<Label>Paciente</Label>
<Popover open={openPatientCombobox} onOpenChange={setOpenPatientCombobox}>
<Popover
open={openPatientCombobox}
onOpenChange={setOpenPatientCombobox}
>
<PopoverTrigger asChild>
<Button
variant="outline"
@ -415,7 +431,8 @@ export default function ScheduleForm() {
className="w-full justify-between"
>
{selectedPatient
? patients.find((p) => p.id === selectedPatient)?.full_name
? patients.find((p) => p.id === selectedPatient)
?.full_name
: "Selecione o paciente..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@ -424,21 +441,29 @@ export default function ScheduleForm() {
<Command>
<CommandInput placeholder="Buscar paciente..." />
<CommandList>
<CommandEmpty>Nenhum paciente encontrado.</CommandEmpty>
<CommandEmpty>
Nenhum paciente encontrado.
</CommandEmpty>
<CommandGroup>
{patients.map((patient) => (
<CommandItem
key={patient.id}
value={patient.full_name}
onSelect={() => {
setSelectedPatient(patient.id === selectedPatient ? "" : patient.id);
setSelectedPatient(
patient.id === selectedPatient
? ""
: patient.id
);
setOpenPatientCombobox(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedPatient === patient.id ? "opacity-100" : "opacity-0"
selectedPatient === patient.id
? "opacity-100"
: "opacity-0"
)}
/>
{patient.full_name}
@ -451,11 +476,15 @@ export default function ScheduleForm() {
</Popover>
</div>
)}
{/* COMBOBOX de Médico (Nova funcionalidade) */}
<div className="flex flex-col gap-2"> {/* Ajuste: gap entre Label e Input */}
<div className="flex flex-col gap-2">
{" "}
{/* Ajuste: gap entre Label e Input */}
<Label>Médico</Label>
<Popover open={openDoctorCombobox} onOpenChange={setOpenDoctorCombobox}>
<Popover
open={openDoctorCombobox}
onOpenChange={setOpenDoctorCombobox}
>
<PopoverTrigger asChild>
<Button
variant="outline"
@ -467,7 +496,8 @@ export default function ScheduleForm() {
{loadingDoctors
? "Carregando médicos..."
: selectedDoctor
? doctors.find((d) => d.id === selectedDoctor)?.full_name
? doctors.find((d) => d.id === selectedDoctor)
?.full_name
: "Selecione o médico..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@ -478,24 +508,38 @@ export default function ScheduleForm() {
<CommandList>
<CommandEmpty>Nenhum médico encontrado.</CommandEmpty>
<CommandGroup>
{doctors.map((doctor) => (
{[...doctors]
.sort((a, b) =>
String(a.full_name).localeCompare(
String(b.full_name)
)
)
.map((doctor) => (
<CommandItem
key={doctor.id}
value={doctor.full_name}
onSelect={() => {
setSelectedDoctor(doctor.id === selectedDoctor ? "" : doctor.id);
setSelectedDoctor(
doctor.id === selectedDoctor
? ""
: doctor.id
);
setOpenDoctorCombobox(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedDoctor === doctor.id ? "opacity-100" : "opacity-0"
selectedDoctor === doctor.id
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex flex-col">
<span>{doctor.full_name}</span>
<span className="text-xs text-muted-foreground">{doctor.specialty}</span>
<span className="text-xs text-muted-foreground">
{doctor.specialty}
</span>
</div>
</CommandItem>
))}
@ -505,7 +549,6 @@ export default function ScheduleForm() {
</PopoverContent>
</Popover>
</div>
<div className="flex flex-col gap-2">
<Label>Data</Label>
<div ref={calendarRef} className="rounded-lg border p-2">
@ -528,7 +571,6 @@ export default function ScheduleForm() {
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label>Observações</Label>
<Textarea
@ -540,7 +582,9 @@ export default function ScheduleForm() {
</div>
</div>
<div className="space-y-4"> {/* Ajuste: Espaçamento no lado direito também */}
<div className="space-y-4">
{" "}
{/* Ajuste: Espaçamento no lado direito também */}
<Card className="shadow-md rounded-xl bg-blue-50 border border-blue-200">
<CardHeader>
<CardTitle className="text-blue-700">Resumo</CardTitle>
@ -596,7 +640,6 @@ export default function ScheduleForm() {
)}
</CardContent>
</Card>
<div className="flex gap-2">
<Button
type="submit"