From 5704965dc5c6538009de3215e2c957c36fd16fd4 Mon Sep 17 00:00:00 2001 From: pedrosiimoes Date: Tue, 4 Nov 2025 22:43:33 -0300 Subject: [PATCH 1/7] ajustes(btn acoes gest de medicos, filtro tabela medico, rfzr data do proximo atendimento) --- app/manager/home/page.tsx | 429 ++++++++++++++++--------------- app/manager/pacientes/page.tsx | 76 +++--- app/secretary/pacientes/page.tsx | 413 +++++++++++++---------------- 3 files changed, 437 insertions(+), 481 deletions(-) diff --git a/app/manager/home/page.tsx b/app/manager/home/page.tsx index abbc858..e83e55f 100644 --- a/app/manager/home/page.tsx +++ b/app/manager/home/page.tsx @@ -1,6 +1,7 @@ "use client"; -import React, { useEffect, useState, useCallback } from "react" +// -> ADICIONADO: useMemo para otimizar a criação da lista de especialidades +import React, { useEffect, useState, useCallback, useMemo } from "react" import ManagerLayout from "@/components/manager-layout"; import Link from "next/link" import { useRouter } from "next/navigation"; @@ -23,14 +24,15 @@ import { doctorsService } from "services/doctorsApi.mjs"; interface Doctor { - id: number; - full_name: string; - specialty: string; - crm: string; - phone_mobile: string | null; - city: string | null; - state: string | null; - + id: number; + full_name: string; + specialty: string; + crm: string; + phone_mobile: string | null; + city: string | null; + state: string | null; + // -> ADICIONADO: Campo 'status' para que o filtro funcione. Sua API precisa retornar este dado. + status?: string; } @@ -38,7 +40,6 @@ interface DoctorDetails { nome: string; crm: string; especialidade: string; - contato: { celular?: string; telefone1?: string; @@ -58,7 +59,6 @@ interface DoctorDetails { export default function DoctorsPage() { const router = useRouter(); - const [doctors, setDoctors] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -66,17 +66,23 @@ export default function DoctorsPage() { const [doctorDetails, setDoctorDetails] = useState(null); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [doctorToDeleteId, setDoctorToDeleteId] = useState(null); - - - + // -> PASSO 1: Criar estados para os filtros + const [specialtyFilter, setSpecialtyFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState("all"); + + const fetchDoctors = useCallback(async () => { setLoading(true); setError(null); try { - const data: Doctor[] = await doctorsService.list(); - setDoctors(data || []); + // Exemplo: Adicionando um status fake para o filtro funcionar. O ideal é que isso venha da API. + const dataWithStatus = data.map((doc, index) => ({ + ...doc, + status: index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo" + })); + setDoctors(dataWithStatus || []); } catch (e: any) { console.error("Erro ao carregar lista de médicos:", e); setError("Não foi possível carregar a lista de médicos. Verifique a conexão com a API."); @@ -86,7 +92,7 @@ export default function DoctorsPage() { } }, []); - + useEffect(() => { fetchDoctors(); }, [fetchDoctors]); @@ -94,44 +100,31 @@ export default function DoctorsPage() { const openDetailsDialog = async (doctor: Doctor) => { setDetailsDialogOpen(true); - setDoctorDetails({ - nome: doctor.full_name, - crm: doctor.crm, - especialidade: doctor.specialty, - contato: { - celular: doctor.phone_mobile ?? undefined, - telefone1: undefined - }, - endereco: { - cidade: doctor.city ?? undefined, - estado: doctor.state ?? undefined, - }, - - convenio: "Particular", - vip: false, - status: "Ativo", - ultimo_atendimento: "N/A", - proximo_atendimento: "N/A", + nome: doctor.full_name, + crm: doctor.crm, + especialidade: doctor.specialty, + contato: { celular: doctor.phone_mobile ?? undefined }, + endereco: { cidade: doctor.city ?? undefined, estado: doctor.state ?? undefined }, + status: doctor.status || "Ativo", // Usa o status do médico + convenio: "Particular", + vip: false, + ultimo_atendimento: "N/A", + proximo_atendimento: "N/A", }); }; - + const handleDelete = async () => { if (doctorToDeleteId === null) return; - setLoading(true); try { await doctorsService.delete(doctorToDeleteId); - - console.log(`Médico com ID ${doctorToDeleteId} excluído com sucesso!`); - setDeleteDialogOpen(false); setDoctorToDeleteId(null); - await fetchDoctors(); + await fetchDoctors(); } catch (e) { console.error("Erro ao excluir:", e); - alert("Erro ao excluir médico."); } finally { setLoading(false); @@ -142,192 +135,210 @@ export default function DoctorsPage() { setDoctorToDeleteId(doctorId); setDeleteDialogOpen(true); }; - + const handleEdit = (doctorId: number) => { - router.push(`/manager/home/${doctorId}/editar`); }; + // -> MELHORIA: Gera a lista de especialidades dinamicamente + const uniqueSpecialties = useMemo(() => { + const specialties = doctors.map(doctor => doctor.specialty).filter(Boolean); + return [...new Set(specialties)]; + }, [doctors]); + + // -> PASSO 3: Aplicar a lógica de filtragem + const filteredDoctors = doctors.filter(doctor => { + const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter; + const statusMatch = statusFilter === "all" || doctor.status === statusFilter; + return specialtyMatch && statusMatch; + }); + return ( -
-
-
-

Médicos Cadastrados

-

Gerencie todos os profissionais de saúde.

-
- - - -
- - -
- - - -
- - -
- {loading ? ( -
- - Carregando médicos... -
- ) : error ? ( -
- {error} -
- ) : doctors.length === 0 ? ( -
- Nenhum médico cadastrado. Adicione um novo. -
- ) : ( -
- - - - - - - - - - - - - {doctors.map((doctor) => ( - - - - - - - - - ))} - -
NomeCRMEspecialidadeCelularCidade/EstadoAções
{doctor.full_name}{doctor.crm}{doctor.specialty}{doctor.phone_mobile || "N/A"} - {(doctor.city || doctor.state) ? `${doctor.city || ''}${doctor.city && doctor.state ? '/' : ''}${doctor.state || ''}` : "N/A"} - - -
- - - - - - - - - - - - - - - - Agendar Consulta - - - -
-
+
+
+
+

Médicos Cadastrados

+

Gerencie todos os profissionais de saúde.

- )} -
- - - - - - Confirma a exclusão? - - Esta ação é irreversível e excluirá permanentemente o registro deste médico. - - - - Cancelar - - {loading ? ( - - ) : null} - Excluir - - - - + + + +
- - - - - {doctorDetails?.nome} - - {doctorDetails && ( -
-

Informações Principais

-
+ + {/* -> PASSO 2: Conectar os estados aos componentes Select <- */} +
+ Especialidades + + Status + + +
+ + +
+ {loading ? ( +
+ + Carregando médicos... +
+ ) : error ? ( +
+ {error} +
+ // -> Atualizado para usar a lista filtrada + ) : filteredDoctors.length === 0 ? ( +
+ {doctors.length === 0 + ? <>Nenhum médico cadastrado. Adicione um novo. + : "Nenhum médico encontrado com os filtros aplicados." + } +
+ ) : ( +
+ + + + + + + + + + + + + + {/* -> ATUALIZADO para mapear a lista filtrada */} + {filteredDoctors.map((doctor) => ( + + + + + {/* Coluna de Status adicionada para visualização */} + + + + + + + ))} + +
NomeCRMEspecialidadeStatusCelularCidade/EstadoAções
{doctor.full_name}{doctor.crm}{doctor.specialty}{doctor.status}{doctor.phone_mobile || "N/A"} + {(doctor.city || doctor.state) ? `${doctor.city || ''}${doctor.city && doctor.state ? '/' : ''}${doctor.state || ''}` : "N/A"} + + + +
Ações
+
+ + openDetailsDialog(doctor)}> + + Ver detalhes + + handleEdit(doctor.id)}> + + Editar + + openDeleteDialog(doctor.id)} + > + + Excluir + + +
+
+
+ )} +
+ + {/* ... O resto do seu código (AlertDialogs) permanece o mesmo ... */} + + + + Confirma a exclusão? + + Esta ação é irreversível e excluirá permanentemente o registro deste médico. + + + + Cancelar + + {loading ? : null} + Excluir + + + + + + + + + {doctorDetails?.nome} + + {doctorDetails && ( +
+

Informações Principais

+
CRM: {doctorDetails.crm}
Especialidade: {doctorDetails.especialidade}
Celular: {doctorDetails.contato.celular || 'N/A'}
Localização: {`${doctorDetails.endereco.cidade || 'N/A'}/${doctorDetails.endereco.estado || 'N/A'}`}
-
- -

Atendimento e Convênio

-
+
+ +

Atendimento e Convênio

+
Convênio: {doctorDetails.convenio || 'N/A'}
VIP: {doctorDetails.vip ? "Sim" : "Não"}
Status: {doctorDetails.status || 'N/A'}
Último atendimento: {doctorDetails.ultimo_atendimento || 'N/A'}
Próximo atendimento: {doctorDetails.proximo_atendimento || 'N/A'}
+
-
- )} - {doctorDetails === null && !loading && ( -
Detalhes não disponíveis.
- )} - - - - Fechar - - - -
+ )} + {doctorDetails === null && !loading && ( +
Detalhes não disponíveis.
+ )} +
+
+ + Fechar + +
+
+ +
); -} +} \ No newline at end of file diff --git a/app/manager/pacientes/page.tsx b/app/manager/pacientes/page.tsx index 7ad7704..f0e86fd 100644 --- a/app/manager/pacientes/page.tsx +++ b/app/manager/pacientes/page.tsx @@ -10,6 +10,36 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, import ManagerLayout from "@/components/manager-layout"; import { patientsService } from "@/services/patientsApi.mjs"; +// --- INÍCIO DA MODIFICAÇÃO --- +// PASSO 1: Criar uma função para formatar a data +const formatDate = (dateString: string | null | undefined): string => { + // Se a data não existir, retorna um texto padrão + if (!dateString) { + return "N/A"; + } + + try { + const date = new Date(dateString); + // Verifica se a data é válida após a conversão + if (isNaN(date.getTime())) { + return "Data inválida"; + } + + 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 year = date.getFullYear(); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + + return `${day}/${month}/${year} ${hours}:${minutes}`; + } catch (error) { + // Se houver qualquer erro na conversão, retorna um texto de erro + return "Data inválida"; + } +}; +// --- FIM DA MODIFICAÇÃO --- + + export default function PacientesPage() { const [searchTerm, setSearchTerm] = useState(""); const [convenioFilter, setConvenioFilter] = useState("all"); @@ -183,34 +213,6 @@ export default function PacientesPage() {
-
- VIP - -
- -
- Aniversariantes - -
-
); -} +} \ No newline at end of file diff --git a/app/secretary/pacientes/page.tsx b/app/secretary/pacientes/page.tsx index ca02124..624e2aa 100644 --- a/app/secretary/pacientes/page.tsx +++ b/app/secretary/pacientes/page.tsx @@ -30,11 +30,67 @@ import { import SecretaryLayout from "@/components/secretary-layout"; import { patientsService } from "@/services/patientsApi.mjs"; +// --- INÍCIO DA CORREÇÃO --- +interface Patient { + id: string; + nome: string; + telefone: string; + cidade: string; + estado: string; + ultimoAtendimento: string; + proximoAtendimento: string; + vip: boolean; + convenio: string; + status?: string; + // Propriedades detalhadas para o modal + full_name?: string; + cpf?: string; + email?: string; + phone_mobile?: string; + phone1?: string; + phone2?: string; + social_name?: string; + sex?: string; + blood_type?: string; + weight_kg?: number; + height_m?: number; + bmi?: number; + street?: string; + neighborhood?: string; + city?: string; // <-- Adicionado + state?: string; // <-- Adicionado + cep?: string; + created_at?: string; + updated_at?: string; +} +// --- FIM DA CORREÇÃO --- + +// Função para formatar a data +const formatDate = (dateString: string | null | undefined): string => { + if (!dateString) { + return "N/A"; + } + try { + const date = new Date(dateString); + if (isNaN(date.getTime())) { + return "Data inválida"; + } + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = date.getFullYear(); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${day}/${month}/${year} ${hours}:${minutes}`; + } catch (error) { + return "Data inválida"; + } +}; + export default function PacientesPage() { const [searchTerm, setSearchTerm] = useState(""); const [convenioFilter, setConvenioFilter] = useState("all"); const [vipFilter, setVipFilter] = useState("all"); - const [patients, setPatients] = useState([]); + const [patients, setPatients] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [page, setPage] = useState(1); @@ -44,7 +100,8 @@ export default function PacientesPage() { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [patientToDelete, setPatientToDelete] = useState(null); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [patientDetails, setPatientDetails] = useState(null); + const [patientDetails, setPatientDetails] = useState(null); + const openDetailsDialog = async (patientId: string) => { setDetailsDialogOpen(true); setPatientDetails(null); @@ -63,7 +120,7 @@ export default function PacientesPage() { setError(null); try { const res = await patientsService.list(); - const mapped = res.map((p: any) => ({ + const mapped: Patient[] = res.map((p: any) => ({ id: String(p.id ?? ""), nome: p.full_name ?? "", telefone: p.phone_mobile ?? p.phone1 ?? "", @@ -72,20 +129,22 @@ export default function PacientesPage() { ultimoAtendimento: p.last_visit_at ?? "", proximoAtendimento: p.next_appointment_at ?? "", vip: Boolean(p.vip ?? false), - convenio: p.convenio ?? "", // se não existir, fica vazio + convenio: p.convenio ?? "", status: p.status ?? undefined, })); setPatients((prev) => { const all = [...prev, ...mapped]; - const unique = Array.from( - new Map(all.map((p) => [p.id, p])).values() - ); + 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); + if (mapped.length === 0) { + setHasNext(false); + } else { + setPage((prev) => prev + 1); + } + } catch (e: any) { setError(e?.message || "Erro ao buscar pacientes"); } finally { @@ -96,7 +155,7 @@ export default function PacientesPage() { ); useEffect(() => { - fetchPacientes(page); + fetchPacientes(1); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -109,24 +168,19 @@ export default function PacientesPage() { }); observer.observe(observerRef.current); return () => { - if (observerRef.current) observer.unobserve(observerRef.current); + if (observerRef.current) { + observer.unobserve(observerRef.current); + } }; }, [fetchPacientes, page, hasNext, isFetching]); const handleDeletePatient = async (patientId: string) => { - // Remove from current list (client-side deletion) try { - const res = await patientsService.delete(patientId); - - if (res) { - alert(`${res.error} ${res.message}`); - } - - setPatients((prev) => - prev.filter((p) => String(p.id) !== String(patientId)) - ); + await patientsService.delete(patientId); + setPatients((prev) => prev.filter((p) => p.id !== patientId)); } catch (e: any) { setError(e?.message || "Erro ao deletar paciente"); + alert("Erro ao deletar paciente."); } setDeleteDialogOpen(false); setPatientToDelete(null); @@ -156,12 +210,8 @@ export default function PacientesPage() {
-

- Pacientes -

-

- Gerencie as informações de seus pacientes -

+

Pacientes

+

Gerencie as informações de seus pacientes

@@ -174,11 +224,8 @@ export default function PacientesPage() {
- {/* Convênio */}
- - Convênio - + Convênio
-
VIP
- - Aniversariantes - + Aniversariantes
- - -
- -
- - {error && ( -
-

Erro no Cadastro:

-

{error}

-
- )} - - -
-

- Dados Principais e Pessoais -

- - -
-
- - handleInputChange("nomeCompleto", e.target.value)} - placeholder="Nome do Médico" - required - /> -
-
- - handleInputChange("crm", e.target.value)} - placeholder="Ex: 123456" - required - /> -
-
- - -
-
- - -
-
- - handleInputChange("especialidade", e.target.value)} - placeholder="Ex: Cardiologia" - /> -
-
- - handleInputChange("cpf", e.target.value)} - placeholder="000.000.000-00" - maxLength={14} - required - /> -
-
- - handleInputChange("rg", e.target.value)} - placeholder="00.000.000-0" - /> -
-
- - -
-
- - handleInputChange("email", e.target.value)} - placeholder="exemplo@dominio.com" - required - /> -
-
- - handleInputChange("dataNascimento", e.target.value)} - /> -
-
-
- - -
-

- Contato e Endereço -

- - -
-
- - handleInputChange("telefoneCelular", e.target.value)} - placeholder="(00) 00000-0000" - maxLength={15} - /> -
-
- - handleInputChange("telefone2", e.target.value)} - placeholder="(00) 00000-0000" - maxLength={15} - /> -
-
-
- handleInputChange("ativo", checked === true)} - /> - -
-
-
- - -
-
- - handleInputChange("cep", e.target.value)} - placeholder="00000-000" - maxLength={9} - /> -
-
- - handleInputChange("endereco", e.target.value)} - placeholder="Rua, Avenida, etc." - /> -
-
-
-
- - handleInputChange("numero", e.target.value)} - placeholder="123" - /> -
-
- - handleInputChange("complemento", e.target.value)} - placeholder="Apto, Bloco, etc." - /> -
-
-
-
- - handleInputChange("bairro", e.target.value)} - placeholder="Bairro" - /> -
-
- - handleInputChange("estado", e.target.value)} - placeholder="SP" - /> -
-
- - handleInputChange("cidade", e.target.value)} - placeholder="São Paulo" - /> -
-
-
- - -
-

- Outras Informações (Internas) -

- -
-
- -