diff --git a/app/manager/pacientes/page.tsx b/app/manager/pacientes/page.tsx index e6886f3..e2938ee 100644 --- a/app/manager/pacientes/page.tsx +++ b/app/manager/pacientes/page.tsx @@ -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"; @@ -14,59 +43,60 @@ import Sidebar from "@/components/Sidebar"; const PAGE_SIZE = 5; export default function PacientesPage() { - // --- ESTADOS DE DADOS E GERAL --- - const [searchTerm, setSearchTerm] = useState(""); - const [convenioFilter, setConvenioFilter] = useState("all"); - const [vipFilter, setVipFilter] = useState("all"); + // --- ESTADOS DE DADOS E GERAL --- + const [searchTerm, setSearchTerm] = useState(""); + const [convenioFilter, setConvenioFilter] = useState("all"); + const [vipFilter, setVipFilter] = useState("all"); - // Lista completa, carregada da API uma única vez - const [allPatients, setAllPatients] = useState([]); - // Lista após a aplicação dos filtros (base para a paginação) - const [filteredPatients, setFilteredPatients] = useState([]); + // Lista completa, carregada da API uma única vez + const [allPatients, setAllPatients] = useState([]); + // Lista após a aplicação dos filtros (base para a paginação) + const [filteredPatients, setFilteredPatients] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - // --- ESTADOS DE PAGINAÇÃO --- - const [page, setPage] = useState(1); + // --- ESTADOS DE PAGINAÇÃO --- + const [page, setPage] = useState(1); - // CÁLCULO DA PAGINAÇÃO - const totalPages = Math.ceil(filteredPatients.length / PAGE_SIZE); - const startIndex = (page - 1) * PAGE_SIZE; - const endIndex = startIndex + PAGE_SIZE; - // Pacientes a serem exibidos na tabela (aplicando a paginação) - const currentPatients = filteredPatients.slice(startIndex, endIndex); + // CÁLCULO DA PAGINAÇÃO + const totalPages = Math.ceil(filteredPatients.length / PAGE_SIZE); + const startIndex = (page - 1) * PAGE_SIZE; + const endIndex = startIndex + PAGE_SIZE; + // Pacientes a serem exibidos na tabela (aplicando a paginação) + const currentPatients = filteredPatients.slice(startIndex, endIndex); - // --- ESTADOS DE DIALOGS --- - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [patientToDelete, setPatientToDelete] = useState(null); - const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [patientDetails, setPatientDetails] = useState(null); + // --- ESTADOS DE DIALOGS --- + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [patientToDelete, setPatientToDelete] = useState(null); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [patientDetails, setPatientDetails] = useState(null); - // --- FUNÇÕES DE LÓGICA --- + // --- FUNÇÕES DE LÓGICA --- - // 1. Função para carregar TODOS os pacientes da API - const fetchAllPacientes = useCallback( - async () => { - setLoading(true); - setError(null); - try { - // Como o backend retorna um array, chamamos sem paginação - const res = await patientsService.list(); + // 1. Função para carregar TODOS os pacientes da API + const fetchAllPacientes = useCallback(async () => { + setLoading(true); + setError(null); + try { + // Como o backend retorna um array, chamamos sem paginação + const res = await patientsService.list(); - const mapped = res.map((p: any) => ({ - id: String(p.id ?? ""), - nome: p.full_name ?? "—", - telefone: p.phone_mobile ?? p.phone1 ?? "—", - 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('-') : "—", - vip: Boolean(p.vip ?? false), - convenio: p.convenio ?? "Particular", // Define um valor padrão - status: p.status ?? undefined, - })); + const mapped = res.map((p: any) => ({ + id: String(p.id ?? ""), + nome: p.full_name ?? "—", + telefone: p.phone_mobile ?? p.phone1 ?? "—", + 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("-") + : "—", + vip: Boolean(p.vip ?? false), + convenio: p.convenio ?? "Particular", // Define um valor padrão + status: p.status ?? undefined, + })); setAllPatients(mapped); } catch (e: any) { @@ -77,32 +107,31 @@ export default function PacientesPage() { } }, []); - // 2. Efeito para aplicar filtros e calcular a lista filtrada (chama-se quando allPatients ou filtros mudam) - useEffect(() => { - const filtered = allPatients.filter((patient) => { - // Filtro por termo de busca (Nome ou Telefone) - const matchesSearch = - patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) || - patient.telefone?.includes(searchTerm); + // 2. Efeito para aplicar filtros e calcular a lista filtrada (chama-se quando allPatients ou filtros mudam) + useEffect(() => { + const filtered = allPatients.filter((patient) => { + // Filtro por termo de busca (Nome ou Telefone) + const matchesSearch = + patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) || + patient.telefone?.includes(searchTerm); - // Filtro por Convênio - const matchesConvenio = - convenioFilter === "all" || - patient.convenio === convenioFilter; + // Filtro por Convênio + const matchesConvenio = + convenioFilter === "all" || patient.convenio === convenioFilter; - // Filtro por VIP - const matchesVip = - vipFilter === "all" || - (vipFilter === "vip" && patient.vip) || - (vipFilter === "regular" && !patient.vip); + // Filtro por VIP + const matchesVip = + vipFilter === "all" || + (vipFilter === "vip" && patient.vip) || + (vipFilter === "regular" && !patient.vip); - return matchesSearch && matchesConvenio && matchesVip; - }); + return matchesSearch && matchesConvenio && matchesVip; + }); - setFilteredPatients(filtered); - // Garante que a página atual seja válida após a filtragem - setPage(1); - }, [allPatients, searchTerm, convenioFilter, vipFilter]); + setFilteredPatients(filtered); + // Garante que a página atual seja válida após a filtragem + setPage(1); + }, [allPatients, searchTerm, convenioFilter, vipFilter]); // 3. Efeito inicial para buscar os pacientes useEffect(() => { @@ -110,18 +139,18 @@ export default function PacientesPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // --- LÓGICA DE AÇÕES (DELETAR / VER DETALHES) --- + // --- LÓGICA DE AÇÕES (DELETAR / VER DETALHES) --- - const openDetailsDialog = async (patientId: string) => { - setDetailsDialogOpen(true); - setPatientDetails(null); - try { - const res = await patientsService.getById(patientId); - setPatientDetails(Array.isArray(res) ? res[0] : res); // Supondo que retorne um array com um item - } catch (e: any) { - setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" }); - } - }; + const openDetailsDialog = async (patientId: string) => { + setDetailsDialogOpen(true); + setPatientDetails(null); + try { + const res = await patientsService.getById(patientId); + setPatientDetails(Array.isArray(res) ? res[0] : res); // Supondo que retorne um array com um item + } catch (e: any) { + setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" }); + } + }; const handleDeletePatient = async (patientId: string) => { try { @@ -157,20 +186,20 @@ export default function PacientesPage() { - {/* Bloco de Filtros (Responsividade APLICADA) */} - {/* Adicionado flex-wrap para permitir que os itens quebrem para a linha de baixo */} -
- + {/* Bloco de Filtros (Responsividade APLICADA) */} + {/* Adicionado flex-wrap para permitir que os itens quebrem para a linha de baixo */} +
+ - {/* Busca - Ocupa 100% no mobile, depois cresce */} - 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" - /> + {/* Busca - Ocupa 100% no mobile, depois cresce */} + 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" + /> {/* Convênio - Ocupa a largura total em telas pequenas, depois se ajusta */}
@@ -193,261 +222,331 @@ export default function PacientesPage() {
- {/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */} -
- VIP - -
+ {/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */} +
+ + VIP + + +
- {/* Aniversariantes - Ocupa 100% no mobile, e se alinha à direita no md+ */} - -
+ {/* Aniversariantes - Ocupa 100% no mobile, e se alinha à direita no md+ */} + +
- {/* --- 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+ */} -
-
{/* Permite rolagem horizontal se a tabela for muito larga */} - {error ? ( -
{`Erro ao carregar pacientes: ${error}`}
- ) : loading ? ( -
- Carregando pacientes... + {/* --- 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+ */} +
+
+ {" "} + {/* Permite rolagem horizontal se a tabela for muito larga */} + {error ? ( +
{`Erro ao carregar pacientes: ${error}`}
+ ) : loading ? ( +
+ {" "} + Carregando pacientes... +
+ ) : ( + + {" "} + {/* min-w para evitar que a tabela se contraia demais */} + + + + {/* Ajustes de visibilidade de colunas para diferentes breakpoints */} + + + + + + + + + + {currentPatients.length === 0 ? ( + + + + ) : ( + currentPatients.map((patient) => ( + + + + )) + )} + +
+ Nome + + Telefone + + Cidade / Estado + + Convênio + + Último atendimento + + Próximo atendimento + + Ações +
+ {allPatients.length === 0 + ? "Nenhum paciente cadastrado" + : "Nenhum paciente encontrado com os filtros aplicados"} +
+
+
+ + {patient.nome?.charAt(0) || "?"} +
- ) : ( - {/* min-w para evitar que a tabela se contraia demais */} - - - - {/* Ajustes de visibilidade de colunas para diferentes breakpoints */} - - - - - - - - - - {currentPatients.length === 0 ? ( - - - - ) : ( - currentPatients.map((patient) => ( - - - - - - - + + {patient.nome} + {patient.vip && ( + + VIP + + )} + + + + + + + + - - - )))} - -
NomeTelefoneCidade / EstadoConvênioÚltimo atendimentoPróximo atendimentoAções
- {allPatients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"} -
-
-
- {patient.nome?.charAt(0) || "?"} -
- - {patient.nome} - {patient.vip && ( - VIP - )} - -
-
{patient.telefone}{`${patient.cidade} / ${patient.estado}`}{patient.convenio}{patient.ultimoAtendimento}{patient.proximoAtendimento} + {patient.telefone} + {`${patient.cidade} / ${patient.estado}`} + {patient.convenio} + + {patient.ultimoAtendimento} + + {patient.proximoAtendimento} + - - -
- -
-
- - openDetailsDialog(String(patient.id))}> - - Ver detalhes - - - - - Editar - - +
+ + +
+ +
+
+ + + openDetailsDialog(String(patient.id)) + } + > + + Ver detalhes + + + + + Editar + + - - - Marcar consulta - - openDeleteDialog(String(patient.id))}> - - Excluir - - -
-
- )} -
- - - {/* Paginação */} - {totalPages > 1 && !loading && ( -
-
{/* Adicionado flex-wrap e justify-center para botões da paginação */} - - - {Array.from({ length: totalPages }, (_, index) => index + 1) - .slice(Math.max(0, page - 3), Math.min(totalPages, page + 2)) - .map((pageNumber) => ( - - ))} - - -
-
- )} - - {/* AlertDialogs (Permanecem os mesmos) */} - - - - Confirmar exclusão - Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita. - - - Cancelar - patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700"> + + + Marcar consulta + + + openDeleteDialog(String(patient.id)) + } + > + Excluir - - - - + + + +
+ )} +
+
- - - - Detalhes do Paciente - - {patientDetails === null ? ( -
- - Carregando... -
- ) : patientDetails?.error ? ( -
{patientDetails.error}
- ) : ( -
-
-
-

Nome Completo

-

{patientDetails.full_name}

-
-
-

Email

-

{patientDetails.email}

-
-
-

Telefone

-

{patientDetails.phone_mobile}

-
-
-

Data de Nascimento

-

{patientDetails.birth_date}

-
-
-

CPF

-

{patientDetails.cpf}

-
-
-

Tipo Sanguíneo

-

{patientDetails.blood_type}

-
-
-

Peso (kg)

-

{patientDetails.weight_kg}

-
-
-

Altura (m)

-

{patientDetails.height_m}

-
-
-
-

Endereço

-
-
-

Rua

-

{`${patientDetails.street}, ${patientDetails.number}`}

-
-
-

Complemento

-

{patientDetails.complement}

-
-
-

Bairro

-

{patientDetails.neighborhood}

-
-
-

Cidade

-

{patientDetails.cidade}

-
-
-

Estado

-

{patientDetails.estado}

-
-
-

CEP

-

{patientDetails.cep}

-
-
-
-
- )} -
-
- - Fechar - -
-
+ {/* Paginação */} + {totalPages > 1 && !loading && ( +
+
+ {" "} + {/* Adicionado flex-wrap e justify-center para botões da paginação */} + + {Array.from({ length: totalPages }, (_, index) => index + 1) + .slice(Math.max(0, page - 3), Math.min(totalPages, page + 2)) + .map((pageNumber) => ( + + ))} +
- - ); +
+ )} + + {/* AlertDialogs (Permanecem os mesmos) */} + + + + Confirmar exclusão + + Tem certeza que deseja excluir este paciente? Esta ação não pode + ser desfeita. + + + + Cancelar + + patientToDelete && handleDeletePatient(patientToDelete) + } + className="bg-red-600 hover:bg-red-700" + > + Excluir + + + + + + + + + Detalhes do Paciente + + {patientDetails === null ? ( +
+ + Carregando... +
+ ) : patientDetails?.error ? ( +
{patientDetails.error}
+ ) : ( +
+
+
+

Nome Completo

+

{patientDetails.full_name}

+
+
+

Email

+

{patientDetails.email}

+
+
+

Telefone

+

{patientDetails.phone_mobile}

+
+
+

Data de Nascimento

+

{patientDetails.birth_date}

+
+
+

CPF

+

{patientDetails.cpf}

+
+
+

Tipo Sanguíneo

+

{patientDetails.blood_type}

+
+
+

Peso (kg)

+

{patientDetails.weight_kg}

+
+
+

Altura (m)

+

{patientDetails.height_m}

+
+
+
+

Endereço

+
+
+

Rua

+

{`${patientDetails.street}, ${patientDetails.number}`}

+
+
+

Complemento

+

{patientDetails.complement}

+
+
+

Bairro

+

{patientDetails.neighborhood}

+
+
+

Cidade

+

{patientDetails.cidade}

+
+
+

Estado

+

{patientDetails.estado}

+
+
+

CEP

+

{patientDetails.cep}

+
+
+
+
+ )} +
+
+ + Fechar + +
+
+
+ + ); } diff --git a/components/schedule/schedule-form.tsx b/components/schedule/schedule-form.tsx index 8d0a0f4..a498ccc 100644 --- a/components/schedule/schedule-form.tsx +++ b/components/schedule/schedule-form.tsx @@ -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; } @@ -302,7 +305,7 @@ export default function ScheduleForm() { }.`, }); - let phoneNumber = "+5511999999999"; + let phoneNumber = "+5511999999999"; try { if (isSecretaryLike) { @@ -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() { Dados da Consulta -
-
{/* Ajuste: maior espaçamento vertical geral */} - + +
+ {" "} + {/* Ajuste: maior espaçamento vertical geral */} {/* Se secretária/gestor/admin → COMBOBOX de Paciente */} {["secretaria", "gestor", "admin"].includes(role) && ( -
{/* Ajuste: gap entre Label e Input */} +
+ {" "} + {/* Ajuste: gap entre Label e Input */} - + @@ -424,21 +441,29 @@ export default function ScheduleForm() { - Nenhum paciente encontrado. + + Nenhum paciente encontrado. + {patients.map((patient) => ( { - setSelectedPatient(patient.id === selectedPatient ? "" : patient.id); + setSelectedPatient( + patient.id === selectedPatient + ? "" + : patient.id + ); setOpenPatientCombobox(false); }} > {patient.full_name} @@ -451,11 +476,15 @@ export default function ScheduleForm() {
)} - {/* COMBOBOX de Médico (Nova funcionalidade) */} -
{/* Ajuste: gap entre Label e Input */} +
+ {" "} + {/* Ajuste: gap entre Label e Input */} - + @@ -478,34 +508,47 @@ export default function ScheduleForm() { Nenhum médico encontrado. - {doctors.map((doctor) => ( - { - setSelectedDoctor(doctor.id === selectedDoctor ? "" : doctor.id); - setOpenDoctorCombobox(false); - }} - > - -
- {doctor.full_name} - {doctor.specialty} -
-
- ))} + {[...doctors] + .sort((a, b) => + String(a.full_name).localeCompare( + String(b.full_name) + ) + ) + .map((doctor) => ( + { + setSelectedDoctor( + doctor.id === selectedDoctor + ? "" + : doctor.id + ); + setOpenDoctorCombobox(false); + }} + > + +
+ {doctor.full_name} + + {doctor.specialty} + +
+
+ ))}
-
@@ -528,7 +571,6 @@ export default function ScheduleForm() { />
-