import { useCallback, useEffect, useMemo, useState, type ChangeEvent, type FormEvent, } from "react"; import { useNavigate } from "react-router-dom"; import toast from "react-hot-toast"; import { useAuth } from "../hooks/useAuth"; import { Activity, Calendar, Edit, FileText, Plus, Search, UserPlus, Users, X, } from "lucide-react"; import PatientListTable, { type PatientListItem, } from "../components/pacientes/PatientListTable"; import PacienteForm from "../components/pacientes/PacienteForm"; import { appointmentService, type Appointment, patientService, type Patient, type CreatePatientInput, type UpdatePatientInput, doctorService, type Doctor, type CreateDoctorInput, type UpdateDoctorInput, type CrmUF, reportService, type Report, type CreateReportInput, type UpdateReportInput, } from "../services"; // Type aliases para compatibilidade com código legado type Medico = Doctor; type Relatorio = Report; type PacienteServiceModel = Patient; type EnderecoPaciente = { rua: string; numero: string; complemento: string; bairro: string; cidade: string; estado: string; cep: string; }; type MedicoUpdate = UpdateDoctorInput; // Mock de ViaCEP const buscarEnderecoViaCEP = async (cep: string) => { try { const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`); const data = await response.json(); if (data.erro) return null; return { rua: data.logradouro, bairro: data.bairro, cidade: data.localidade, estado: data.uf, cep: data.cep, }; } catch { return null; } }; import ScheduleAppointmentModal from "../components/agenda/ScheduleAppointmentModal"; import ConsultasSection from "../components/secretaria/ConsultasSection"; import RelatoriosSection from "../components/secretaria/RelatoriosSection"; import AgendaSection from "../components/secretaria/AgendaSection"; // Tipos e constantes reinseridos após refatoração type TabId = | "dashboard" | "pacientes" | "medicos" | "consultas" | "agenda" | "relatorios"; const BLOOD_TYPES = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"]; const COUNTRY_OPTIONS = [ { value: "55", label: "+55 🇧🇷 Brasil" }, { value: "1", label: "+1 🇺🇸 EUA/Canadá" }, { value: "93", label: "+93 🇦🇫 Afeganistão" }, { value: "355", label: "+355 🇦🇱 Albânia" }, { value: "49", label: "+49 🇩🇪 Alemanha" }, { value: "376", label: "+376 🇦🇩 Andorra" }, { value: "244", label: "+244 🇦🇴 Angola" }, { value: "54", label: "+54 🇦🇷 Argentina" }, { value: "374", label: "+374 🇦🇲 Armênia" }, { value: "61", label: "+61 🇦🇺 Austrália" }, { value: "43", label: "+43 🇦🇹 Áustria" }, { value: "994", label: "+994 🇦🇿 Azerbaijão" }, { value: "973", label: "+973 🇧🇭 Bahrein" }, { value: "880", label: "+880 🇧🇩 Bangladesh" }, { value: "375", label: "+375 🇧🇾 Bielorrússia" }, { value: "32", label: "+32 🇧🇪 Bélgica" }, { value: "501", label: "+501 🇧🇿 Belize" }, { value: "591", label: "+591 🇧🇴 Bolívia" }, { value: "387", label: "+387 🇧🇦 Bósnia" }, { value: "267", label: "+267 🇧🇼 Botsuana" }, { value: "359", label: "+359 🇧🇬 Bulgária" }, { value: "226", label: "+226 🇧🇫 Burkina Faso" }, { value: "257", label: "+257 🇧🇮 Burundi" }, { value: "238", label: "+238 🇨🇻 Cabo Verde" }, { value: "855", label: "+855 🇰🇭 Camboja" }, { value: "237", label: "+237 🇨🇲 Camarões" }, { value: "56", label: "+56 🇨🇱 Chile" }, { value: "86", label: "+86 🇨🇳 China" }, { value: "57", label: "+57 🇨🇴 Colômbia" }, { value: "242", label: "+242 🇨🇬 Congo" }, { value: "82", label: "+82 🇰🇷 Coreia do Sul" }, { value: "850", label: "+850 🇰🇵 Coreia do Norte" }, { value: "506", label: "+506 🇨🇷 Costa Rica" }, { value: "225", label: "+225 🇨🇮 Costa do Marfim" }, { value: "385", label: "+385 🇭🇷 Croácia" }, { value: "53", label: "+53 🇨🇺 Cuba" }, { value: "45", label: "+45 🇩🇰 Dinamarca" }, { value: "593", label: "+593 🇪🇨 Equador" }, { value: "20", label: "+20 🇪🇬 Egito" }, { value: "503", label: "+503 🇸🇻 El Salvador" }, { value: "971", label: "+971 🇦🇪 Emirados Árabes" }, { value: "593", label: "+593 🇪🇨 Equador" }, { value: "291", label: "+291 🇪🇷 Eritreia" }, { value: "421", label: "+421 🇸🇰 Eslováquia" }, { value: "386", label: "+386 🇸🇮 Eslovênia" }, { value: "34", label: "+34 🇪🇸 Espanha" }, { value: "251", label: "+251 🇪🇹 Etiópia" }, { value: "679", label: "+679 🇫🇯 Fiji" }, { value: "63", label: "+63 🇵🇭 Filipinas" }, { value: "358", label: "+358 🇫🇮 Finlândia" }, { value: "33", label: "+33 🇫🇷 França" }, { value: "241", label: "+241 🇬🇦 Gabão" }, { value: "220", label: "+220 🇬🇲 Gâmbia" }, { value: "233", label: "+233 🇬🇭 Gana" }, { value: "995", label: "+995 🇬🇪 Geórgia" }, { value: "350", label: "+350 🇬🇮 Gibraltar" }, { value: "30", label: "+30 🇬🇷 Grécia" }, { value: "502", label: "+502 🇬🇹 Guatemala" }, { value: "224", label: "+224 🇬🇳 Guiné" }, { value: "245", label: "+245 🇬🇼 Guiné-Bissau" }, { value: "509", label: "+509 🇭🇹 Haiti" }, { value: "504", label: "+504 🇭🇳 Honduras" }, { value: "852", label: "+852 🇭🇰 Hong Kong" }, { value: "36", label: "+36 🇭🇺 Hungria" }, { value: "967", label: "+967 🇾🇪 Iêmen" }, { value: "91", label: "+91 🇮🇳 Índia" }, { value: "62", label: "+62 🇮🇩 Indonésia" }, { value: "98", label: "+98 🇮🇷 Irã" }, { value: "964", label: "+964 🇮🇶 Iraque" }, { value: "353", label: "+353 🇮🇪 Irlanda" }, { value: "354", label: "+354 🇮🇸 Islândia" }, { value: "972", label: "+972 🇮🇱 Israel" }, { value: "39", label: "+39 🇮🇹 Itália" }, { value: "81", label: "+81 🇯🇵 Japão" }, { value: "962", label: "+962 🇯🇴 Jordânia" }, { value: "254", label: "+254 🇰🇪 Quênia" }, { value: "996", label: "+996 🇰🇬 Quirguistão" }, { value: "383", label: "+383 🇽🇰 Kosovo" }, { value: "965", label: "+965 🇰🇼 Kuwait" }, { value: "856", label: "+856 🇱🇦 Laos" }, { value: "371", label: "+371 🇱🇻 Letônia" }, { value: "961", label: "+961 🇱🇧 Líbano" }, { value: "266", label: "+266 🇱🇸 Lesoto" }, { value: "231", label: "+231 🇱🇷 Libéria" }, { value: "218", label: "+218 🇱🇾 Líbia" }, { value: "423", label: "+423 🇱🇮 Liechtenstein" }, { value: "370", label: "+370 🇱🇹 Lituânia" }, { value: "352", label: "+352 🇱🇺 Luxemburgo" }, { value: "853", label: "+853 🇲🇴 Macau" }, { value: "389", label: "+389 🇲🇰 Macedônia" }, { value: "261", label: "+261 🇲🇬 Madagascar" }, { value: "60", label: "+60 🇲🇾 Malásia" }, { value: "265", label: "+265 🇲🇼 Malawi" }, { value: "960", label: "+960 🇲🇻 Maldivas" }, { value: "223", label: "+223 🇲🇱 Mali" }, { value: "356", label: "+356 🇲🇹 Malta" }, { value: "212", label: "+212 🇲🇦 Marrocos" }, { value: "230", label: "+230 🇲🇺 Maurício" }, { value: "222", label: "+222 🇲🇷 Mauritânia" }, { value: "52", label: "+52 🇲🇽 México" }, { value: "95", label: "+95 🇲🇲 Mianmar" }, { value: "258", label: "+258 🇲🇿 Moçambique" }, { value: "373", label: "+373 🇲🇩 Moldávia" }, { value: "377", label: "+377 🇲🇨 Mônaco" }, { value: "976", label: "+976 🇲🇳 Mongólia" }, { value: "382", label: "+382 🇲🇪 Montenegro" }, { value: "264", label: "+264 🇳🇦 Namíbia" }, { value: "977", label: "+977 🇳🇵 Nepal" }, { value: "505", label: "+505 🇳🇮 Nicarágua" }, { value: "227", label: "+227 🇳🇪 Níger" }, { value: "234", label: "+234 🇳🇬 Nigéria" }, { value: "47", label: "+47 🇳🇴 Noruega" }, { value: "64", label: "+64 🇳🇿 Nova Zelândia" }, { value: "968", label: "+968 🇴🇲 Omã" }, { value: "31", label: "+31 🇳🇱 Países Baixos" }, { value: "92", label: "+92 🇵🇰 Paquistão" }, { value: "507", label: "+507 🇵🇦 Panamá" }, { value: "675", label: "+675 🇵🇬 Papua Nova Guiné" }, { value: "595", label: "+595 🇵🇾 Paraguai" }, { value: "51", label: "+51 🇵🇪 Peru" }, { value: "48", label: "+48 🇵🇱 Polônia" }, { value: "351", label: "+351 🇵🇹 Portugal" }, { value: "974", label: "+974 🇶🇦 Qatar" }, { value: "44", label: "+44 🇬🇧 Reino Unido" }, { value: "236", label: "+236 🇨🇫 Rep. Centro-Africana" }, { value: "243", label: "+243 🇨🇩 Rep. Dem. do Congo" }, { value: "420", label: "+420 🇨🇿 República Tcheca" }, { value: "40", label: "+40 🇷🇴 Romênia" }, { value: "250", label: "+250 🇷🇼 Ruanda" }, { value: "7", label: "+7 🇷🇺 Rússia" }, { value: "966", label: "+966 🇸🇦 Arábia Saudita" }, { value: "221", label: "+221 🇸🇳 Senegal" }, { value: "381", label: "+381 🇷🇸 Sérvia" }, { value: "65", label: "+65 🇸🇬 Singapura" }, { value: "963", label: "+963 🇸🇾 Síria" }, { value: "252", label: "+252 🇸🇴 Somália" }, { value: "94", label: "+94 🇱🇰 Sri Lanka" }, { value: "268", label: "+268 🇸🇿 Suazilândia" }, { value: "249", label: "+249 🇸🇩 Sudão" }, { value: "211", label: "+211 🇸🇸 Sudão do Sul" }, { value: "46", label: "+46 🇸🇪 Suécia" }, { value: "41", label: "+41 🇨🇭 Suíça" }, { value: "597", label: "+597 🇸🇷 Suriname" }, { value: "66", label: "+66 🇹🇭 Tailândia" }, { value: "886", label: "+886 🇹🇼 Taiwan" }, { value: "992", label: "+992 🇹🇯 Tajiquistão" }, { value: "255", label: "+255 🇹🇿 Tanzânia" }, { value: "670", label: "+670 🇹🇱 Timor-Leste" }, { value: "228", label: "+228 🇹🇬 Togo" }, { value: "676", label: "+676 🇹🇴 Tonga" }, { value: "216", label: "+216 🇹🇳 Tunísia" }, { value: "993", label: "+993 🇹🇲 Turcomenistão" }, { value: "90", label: "+90 🇹🇷 Turquia" }, { value: "380", label: "+380 🇺🇦 Ucrânia" }, { value: "256", label: "+256 🇺🇬 Uganda" }, { value: "598", label: "+598 🇺🇾 Uruguai" }, { value: "998", label: "+998 🇺🇿 Uzbequistão" }, { value: "678", label: "+678 🇻🇺 Vanuatu" }, { value: "58", label: "+58 🇻🇪 Venezuela" }, { value: "84", label: "+84 🇻🇳 Vietnã" }, { value: "260", label: "+260 🇿🇲 Zâmbia" }, { value: "263", label: "+263 🇿🇼 Zimbábue" }, ]; const CONVENIOS = [ "Particular", "Unimed", "SulAmérica", "Bradesco Saúde", "Amil", "NotreDame", ]; const generateFallbackId = (): string => { if (typeof crypto !== "undefined" && "randomUUID" in crypto) { return crypto.randomUUID(); } return Math.random().toString(36).slice(2, 11); }; interface Consulta { id: string; pacienteId: string; medicoId: string; pacienteNome: string; medicoNome: string; dataHora: string; // ISO tipo: string; status: | "agendada" | "confirmada" | "cancelada" | "realizada" | "faltou" | string; } interface PacienteUI { id: string; nome: string; email?: string; telefone?: string; codigoPais: string; ddd: string; numeroTelefone: string; cpf?: string; sexo?: string; dataNascimento?: string; tipo_sanguineo?: string; altura?: number | null; peso?: number | null; convenio?: string | null; numeroCarteirinha?: string | null; observacoes?: string | null; vip: boolean; endereco: EnderecoPaciente; } interface PacienteForm { id?: string; nome: string; social_name: string; cpf: string; sexo: string; dataNascimento: string; email: string; codigoPais: string; ddd: string; numeroTelefone: string; telefone?: string; tipo_sanguineo: string; altura: string; peso: string; convenio: string; numeroCarteirinha: string; observacoes: string; endereco: EnderecoPaciente; rg?: string; estado_civil?: string; profissao?: string; telefoneSecundario?: string; telefoneReferencia?: string; codigo_legado?: string; responsavel_nome?: string; responsavel_cpf?: string; documentos?: { tipo: string; numero: string }[]; } interface MedicoForm { id?: string; nome: string; email: string; crm: string; crmUf: string; cpf: string; telefone: string; telefone2: string; especialidade: string; dataNascimento: string; rg: string; cep: string; rua: string; numero: string; complemento: string; bairro: string; cidade: string; estado: string; senha: string; } const buildEmptyPacienteForm = (): PacienteForm => ({ nome: "", social_name: "", cpf: "", sexo: "", dataNascimento: "", email: "", codigoPais: "55", ddd: "", numeroTelefone: "", telefone: undefined, tipo_sanguineo: "", altura: "", peso: "", convenio: "", numeroCarteirinha: "", observacoes: "", endereco: { rua: "", numero: "", complemento: "", bairro: "", cidade: "", estado: "", cep: "", }, rg: "", estado_civil: "", profissao: "", telefoneSecundario: "", telefoneReferencia: "", codigo_legado: "", responsavel_nome: "", responsavel_cpf: "", documentos: [], }); const buildEmptyMedicoForm = (): MedicoForm => ({ nome: "", email: "", crm: "", crmUf: "SP", cpf: "", telefone: "", telefone2: "", especialidade: "", dataNascimento: "", rg: "", cep: "", rua: "", numero: "", complemento: "", bairro: "", cidade: "", estado: "", senha: "", }); const maskCpf = (value: string) => { const digits = value.replace(/\D/g, "").slice(0, 11); let formatted = digits; if (digits.length > 3) { formatted = formatted.replace(/(\d{3})(\d)/, "$1.$2"); } if (digits.length > 6) { formatted = formatted.replace(/(\d{3})\.(\d{3})(\d)/, "$1.$2.$3"); } if (digits.length > 9) { formatted = formatted.replace( /(\d{3})\.(\d{3})\.(\d{3})(\d{1,2})/, "$1.$2.$3-$4" ); } return { formatted, digits }; }; const maskCep = (value: string) => { const digits = value.replace(/\D/g, "").slice(0, 8); if (digits.length > 5) { return digits.replace(/(\d{5})(\d{1,3})/, "$1-$2"); } return digits; }; const splitTelefone = (telefone?: string) => { if (!telefone) { return { codigoPais: "55", ddd: "", numeroTelefone: "" }; } const digits = telefone.replace(/\D/g, ""); if (digits.length <= 9) { return { codigoPais: "55", ddd: "", numeroTelefone: digits }; } const numeroTelefone = digits.slice(-9); const ddd = digits.length >= 11 ? digits.slice(-11, -9) : ""; const codigoPais = digits.slice(0, digits.length - 9 - (ddd ? 2 : 0)) || "55"; return { codigoPais, ddd, numeroTelefone }; }; const composeTelefone = (codigoPais: string, ddd: string, numero: string) => { const sanitizedCodigo = codigoPais.replace(/\D/g, ""); const sanitizedDDD = ddd.replace(/\D/g, ""); const sanitizedNumero = numero.replace(/\D/g, ""); if (!sanitizedCodigo && !sanitizedDDD && !sanitizedNumero) { return undefined; } let formatted = sanitizedNumero; if (sanitizedNumero.length > 5) { formatted = sanitizedNumero.replace(/(\d{5})(\d{1,4})?/, "$1-$2"); } if (sanitizedDDD) { formatted = `(${sanitizedDDD}) ${formatted}`; } if (sanitizedCodigo) { formatted = `+${sanitizedCodigo} ${formatted}`; } return formatted; }; const normalizePaciente = (data: PacienteServiceModel): PacienteUI => { const id = data.id || generateFallbackId(); const telefoneInfo = splitTelefone(data.phone_mobile); return { id, nome: data.full_name ?? "", email: data.email ?? "", telefone: data.phone_mobile ?? undefined, codigoPais: telefoneInfo.codigoPais || "55", ddd: telefoneInfo.ddd, numeroTelefone: telefoneInfo.numeroTelefone, cpf: data.cpf ?? "", sexo: data.sex ?? "", dataNascimento: data.birth_date ?? "", tipo_sanguineo: data.blood_type ?? "", altura: data.height_m ?? null, peso: data.weight_kg ?? null, convenio: (data as any).convenio ?? null, numeroCarteirinha: (data as any).numeroCarteirinha ?? null, observacoes: (data as any).observacoes ?? null, vip: Boolean((data as any).vip), endereco: { rua: data.street ?? "", numero: data.number ?? "", complemento: data.complement ?? "", bairro: data.neighborhood ?? "", cidade: data.city ?? "", estado: data.state ?? "", cep: data.cep ?? "", }, }; }; const buildPacienteFormFromPaciente = (paciente: PacienteUI): PacienteForm => { const { formatted } = maskCpf(paciente.cpf ?? ""); return { id: paciente.id, nome: paciente.nome, social_name: "", cpf: formatted, sexo: paciente.sexo ?? "", dataNascimento: paciente.dataNascimento ?? "", email: paciente.email ?? "", codigoPais: paciente.codigoPais || "55", ddd: paciente.ddd, numeroTelefone: paciente.numeroTelefone, telefone: paciente.telefone, tipo_sanguineo: paciente.tipo_sanguineo ?? "", altura: paciente.altura !== null && paciente.altura !== undefined ? String(paciente.altura) : "", peso: paciente.peso !== null && paciente.peso !== undefined ? String(paciente.peso) : "", convenio: paciente.convenio ?? "", numeroCarteirinha: paciente.numeroCarteirinha ?? "", observacoes: paciente.observacoes ?? "", endereco: { rua: paciente.endereco.rua ?? "", numero: paciente.endereco.numero ?? "", complemento: paciente.endereco.complemento ?? "", bairro: paciente.endereco.bairro ?? "", cidade: paciente.endereco.cidade ?? "", estado: paciente.endereco.estado ?? "", cep: paciente.endereco.cep ?? "", }, }; }; // Normalize doctor for components expecting legacy format const normalizeMedico = (doctor: Doctor): any => ({ ...doctor, nome: doctor.full_name, telefone: doctor.phone_mobile, especialidade: doctor.specialty, dataNascimento: doctor.birth_date, }); // Normalize report for components expecting legacy format const normalizeRelatorio = (report: Report): any => ({ ...report, exam: report.exam ?? undefined, }); const formatTelefone = (paciente: PacienteUI) => { const composed = composeTelefone( paciente.codigoPais, paciente.ddd, paciente.numeroTelefone ); return composed ?? paciente.telefone ?? "Telefone não informado"; }; const formatEmail = (email?: string) => email ? email.trim().toLowerCase() : "Não informado"; // formatarData e getStatusColor removidos após adoção de ConsultationList // Formata ISO "YYYY-MM-DDTHH:mm:ss" sem alterar fuso/offset const formatDateTimeLocal = (iso?: string) => { if (!iso) return ""; const [date, timeRaw] = iso.split("T"); if (!date) return iso; const [y, m, d] = date.split("-"); const time = (timeRaw || "").slice(0, 8); // HH:mm:ss if (y && m && d && time) return `${d}/${m}/${y}, ${time}`; if (y && m && d) return `${d}/${m}/${y}`; return iso; }; const buildMedicoTelefone = (value: string) => { const digits = value.replace(/\D/g, "").slice(0, 13); if (!digits) return ""; if (digits.length <= 2) return `(${digits}`; if (digits.length <= 10) { return digits.replace( /(\d{2})(\d{4})(\d{0,4})?/, (_, d1: string, d2: string, d3?: string) => { if (d3) { return `(${d1}) ${d2}-${d3}`; } return `(${d1}) ${d2}`; } ); } return digits.replace( /(\d{2})(\d{2})(\d{5})(\d{0,4})?/, (_: string, pais: string, ddd: string, n1: string, n2?: string) => { const base = `+${pais} (${ddd}) ${n1}`; return n2 ? `${base}-${n2}` : base; } ); }; // Mapear status da API para status da UI const mapAppointmentStatus = (status: string): Consulta["status"] => { const statusMap: Record = { requested: "agendada", confirmed: "confirmada", cancelled: "cancelada", completed: "realizada", no_show: "faltou", checked_in: "confirmada", in_progress: "confirmada", }; return statusMap[status] || "agendada"; }; // Converter Appointment da API para Consulta da UI const appointmentToConsulta = ( apt: Appointment, pacientes: PacienteUI[], medicos: Medico[] ): Consulta => { const paciente = pacientes.find((p) => p.id === apt.patient_id); const medico = medicos.find((m) => m.id === apt.doctor_id); return { id: apt.id || "", pacienteId: apt.patient_id || "", medicoId: apt.doctor_id || "", pacienteNome: paciente?.nome || "Paciente não encontrado", medicoNome: medico?.full_name || "Médico não encontrado", dataHora: apt.scheduled_at || "", tipo: apt.appointment_type || "consulta", status: mapAppointmentStatus(apt.status || "requested"), }; }; const PainelSecretaria = () => { const navigate = useNavigate(); const { logout } = useAuth(); const [loading, setLoading] = useState(false); const [activeTab, setActiveTab] = useState("pacientes"); const [relatorios, setRelatorios] = useState([]); const [loadingRelatorios, setLoadingRelatorios] = useState(false); const [reportModalOpen, setReportModalOpen] = useState(false); const [reportSaving, setReportSaving] = useState(false); const [reportForm, setReportForm] = useState({ patientId: "", orderNumber: "", exam: "", diagnosis: "", conclusion: "", dueAt: "", // YYYY-MM-DD status: "draft" as "draft" | "pending" | "completed" | "cancelled", }); const [reportModalMode, setReportModalMode] = useState<"create" | "edit">( "create" ); const [editingReportId, setEditingReportId] = useState(null); // Gera número padrão no formato exato solicitado: REL-YYYY-MM-XXXXXX (6 caracteres alfanuméricos) const generateDefaultOrderNumber = () => { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, "0"); const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let suffix = ""; for (let i = 0; i < 6; i++) { suffix += chars[Math.floor(Math.random() * chars.length)]; } return `REL-${year}-${month}-${suffix}`; }; const ORDER_NUMBER_PATTERN = /^REL-\d{4}-\d{2}-[A-Z0-9]{6}$/; const [reportDetailsOpen, setReportDetailsOpen] = useState(false); const [reportDetailsLoading, setReportDetailsLoading] = useState(false); const [reportDetails, setReportDetails] = useState(null); const openReportDetails = async (id?: string) => { if (!id) return; setReportDetailsLoading(true); setReportDetails(null); try { const report = await reportService.getById(id); setReportDetails(report); setReportDetailsOpen(true); } catch { toast.error("Erro ao carregar relatório"); } finally { setReportDetailsLoading(false); } }; const [selectedDoctorAgenda, setSelectedDoctorAgenda] = useState(""); const [agendaDoctors, setAgendaDoctors] = useState([]); const [pacientes, setPacientes] = useState([]); const [medicos, setMedicos] = useState([]); const [agendamentos, setAgendamentos] = useState([]); const [agendamentosLoading, setAgendamentosLoading] = useState(false); // Estados de filtros de agendamentos const [consultaFiltroMedico, setConsultaFiltroMedico] = useState(""); const [consultaFiltroPaciente, setConsultaFiltroPaciente] = useState(""); const [consultaFiltroStatus, setConsultaFiltroStatus] = useState(""); const [consultaFiltroDataDe, setConsultaFiltroDataDe] = useState(""); const [consultaFiltroDataAte, setConsultaFiltroDataAte] = useState(""); // Estado para lista de pacientes com dados de último/próximo atendimento const [pacientesEnriquecidos, setPacientesEnriquecidos] = useState< Record >({}); const [searchTerm, setSearchTerm] = useState(""); const [searchId, setSearchId] = useState(""); const [selectedConvenio, setSelectedConvenio] = useState(""); const [filterBirthday, setFilterBirthday] = useState(false); const [filterVip, setFilterVip] = useState(false); const [patientModalOpen, setPatientModalOpen] = useState(false); const [patientModalMode, setPatientModalMode] = useState<"create" | "edit">( "create" ); const [formDataPaciente, setFormDataPaciente] = useState( buildEmptyPacienteForm() ); // Removida validação de CPF (local + externa) const [doctorModalOpen, setDoctorModalOpen] = useState(false); const [doctorModalMode, setDoctorModalMode] = useState<"create" | "edit">( "create" ); const [formDataMedico, setFormDataMedico] = useState( buildEmptyMedicoForm() ); // Estado para modal de agendamento const [scheduleModalOpen, setScheduleModalOpen] = useState(false); const [schedulePatientId, setSchedulePatientId] = useState(""); const [schedulePatientName, setSchedulePatientName] = useState(""); const carregarPacientes = useCallback(async () => { try { console.log("[PainelSecretaria] Carregando pacientes..."); const patients = await patientService.list(); console.log("[PainelSecretaria] Resposta pacientes:", patients); console.log("[PainelSecretaria] Pacientes recebidos:", patients.length); setPacientes(patients.map(normalizePaciente)); } catch (error) { console.error("Erro ao carregar pacientes:", error); toast.error("Erro ao carregar pacientes"); } }, []); const carregarMedicos = useCallback(async () => { try { console.log("[PainelSecretaria] Carregando médicos..."); const doctors = await doctorService.list(); console.log("[PainelSecretaria] Resposta médicos:", doctors); if (doctors && doctors.length > 0) { console.log("[PainelSecretaria] Médicos recebidos:", doctors.length); setMedicos(doctors); } else { console.error("[PainelSecretaria] Erro na resposta"); toast.error("Erro ao carregar médicos"); } } catch (error) { console.error("Erro ao carregar médicos:", error); toast.error("Erro ao carregar médicos"); } }, []); const carregarRelatorios = useCallback(async () => { setLoadingRelatorios(true); try { const reports = await reportService.list(); setRelatorios(reports); } catch (error) { console.error("Erro ao carregar relatórios:", error); toast.error("Erro ao carregar relatórios"); } finally { setLoadingRelatorios(false); } }, []); const carregarAgendamentos = useCallback(async () => { setAgendamentosLoading(true); try { console.log("[PainelSecretaria] Carregando agendamentos..."); const response = await appointmentService.list(); console.log("[PainelSecretaria] Resposta agendamentos:", response); if (response && response.length > 0) { console.log( "[PainelSecretaria] Agendamentos recebidos:", response.length ); setAgendamentos(response); } else { setAgendamentos([]); } } catch (error) { console.error("Erro ao carregar agendamentos:", error); toast.error("Erro ao carregar agendamentos"); } finally { setAgendamentosLoading(false); } }, []); const carregarDados = useCallback(async () => { setLoading(true); try { await Promise.all([ carregarPacientes(), carregarMedicos(), carregarRelatorios(), carregarAgendamentos(), ]); } finally { setLoading(false); } }, [ carregarPacientes, carregarMedicos, carregarRelatorios, carregarAgendamentos, ]); useEffect(() => { void carregarDados(); }, [carregarDados]); useEffect(() => { if (activeTab === "relatorios") { void carregarRelatorios(); } }, [activeTab, carregarRelatorios]); useEffect(() => { (async () => { const doctors = await doctorService.list(); // Filter active doctors if needed const activeDoctors = doctors.filter((d: Doctor) => d.active !== false); setAgendaDoctors(activeDoctors); if (!selectedDoctorAgenda && activeDoctors.length) { setSelectedDoctorAgenda(activeDoctors[0].id); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const resetPacienteForm = useCallback(() => { setFormDataPaciente(buildEmptyPacienteForm()); }, []); const resetMedicoForm = useCallback(() => { setFormDataMedico(buildEmptyMedicoForm()); }, []); const handleLogout = useCallback(() => { console.log("[PainelSecretaria] Fazendo logout..."); logout(); toast.success("Sessão encerrada"); navigate("/login-secretaria"); }, [logout, navigate]); const openCreatePacienteModal = useCallback(() => { resetPacienteForm(); setPatientModalMode("create"); setPatientModalOpen(true); }, [resetPacienteForm]); const openEditPacienteModal = useCallback((paciente: PacienteUI) => { setFormDataPaciente(buildPacienteFormFromPaciente(paciente)); setPatientModalMode("edit"); setPatientModalOpen(true); }, []); const closePacienteModal = useCallback(() => { setPatientModalOpen(false); }, []); const openCreateMedicoModal = useCallback(() => { resetMedicoForm(); setDoctorModalMode("create"); setDoctorModalOpen(true); }, [resetMedicoForm]); const openEditMedicoModal = useCallback((medico: Medico) => { const medicoDetalhado = medico as Medico & Partial>; setFormDataMedico({ id: medico.id, nome: medico.full_name || "", email: medico.email || "", crm: medico.crm || "", crmUf: medico.crm_uf || "", cpf: medico.cpf || "", telefone: medico.phone_mobile || "", telefone2: medico.phone2 || "", especialidade: medico.specialty || "", dataNascimento: medico.birth_date || "", rg: medico.rg || "", cep: medicoDetalhado.cep || "", rua: medicoDetalhado.rua || "", numero: medicoDetalhado.numero || "", complemento: medicoDetalhado.complemento || "", bairro: medicoDetalhado.bairro || "", cidade: medicoDetalhado.cidade || "", estado: medicoDetalhado.estado || "", senha: "", }); setDoctorModalMode("edit"); setDoctorModalOpen(true); }, []); const closeMedicoModal = useCallback(() => { setDoctorModalOpen(false); }, []); const handleBuscarPorId = useCallback(async () => { if (!searchId) { toast.error("Informe o ID do paciente"); return; } setLoading(true); try { const patient = await patientService.getById(searchId); setPacientes([normalizePaciente(patient)]); } catch (error) { console.error("Erro ao buscar paciente:", error); toast.error("Paciente não encontrado"); } finally { setLoading(false); } }, [searchId]); const handleDeletePaciente = useCallback(async (paciente: PacienteUI) => { if (!window.confirm(`Deseja remover o paciente ${paciente.nome}?`)) { return; } try { console.log("[PainelSecretaria] Deletando paciente:", { id: paciente.id, nome: paciente.nome, }); await patientService.delete(paciente.id); console.log("[PainelSecretaria] Paciente deletado com sucesso"); setPacientes((prev) => prev.filter((p) => p.id !== paciente.id)); toast.success("Paciente removido com sucesso"); } catch (error) { console.error("[PainelSecretaria] Erro ao remover paciente:", error); toast.error("Erro ao remover paciente"); } }, []); const handleDeleteMedico = useCallback(async (medico: Medico) => { if (!window.confirm(`Deseja remover o médico ${medico.full_name}?`)) { return; } try { await doctorService.delete(medico.id!); setMedicos((prev) => prev.filter((m) => m.id !== medico.id)); toast.success("Médico removido"); } catch (error) { console.error("Erro ao remover médico:", error); toast.error("Erro ao remover médico"); } }, []); const handleCpfChange = useCallback( (event: ChangeEvent) => { const { formatted } = maskCpf(event.target.value); setFormDataPaciente((prev) => ({ ...prev, cpf: formatted })); }, [] ); const handleCepLookup = useCallback(async (rawCep: string) => { const digits = rawCep.replace(/\D/g, ""); if (digits.length !== 8) return; try { const endereco = await buscarEnderecoViaCEP(digits); if (!endereco) { toast.error("CEP não encontrado"); return; } setFormDataPaciente((prev) => ({ ...prev, endereco: { ...prev.endereco, rua: endereco.rua ?? prev.endereco.rua, bairro: endereco.bairro ?? prev.endereco.bairro, cidade: endereco.cidade ?? prev.endereco.cidade, estado: endereco.estado ?? prev.endereco.estado, cep: endereco.cep ?? rawCep, }, })); } catch (error) { console.warn("Erro ao buscar CEP:", error); toast.error("Erro ao buscar CEP"); } }, []); const handleCepLookupMedico = useCallback(async (rawCep: string) => { const digits = rawCep.replace(/\D/g, ""); if (digits.length !== 8) return; try { const endereco = await buscarEnderecoViaCEP(digits); if (!endereco) { toast.error("CEP não encontrado"); return; } setFormDataMedico((prev) => ({ ...prev, rua: endereco.rua ?? prev.rua, bairro: endereco.bairro ?? prev.bairro, cidade: endereco.cidade ?? prev.cidade, estado: endereco.estado ?? prev.estado, cep: endereco.cep ?? rawCep, })); toast.success("Endereço preenchido automaticamente!"); } catch (error) { console.warn("Erro ao buscar CEP:", error); toast.error("Erro ao buscar CEP"); } }, []); const handleSubmitPaciente = useCallback( async (event: FormEvent) => { event.preventDefault(); const { digits } = maskCpf(formDataPaciente.cpf); // Validação de CPF removida – apenas mascaramento e envio dos dígitos. setLoading(true); try { // Validação externa de CPF removida const telefone = composeTelefone( formDataPaciente.codigoPais, formDataPaciente.ddd, formDataPaciente.numeroTelefone ) ?? formDataPaciente.telefone; const payload: Partial = { full_name: formDataPaciente.nome, cpf: digits, email: formDataPaciente.email, phone_mobile: telefone || "", sex: formDataPaciente.sexo, birth_date: formDataPaciente.dataNascimento, blood_type: formDataPaciente.tipo_sanguineo, street: formDataPaciente.endereco.rua, number: formDataPaciente.endereco.numero, complement: formDataPaciente.endereco.complemento, neighborhood: formDataPaciente.endereco.bairro, city: formDataPaciente.endereco.cidade, state: formDataPaciente.endereco.estado, cep: formDataPaciente.endereco.cep, social_name: formDataPaciente.social_name || undefined, }; // Campos estendidos ainda não suportados pelo backend oficial (armazenar localmente para futura sincronização) interface ExtendedPacienteMeta { rg?: string; estado_civil?: string; profissao?: string; telefoneSecundario?: string; telefoneReferencia?: string; codigo_legado?: string; responsavel_nome?: string; responsavel_cpf?: string; documentos?: { tipo: string; numero: string }[]; updatedAt?: string; } const extended: ExtendedPacienteMeta = { rg: formDataPaciente.rg, estado_civil: formDataPaciente.estado_civil, profissao: formDataPaciente.profissao, telefoneSecundario: formDataPaciente.telefoneSecundario, telefoneReferencia: formDataPaciente.telefoneReferencia, codigo_legado: formDataPaciente.codigo_legado, responsavel_nome: formDataPaciente.responsavel_nome, responsavel_cpf: formDataPaciente.responsavel_cpf, documentos: formDataPaciente.documentos || [], }; // Persistir metadados localmente (namespace pacientes_meta) para fins de prontuário até backend try { const metaRaw = localStorage.getItem("pacientes_meta") || "{}"; const meta = JSON.parse(metaRaw) as Record< string, ExtendedPacienteMeta >; meta[formDataPaciente.id || digits] = { ...(meta[formDataPaciente.id || digits] || {}), ...extended, updatedAt: new Date().toISOString(), }; localStorage.setItem("pacientes_meta", JSON.stringify(meta)); } catch { // falha silenciosa } if (formDataPaciente.altura.trim()) { payload.height_m = Number(formDataPaciente.altura); } if (formDataPaciente.peso.trim()) { payload.weight_kg = Number(formDataPaciente.peso); } if (patientModalMode === "create") { const newPatient = await patientService.create( payload as CreatePatientInput ); setPacientes((prev) => [...prev, normalizePaciente(newPatient)]); toast.success("Paciente cadastrado com sucesso!"); } else if (patientModalMode === "edit" && formDataPaciente.id) { const updatedPatient = await patientService.update( formDataPaciente.id, payload as UpdatePatientInput ); const normalizado = normalizePaciente(updatedPatient); setPacientes((prev) => prev.map((p) => (p.id === formDataPaciente.id ? normalizado : p)) ); toast.success("Paciente atualizado com sucesso!"); } resetPacienteForm(); setPatientModalOpen(false); } catch (error) { console.error("Erro ao salvar paciente:", error); toast.error("Erro ao salvar paciente"); } finally { setLoading(false); } }, [formDataPaciente, patientModalMode, resetPacienteForm] ); const handleSubmitMedico = useCallback( async (event: FormEvent) => { event.preventDefault(); setLoading(true); try { if (doctorModalMode === "create") { const payload = { full_name: formDataMedico.nome, email: formDataMedico.email, crm: formDataMedico.crm, crm_uf: formDataMedico.crmUf as CrmUF, cpf: formDataMedico.cpf, phone_mobile: formDataMedico.telefone, phone2: formDataMedico.telefone2, specialty: formDataMedico.especialidade, birth_date: formDataMedico.dataNascimento, rg: formDataMedico.rg, cep: formDataMedico.cep, street: formDataMedico.rua, number: formDataMedico.numero, complement: formDataMedico.complemento, neighborhood: formDataMedico.bairro, city: formDataMedico.cidade, state: formDataMedico.estado, }; const newDoctor = await doctorService.create(payload); setMedicos((prev) => [...prev, newDoctor]); toast.success("Médico cadastrado com sucesso!"); } else if (doctorModalMode === "edit" && formDataMedico.id) { const payload: MedicoUpdate = { full_name: formDataMedico.nome, email: formDataMedico.email, crm: formDataMedico.crm, crm_uf: formDataMedico.crmUf as CrmUF, cpf: formDataMedico.cpf, phone_mobile: formDataMedico.telefone, phone2: formDataMedico.telefone2, specialty: formDataMedico.especialidade, birth_date: formDataMedico.dataNascimento, rg: formDataMedico.rg, cep: formDataMedico.cep, street: formDataMedico.rua, number: formDataMedico.numero, complement: formDataMedico.complemento, neighborhood: formDataMedico.bairro, city: formDataMedico.cidade, state: formDataMedico.estado, }; const updatedDoctor = await doctorService.update( formDataMedico.id, payload ); setMedicos((prev) => prev.map((m) => (m.id === formDataMedico.id ? updatedDoctor : m)) ); toast.success("Médico atualizado com sucesso!"); } resetMedicoForm(); setDoctorModalOpen(false); } catch (error) { console.error("Erro ao salvar médico:", error); toast.error("Erro ao salvar médico"); } finally { setLoading(false); } }, [doctorModalMode, formDataMedico, resetMedicoForm] ); const conveniosDisponiveis = useMemo(() => { const values = new Set(); CONVENIOS.forEach((item) => values.add(item)); pacientes.forEach((paciente) => { const convenio = paciente.convenio?.trim(); if (convenio) { values.add(convenio); } }); return Array.from(values).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" }) ); }, [pacientes]); const pacientesFiltrados = useMemo(() => { const termo = searchTerm.trim().toLowerCase(); const convenioFiltro = selectedConvenio.trim().toLowerCase(); const currentMonth = new Date().getMonth(); return pacientes.filter((paciente) => { const nome = paciente.nome?.toLowerCase() ?? ""; const email = (paciente.email ?? "").toLowerCase(); const matchesSearch = !termo || nome.includes(termo) || email.includes(termo); if (!matchesSearch) return false; const matchesConvenio = !convenioFiltro || (paciente.convenio ?? "").trim().toLowerCase() === convenioFiltro; if (!matchesConvenio) return false; if (filterVip && !paciente.vip) return false; if (filterBirthday) { if (!paciente.dataNascimento) return false; const data = new Date(paciente.dataNascimento); if (Number.isNaN(data.getTime())) return false; if (data.getMonth() !== currentMonth) return false; } return true; }); }, [pacientes, searchTerm, selectedConvenio, filterVip, filterBirthday]); // Converter agendamentos da API para o formato de consultas da UI const consultas = useMemo(() => { return agendamentos.map((apt) => appointmentToConsulta(apt, pacientes, medicos) ); }, [agendamentos, pacientes, medicos]); // Enriquecer pacientes com info de consultas useEffect(() => { const baseIds = new Set(pacientesFiltrados.map((p) => p.id)); const enriq: Record< string, { ultimo?: string | null; proximo?: string | null } > = {}; baseIds.forEach((id) => { const consultasPaciente = consultas.filter((c) => c.pacienteId === id); if (consultasPaciente.length === 0) { enriq[id] = { ultimo: null, proximo: null }; return; } const agora = new Date(); const passadas = consultasPaciente .filter((c) => new Date(c.dataHora) < agora) .sort( (a, b) => new Date(b.dataHora).getTime() - new Date(a.dataHora).getTime() ); const futuras = consultasPaciente .filter((c) => new Date(c.dataHora) >= agora) .sort( (a, b) => new Date(a.dataHora).getTime() - new Date(b.dataHora).getTime() ); enriq[id] = { ultimo: passadas[0] ? new Date(passadas[0].dataHora).toLocaleDateString("pt-BR", { dateStyle: "short", }) : null, proximo: futuras[0] ? new Date(futuras[0].dataHora).toLocaleDateString("pt-BR", { dateStyle: "short", }) : null, }; }); setPacientesEnriquecidos(enriq); }, [pacientesFiltrados, consultas]); const medicosFiltrados = useMemo(() => { const termo = searchTerm.toLowerCase(); return medicos.filter( (medico) => (medico.full_name || "").toLowerCase().includes(termo) || (medico.specialty ?? "").toLowerCase().includes(termo) ); }, [medicos, searchTerm]); const consultasFiltradas = useMemo(() => { return consultas.filter((c) => { if (searchTerm) { const termo = searchTerm.toLowerCase(); if ( !( c.pacienteNome.toLowerCase().includes(termo) || c.medicoNome.toLowerCase().includes(termo) || c.tipo.toLowerCase().includes(termo) ) ) { return false; } } if ( consultaFiltroMedico && c.medicoNome !== consultaFiltroMedico && c.medicoNome !== consultaFiltroMedico ) { // ajuste: filtro usa id, adaptar quando mapeamento existir } if (consultaFiltroStatus && c.status !== consultaFiltroStatus) return false; if (consultaFiltroDataDe) { if (new Date(c.dataHora) < new Date(consultaFiltroDataDe)) return false; } if (consultaFiltroDataAte) { if ( new Date(c.dataHora) > new Date(consultaFiltroDataAte + "T23:59:59") ) return false; } if ( consultaFiltroPaciente && c.pacienteNome !== consultaFiltroPaciente && c.pacienteNome !== consultaFiltroPaciente ) { // idem futuro mapeamento por id } return true; }); }, [ consultas, searchTerm, consultaFiltroMedico, consultaFiltroPaciente, consultaFiltroStatus, consultaFiltroDataDe, consultaFiltroDataAte, ]); // Funções para manipular agendamentos via API const alterarStatusConsulta = async (id: string, novoStatus: string) => { try { // Mapear status da UI para status da API const statusMap: Record< string, | "requested" | "confirmed" | "cancelled" | "completed" | "no_show" | "checked_in" | "in_progress" > = { agendada: "requested", confirmada: "confirmed", cancelada: "cancelled", realizada: "completed", faltou: "no_show", }; const apiStatus = statusMap[novoStatus] || "requested"; await appointmentService.update(id, { status: apiStatus, }); await carregarAgendamentos(); toast.success("Status atualizado"); } catch { toast.error("Erro ao atualizar status"); } }; const deletarConsulta = async (id: string) => { const confirma = window.confirm("Confirma excluir este agendamento?"); if (!confirma) return; try { await appointmentService.delete(id); await carregarAgendamentos(); toast.success("Agendamento excluído"); } catch { toast.error("Erro ao excluir"); } }; // Agendamentos são carregados via API no carregarDados() const isInitialLoading = loading && !patientModalOpen && !doctorModalOpen && pacientes.length === 0 && medicos.length === 0; if (isInitialLoading) { return (

Carregando painel da secretária...

); } return (
{/* Header */}
{/* Logo */}
Secretária
{/* Navigation */} {/* Actions */}
{activeTab === "dashboard" && (
{/* Stats Cards */}

Pacientes

{pacientes.length}

Médicos

{medicos.length}

Consultas

{consultas.length}

Relatórios

{relatorios.length}

{/* Quick Actions */}

Ações Rápidas

{/* Activity Feed */}

Atividade Recente

{consultas.slice(0, 5).map((consulta) => (

Consulta: {consulta.pacienteNome}

com {consulta.medicoNome}

{consulta.dataHora ? new Date(consulta.dataHora).toLocaleDateString( "pt-BR" ) : "-"}
))} {consultas.length === 0 && (

Nenhuma atividade recente

)}
)} {activeTab === "pacientes" && (
{/* Header */}

Pacientes

Gerencie todos os pacientes cadastrados

{/* Search and Filters */}
setSearchTerm(event.target.value)} className="pl-10 w-full h-10 px-3 rounded-md border border-input bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring" />
setSearchId(event.target.value)} className="w-48 h-10 px-3 rounded-md border border-input bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring" />
{/* Patient Table */}
((p) => ({ id: p.id, nome: p.nome, cpf: p.cpf, email: formatEmail(p.email), telefoneFormatado: formatTelefone(p), convenio: p.convenio, vip: p.vip, cidade: p.endereco?.cidade, estado: p.endereco?.estado, ultimoAtendimento: pacientesEnriquecidos[p.id]?.ultimo ?? null, proximoAtendimento: pacientesEnriquecidos[p.id]?.proximo ?? null, }))} onEdit={(item) => { const original = pacientesFiltrados.find( (pf) => pf.id === item.id ); if (original) openEditPacienteModal(original); }} onDelete={(item) => { const original = pacientesFiltrados.find( (pf) => pf.id === item.id ); if (original) void handleDeletePaciente(original); }} onView={(item) => { navigate(`/pacientes/${encodeURIComponent(item.id)}`); }} onSchedule={(item) => { setSchedulePatientId(item.id); setSchedulePatientName(item.nome); setScheduleModalOpen(true); }} />
)} {activeTab === "medicos" && (
{/* Header */}

Médicos

Gerencie os médicos cadastrados na clínica

{/* Search */}
setSearchTerm(event.target.value)} className="pl-10 pr-4 py-2 w-full bg-background border border-input rounded-lg focus:ring-2 focus:ring-ring focus:border-transparent" />
{/* Table */}
{medicosFiltrados.map((medico) => { const getInitials = (nome: string) => { return nome .split(" ") .map((n) => n[0]) .join("") .substring(0, 2) .toUpperCase(); }; const getColorClass = (nome: string) => { const colors = [ "bg-blue-500", "bg-green-500", "bg-purple-500", "bg-pink-500", "bg-yellow-500", "bg-indigo-500", ]; const index = nome .split("") .reduce( (acc, char) => acc + char.charCodeAt(0), 0 ) % colors.length; return colors[index]; }; return ( ); })} {medicosFiltrados.length === 0 && ( )}
Médico Especialidade Contato Ações
{getInitials(medico.full_name || "Sem nome")}
Dr(a). {medico.full_name || "Sem nome"}
CRM: {medico.crm || "Não informado"}
{medico.specialty || "Não informado"}
{formatEmail(medico.email)}
{medico.phone_mobile || "Telefone não informado"}
Nenhum médico encontrado.
)} {activeTab === "consultas" && ( { setSchedulePatientId(""); setSchedulePatientName(""); setScheduleModalOpen(true); }} onDeleteConsulta={deletarConsulta} onAlterarStatus={alterarStatusConsulta} /> )} {activeTab === "agenda" && ( )} {activeTab === "relatorios" && ( { setReportModalMode("create"); setEditingReportId(null); setReportForm({ patientId: "", orderNumber: generateDefaultOrderNumber(), exam: "", diagnosis: "", conclusion: "", dueAt: "", status: "draft", }); setReportModalOpen(true); }} onVerDetalhes={openReportDetails} onEditarRelatorio={async (id: string) => { setReportModalMode("edit"); setEditingReportId(id); const r = await reportService.getById(id); setReportForm({ patientId: r.patient_id || "", orderNumber: r.order_number || "", exam: r.exam || "", diagnosis: r.diagnosis || "", conclusion: r.conclusion || "", dueAt: r.due_at ? r.due_at.slice(0, 10) : "", status: (r.status && ["draft", "pending", "completed", "cancelled"].includes( r.status ) ? r.status : "draft") as "draft" | "pending" | "completed" | "cancelled", }); setReportModalOpen(true); }} /> )}
{patientModalOpen && (

{patientModalMode === "create" ? "Cadastrar Novo Paciente" : "Editar Paciente"}

Preencha todos os campos obrigatórios (*)

{/* Seção: Dados Pessoais */}

Dados Pessoais

setFormDataPaciente((prev) => ({ ...prev, nome: event.target.value, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" required placeholder="Maria Santos Silva" />
setFormDataPaciente((prev) => ({ ...prev, social_name: event.target.value, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="Maria Santos" />
setFormDataPaciente((prev) => ({ ...prev, dataNascimento: event.target.value, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" required />
{/* Seção: Contato */}

Contato

setFormDataPaciente((prev) => ({ ...prev, email: event.target.value, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" required placeholder="maria@email.com" />
setFormDataPaciente((prev) => ({ ...prev, ddd: event.target.value .replace(/\D/g, "") .slice(0, 2), })) } className="w-16 px-2 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent text-sm text-center" placeholder="11" required /> setFormDataPaciente((prev) => ({ ...prev, numeroTelefone: event.target.value .replace(/\D/g, "") .slice(0, 9), })) } className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="99999-9999" required />
{/* Seção: Informações Clínicas */}

Informações Clínicas

setFormDataPaciente((prev) => ({ ...prev, peso: event.target.value, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="65.5" />
setFormDataPaciente((prev) => ({ ...prev, altura: event.target.value, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="1.65" />
setFormDataPaciente((prev) => ({ ...prev, numeroCarteirinha: event.target.value, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="Número da carteirinha" />
{/* Seção: Endereço */}

Endereço

{ const digits = event.target.value .replace(/\D/g, "") .slice(0, 8); setFormDataPaciente((prev) => ({ ...prev, endereco: { ...prev.endereco, cep: digits, }, })); }} onBlur={(event) => { const digits = event.target.value.replace(/\D/g, ""); if (digits.length === 8) { void handleCepLookup(digits); } }} className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="01234-567" maxLength={9} />
setFormDataPaciente((prev) => ({ ...prev, endereco: { ...prev.endereco, rua: event.target.value, }, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="Rua das Flores" />
setFormDataPaciente((prev) => ({ ...prev, endereco: { ...prev.endereco, numero: event.target.value, }, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="123" />
setFormDataPaciente((prev) => ({ ...prev, endereco: { ...prev.endereco, bairro: event.target.value, }, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="Centro" />
setFormDataPaciente((prev) => ({ ...prev, endereco: { ...prev.endereco, cidade: event.target.value, }, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="São Paulo" />
setFormDataPaciente((prev) => ({ ...prev, endereco: { ...prev.endereco, estado: event.target.value.toUpperCase(), }, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="SP" maxLength={2} />
setFormDataPaciente((prev) => ({ ...prev, endereco: { ...prev.endereco, complemento: event.target.value, }, })) } className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="Apto 45, Bloco B..." />
{/* Seção: Observações */}

Observações Adicionais