diff --git a/susconecta/app/dashboard/pacientes/page.tsx b/susconecta/app/dashboard/pacientes/page.tsx index bbb5d20..b1d8ff8 100644 --- a/susconecta/app/dashboard/pacientes/page.tsx +++ b/susconecta/app/dashboard/pacientes/page.tsx @@ -1,421 +1,251 @@ -"use client" +/* src/app/dashboard/pacientes/page.tsx */ +"use client"; -import { useState } from "react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Badge } from "@/components/ui/badge" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { Label } from "@/components/ui/label" -import { - Search, - Filter, - Plus, - MoreHorizontal, - Calendar, - Gift, - Eye, - Edit, - Trash2, - CalendarPlus, - ArrowLeft, -} from "lucide-react" -import { PatientRegistrationForm } from "@/components/forms/patient-registration-form" +import { useEffect, useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { MoreHorizontal, Plus, Search, Eye, Edit, Trash2, ArrowLeft } from "lucide-react"; -const patients = [ - { - id: 1, - name: "Aaron Avalos Perez", - phone: "(75) 99982-6363", - city: "Aracaju", - state: "Sergipe", - lastAppointment: "26/09/2025 14:30", - nextAppointment: "19/08/2025 15:00", - isVip: false, - convenio: "unimed", - birthday: "1985-03-15", - age: 40, - }, - { - id: 2, - name: "ABENANDO OLIVEIRA DE JESUS", - phone: "(75) 99986-0093", - city: "-", - state: "-", - lastAppointment: "Ainda não houve atendimento", - nextAppointment: "Nenhum atendimento agendado", - isVip: false, - convenio: "particular", - birthday: "1978-12-03", - age: 46, - }, - { - id: 3, - name: "ABDIAS DANTAS DOS SANTOS", - phone: "(75) 99125-7267", - city: "São Cristóvão", - state: "Sergipe", - lastAppointment: "30/12/2024 08:40", - nextAppointment: "Nenhum atendimento agendado", - isVip: true, - convenio: "bradesco", - birthday: "1990-12-03", - age: 34, - }, - { - id: 4, - name: "Abdias Matheus Rodrigues Ferreira", - phone: "(75) 99983-7711", - city: "Pirambu", - state: "Sergipe", - lastAppointment: "04/09/2024 16:20", - nextAppointment: "Nenhum atendimento agendado", - isVip: false, - convenio: "amil", - birthday: "1982-12-03", - age: 42, - }, - { - id: 5, - name: "Abdon Ferreira Guerra", - phone: "(75) 99971-0228", - city: "-", - state: "-", - lastAppointment: "08/05/2025 08:00", - nextAppointment: "Nenhum atendimento agendado", - isVip: false, - convenio: "unimed", - birthday: "1975-12-03", - age: 49, - }, -] +import { Paciente, Endereco, listarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api"; +import { PatientRegistrationForm } from "@/components/forms/patient-registration-form"; + +// Converte qualquer formato que vier do mock para o shape do nosso tipo Paciente +function normalizePaciente(p: any): Paciente { + const endereco: Endereco = { + cep: p.endereco?.cep ?? p.cep ?? "", + logradouro: p.endereco?.logradouro ?? p.logradouro ?? "", + numero: p.endereco?.numero ?? p.numero ?? "", + complemento: p.endereco?.complemento ?? p.complemento ?? "", + bairro: p.endereco?.bairro ?? p.bairro ?? "", + cidade: p.endereco?.cidade ?? p.cidade ?? "", + estado: p.endereco?.estado ?? p.estado ?? "", + }; + + return { + id: String(p.id ?? p.uuid ?? p.paciente_id ?? ""), + nome: p.nome ?? "", + nome_social: p.nome_social ?? null, + cpf: p.cpf ?? "", + rg: p.rg ?? null, + sexo: p.sexo ?? null, + data_nascimento: p.data_nascimento ?? null, + telefone: p.telefone ?? "", + email: p.email ?? "", + endereco, + observacoes: p.observacoes ?? null, + foto_url: p.foto_url ?? null, + }; +} export default function PacientesPage() { - const [searchTerm, setSearchTerm] = useState("") - const [selectedConvenio, setSelectedConvenio] = useState("all") - const [showVipOnly, setShowVipOnly] = useState(false) - const [showBirthdays, setShowBirthdays] = useState(false) - const [showPatientForm, setShowPatientForm] = useState(false) - const [editingPatient, setEditingPatient] = useState(null) - const [advancedFilters, setAdvancedFilters] = useState({ - city: "", - state: "", - minAge: "", - maxAge: "", - lastAppointmentFrom: "", - lastAppointmentTo: "", - }) - const [isAdvancedFilterOpen, setIsAdvancedFilterOpen] = useState(false) + const [patients, setPatients] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const filteredPatients = patients.filter((patient) => { - const matchesSearch = - patient.name.toLowerCase().includes(searchTerm.toLowerCase()) || patient.phone.includes(searchTerm) + const [search, setSearch] = useState(""); + const [showForm, setShowForm] = useState(false); + const [editingId, setEditingId] = useState(null); - const matchesConvenio = selectedConvenio === "all" || patient.convenio === selectedConvenio - const matchesVip = !showVipOnly || patient.isVip - - const currentMonth = new Date().getMonth() + 1 - const patientBirthMonth = new Date(patient.birthday).getMonth() + 1 - const matchesBirthday = !showBirthdays || patientBirthMonth === currentMonth - - const matchesCity = !advancedFilters.city || patient.city.toLowerCase().includes(advancedFilters.city.toLowerCase()) - const matchesState = - !advancedFilters.state || patient.state.toLowerCase().includes(advancedFilters.state.toLowerCase()) - const matchesMinAge = !advancedFilters.minAge || patient.age >= Number.parseInt(advancedFilters.minAge) - const matchesMaxAge = !advancedFilters.maxAge || patient.age <= Number.parseInt(advancedFilters.maxAge) - - return ( - matchesSearch && - matchesConvenio && - matchesVip && - matchesBirthday && - matchesCity && - matchesState && - matchesMinAge && - matchesMaxAge - ) - }) - - const clearAdvancedFilters = () => { - setAdvancedFilters({ - city: "", - state: "", - minAge: "", - maxAge: "", - lastAppointmentFrom: "", - lastAppointmentTo: "", - }) + async function loadAll() { + try { + setLoading(true); + const data = await listarPacientes({ page: 1, limit: 20 }); + setPatients((data ?? []).map(normalizePaciente)); + setError(null); + } catch (e: any) { + setPatients([]); + setError(e?.message || "Erro ao carregar pacientes."); + } finally { + setLoading(false); + } } - const handleViewDetails = (patientId: number) => { - console.log("[v0] Ver detalhes do paciente:", patientId) + useEffect(() => { + loadAll(); + }, []); + + const filtered = useMemo(() => { + if (!search.trim()) return patients; + const q = search.toLowerCase(); + const qDigits = q.replace(/\D/g, ""); + return patients.filter((p) => { + const byName = (p.nome || "").toLowerCase().includes(q); + const byCPF = (p.cpf || "").replace(/\D/g, "").includes(qDigits); + const byId = String(p.id || "").includes(qDigits); + return byName || byCPF || byId; + }); + }, [patients, search]); + + function handleAdd() { + setEditingId(null); + setShowForm(true); } - const handleEditPatient = (patientId: number) => { - console.log("[v0] Editar paciente:", patientId) - setEditingPatient(patientId) - setShowPatientForm(true) + function handleEdit(id: string) { + setEditingId(id); + setShowForm(true); } - const handleScheduleAppointment = (patientId: number) => { - console.log("[v0] Marcar consulta para paciente:", patientId) + async function handleDelete(id: string) { + if (!confirm("Excluir este paciente?")) return; + try { + await excluirPaciente(id); + setPatients((prev) => prev.filter((x) => String(x.id) !== String(id))); + } catch (e: any) { + alert(e?.message || "Não foi possível excluir."); + } } - const handleDeletePatient = (patientId: number) => { - console.log("[v0] Excluir paciente:", patientId) + function handleSaved(p: Paciente) { + const saved = normalizePaciente(p); + setPatients((prev) => { + const i = prev.findIndex((x) => String(x.id) === String(saved.id)); + if (i < 0) return [saved, ...prev]; + const clone = [...prev]; + clone[i] = saved; + return clone; + }); + setShowForm(false); } - const handleAddPatient = () => { - setEditingPatient(null) - setShowPatientForm(true) + async function handleBuscarServidor() { + const q = search.trim(); + if (!q) return loadAll(); + + // Se for apenas números, tentamos como ID no servidor + if (/^\d+$/.test(q)) { + try { + setLoading(true); + const one = await buscarPacientePorId(q); + setPatients(one ? [normalizePaciente(one)] : []); + setError(one ? null : "Paciente não encontrado."); + } catch (e: any) { + setPatients([]); + setError(e?.message || "Paciente não encontrado."); + } finally { + setLoading(false); + } + return; + } + + // Senão, recarrega e filtra local (o mock nem sempre filtra por nome/CPF) + await loadAll(); + setTimeout(() => setSearch(q), 0); } - const handleFormClose = () => { - setShowPatientForm(false) - setEditingPatient(null) - } + if (loading) return

Carregando pacientes...

; + if (error) return

{error}

; - if (showPatientForm) { + if (showForm) { return (
- -
-

- {editingPatient ? "Editar Paciente" : "Cadastrar Novo Paciente"} -

-

- {editingPatient ? "Atualize as informações do paciente" : "Preencha os dados do novo paciente"} -

-
+

{editingId ? "Editar paciente" : "Novo paciente"}

- + setShowForm(false)} + />
- ) + ); } return (
-
+ {/* Cabeçalho + Busca (um único input no padrão do print) */} +
-

Pacientes

-

Gerencie as informações de seus pacientes

-
- -
- -
-
- - setSearchTerm(e.target.value)} - className="pl-10" - /> +

Pacientes

+

Gerencie os pacientes

- - - - - - - - - - - - - Filtros Avançados - - Use os filtros abaixo para refinar sua busca por pacientes específicos. - - -
-
-
- - setAdvancedFilters((prev) => ({ ...prev, city: e.target.value }))} - placeholder="Digite a cidade" - /> -
-
- - setAdvancedFilters((prev) => ({ ...prev, state: e.target.value }))} - placeholder="Digite o estado" - /> -
-
-
-
- - setAdvancedFilters((prev) => ({ ...prev, minAge: e.target.value }))} - placeholder="Ex: 18" - /> -
-
- - setAdvancedFilters((prev) => ({ ...prev, maxAge: e.target.value }))} - placeholder="Ex: 65" - /> -
-
-
- - -
-
-
-
+
+
+ + setSearch(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()} + /> +
+ + +
-
+
Nome + CPF Telefone Cidade Estado - Último atendimento - Próximo atendimento Ações - {filteredPatients.map((patient) => ( - - -
-
- {patient.name.charAt(0).toUpperCase()} -
- - {patient.isVip && ( - - VIP - - )} -
-
- {patient.phone} - {patient.city} - {patient.state} - - - {patient.lastAppointment} - - - - - {patient.nextAppointment} - - - - - - - - - handleViewDetails(patient.id)}> - - Ver detalhes - - handleEditPatient(patient.id)}> - - Editar - - handleScheduleAppointment(patient.id)}> - - Marcar consulta - - handleDeletePatient(patient.id)} className="text-destructive"> - - Excluir - - - + {filtered.length > 0 ? ( + filtered.map((p) => ( + + {p.nome || "(sem nome)"} + {p.cpf || "-"} + {p.telefone || "-"} + {p.endereco?.cidade || "-"} + {p.endereco?.estado || "-"} + + + + + + + alert(JSON.stringify(p, null, 2))}> + + Ver + + handleEdit(String(p.id))}> + + Editar + + handleDelete(String(p.id))} className="text-destructive"> + + Excluir + + + + + + )) + ) : ( + + + Nenhum paciente encontrado - ))} + )}
-
- Mostrando {filteredPatients.length} de {patients.length} pacientes -
+
Mostrando {filtered.length} de {patients.length}
- ) + ); } diff --git a/susconecta/components/dashboard/sidebar.tsx b/susconecta/components/dashboard/sidebar.tsx index f277ea9..08d6159 100644 --- a/susconecta/components/dashboard/sidebar.tsx +++ b/susconecta/components/dashboard/sidebar.tsx @@ -3,12 +3,13 @@ import Link from "next/link" import { usePathname } from "next/navigation" import { cn } from "@/lib/utils" -import { Home, Calendar, Users, UserCheck, FileText, BarChart3, Settings, Stethoscope } from "lucide-react" +import { Home, Calendar, Users, UserCheck, FileText, BarChart3, Settings, Stethoscope, User } from "lucide-react" const navigation = [ { name: "Dashboard", href: "/dashboard", icon: Home }, { name: "Agenda", href: "/dashboard/agenda", icon: Calendar }, { name: "Pacientes", href: "/dashboard/pacientes", icon: Users }, + { name: "Médicos", href: "/dashboard/medicos", icon: User }, { name: "Consultas", href: "/dashboard/consultas", icon: UserCheck }, { name: "Prontuários", href: "/dashboard/prontuarios", icon: FileText }, { name: "Relatórios", href: "/dashboard/relatorios", icon: BarChart3 }, diff --git a/susconecta/components/forms/patient-registration-form.tsx b/susconecta/components/forms/patient-registration-form.tsx index c54bf65..0263a23 100644 --- a/susconecta/components/forms/patient-registration-form.tsx +++ b/susconecta/components/forms/patient-registration-form.tsx @@ -1,1676 +1,616 @@ -"use client" +/* src/components/forms/patient-registration-form.tsx */ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { AlertCircle, ChevronDown, ChevronUp, FileImage, Loader2, Save, Upload, User, X, XCircle, Trash2 } from "lucide-react"; -import type React from "react" -import { useState, useEffect } from "react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Textarea } from "@/components/ui/textarea" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" -import { Checkbox } from "@/components/ui/checkbox" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { salvarPaciente } from "@/lib/api"; import { - Upload, - ChevronDown, - ChevronUp, - X, - FileText, - User, - Phone, - MapPin, - FileImage, - Save, - XCircle, - AlertCircle, - Loader2, -} from "lucide-react" + Paciente, + PacienteInput, + buscarCepAPI, + validarCPF, + criarPaciente, + atualizarPaciente, + uploadFotoPaciente, + removerFotoPaciente, + adicionarAnexo, + listarAnexos, + removerAnexo, + buscarPacientePorId, +} from "@/lib/api"; +type Mode = "create" | "edit"; -interface PatientFormData { - // Dados pessoais - photo: File | null - nome: string - nomeSocial: string - cpf: string - rg: string - outroDocumento: string - numeroDocumento: string - sexo: string - dataNascimento: string - etnia: string - raca: string - naturalidade: string - nacionalidade: string - profissao: string - estadoCivil: string - nomeMae: string - profissaoMae: string - nomePai: string - profissaoPai: string - nomeResponsavel: string - cpfResponsavel: string - nomeEsposo: string - rnGuiaConvenio: boolean - codigoLegado: string - - // Observações e anexos - observacoes: string - anexos: File[] - - // Contato - email: string - celular: string - telefone1: string - telefone2: string - - // Endereço - cep: string - logradouro: string - numero: string - complemento: string - bairro: string - cidade: string - estado: string - referencia: string +export interface PatientRegistrationFormProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; + patientId?: number | null; + inline?: boolean; + mode?: Mode; + onSaved?: (paciente: Paciente) => void; + onClose?: () => void; } -interface PatientRegistrationFormProps { - open?: boolean - onOpenChange?: (open: boolean) => void - patientData?: PatientFormData | null - patientId?: number | null - mode?: "create" | "edit" - onClose?: () => void - inline?: boolean -} +type FormData = { + photo: File | null; + nome: string; + nome_social: string; + cpf: string; + rg: string; + sexo: string; + data_nascimento: string; + email: string; + telefone: string; + cep: string; + logradouro: string; + numero: string; + complemento: string; + bairro: string; + cidade: string; + estado: string; + observacoes: string; + anexos: File[]; +}; + +const initial: FormData = { + photo: null, + nome: "", + nome_social: "", + cpf: "", + rg: "", + sexo: "", + data_nascimento: "", + email: "", + telefone: "", + cep: "", + logradouro: "", + numero: "", + complemento: "", + bairro: "", + cidade: "", + estado: "", + observacoes: "", + anexos: [], +}; export function PatientRegistrationForm({ open = true, onOpenChange, - patientData = null, patientId = null, - mode = "create", - onClose, inline = false, + mode = "create", + onSaved, + onClose, }: PatientRegistrationFormProps) { - const initialFormData: PatientFormData = { - photo: null, - nome: "", - nomeSocial: "", - cpf: "", - rg: "", - outroDocumento: "", - numeroDocumento: "", - sexo: "", - dataNascimento: "", - etnia: "", - raca: "", - naturalidade: "", - nacionalidade: "Brasileira", - profissao: "", - estadoCivil: "", - nomeMae: "", - profissaoMae: "", - nomePai: "", - profissaoPai: "", - nomeResponsavel: "", - cpfResponsavel: "", - nomeEsposo: "", - rnGuiaConvenio: false, - codigoLegado: "", - observacoes: "", - anexos: [], - email: "", - celular: "", - telefone1: "", - telefone2: "", - cep: "", - logradouro: "", - numero: "", - complemento: "", - bairro: "", - cidade: "", - estado: "", - referencia: "", - } + const [form, setForm] = useState(initial); + const [errors, setErrors] = useState>({}); + const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false }); + const [isSubmitting, setSubmitting] = useState(false); + const [isSearchingCEP, setSearchingCEP] = useState(false); + const [photoPreview, setPhotoPreview] = useState(null); + const [serverAnexos, setServerAnexos] = useState([]); - const [formData, setFormData] = useState(patientData || initialFormData) - const [expandedSections, setExpandedSections] = useState({ - dadosPessoais: true, - observacoes: false, - contato: false, - endereco: false, - }) - - const [photoPreview, setPhotoPreview] = useState(null) - const [isLoadingCep, setIsLoadingCep] = useState(false) - const [errors, setErrors] = useState>({}) - const [isSubmitting, setIsSubmitting] = useState(false) + const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]); + // Edição useEffect(() => { - if (patientId && mode === "edit") { - // TODO: Fetch patient data by ID - console.log("[v0] Loading patient data for ID:", patientId) - // For now, use mock data or existing patientData - if (patientData) { - setFormData(patientData) - if (patientData.photo) { - const reader = new FileReader() - reader.onload = (e) => setPhotoPreview(e.target?.result as string) - reader.readAsDataURL(patientData.photo) - } + async function load() { + if (mode !== "edit" || patientId == null) return; + try { + const p = await buscarPacientePorId(String(patientId)); + setForm((s) => ({ + ...s, + nome: p.nome || "", + nome_social: p.nome_social || "", + cpf: p.cpf || "", + rg: p.rg || "", + sexo: p.sexo || "", + data_nascimento: (p.data_nascimento as string) || "", + telefone: p.telefone || "", + email: p.email || "", + cep: p.endereco?.cep || "", + logradouro: p.endereco?.logradouro || "", + numero: p.endereco?.numero || "", + complemento: p.endereco?.complemento || "", + bairro: p.endereco?.bairro || "", + cidade: p.endereco?.cidade || "", + estado: p.endereco?.estado || "", + observacoes: p.observacoes || "", + })); + const ax = await listarAnexos(String(patientId)).catch(() => []); + setServerAnexos(Array.isArray(ax) ? ax : []); + } catch { + // ignora } - } else if (mode === "create") { - setFormData(initialFormData) - setPhotoPreview(null) } - }, [patientId, patientData, mode]) + load(); + }, [mode, patientId]); - const toggleSection = (section: keyof typeof expandedSections) => { - setExpandedSections((prev) => ({ - ...prev, - [section]: !prev[section], - })) + function setField(k: T, v: FormData[T]) { + setForm((s) => ({ ...s, [k]: v })); + if (errors[k as string]) setErrors((e) => ({ ...e, [k]: "" })); } - const handleInputChange = (field: keyof PatientFormData, value: string | boolean | File | File[]) => { - setFormData((prev) => ({ - ...prev, - [field]: value, - })) - - // Clear error when user starts typing - if (errors[field]) { - setErrors((prev) => { - const newErrors = { ...prev } - delete newErrors[field] - return newErrors - }) - } + function formatCPF(v: string) { + const n = v.replace(/\D/g, "").slice(0, 11); + return n.replace(/(\d{3})(\d{3})(\d{3})(\d{0,2})/, (_, a, b, c, d) => `${a}.${b}.${c}${d ? "-" + d : ""}`); + } + function handleCPFChange(v: string) { + setField("cpf", formatCPF(v)); } - const formatCPF = (value: string) => { - const numbers = value.replace(/\D/g, "") - return numbers.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4") + function formatCEP(v: string) { + const n = v.replace(/\D/g, "").slice(0, 8); + return n.replace(/(\d{5})(\d{0,3})/, (_, a, b) => `${a}${b ? "-" + b : ""}`); } - - const handleCPFChange = (value: string) => { - const formatted = formatCPF(value) - handleInputChange("cpf", formatted) - } - - const formatPhone = (value: string) => { - const numbers = value.replace(/\D/g, "") - if (numbers.length <= 10) { - return numbers.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3") - } - return numbers.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3") - } - - const formatCellPhone = (value: string) => { - const numbers = value.replace(/\D/g, "") - return numbers.replace(/(\d{2})(\d{5})(\d{4})/, "+55 ($1) $2-$3") - } - - const formatCEP = (value: string) => { - const numbers = value.replace(/\D/g, "") - return numbers.replace(/(\d{5})(\d{3})/, "$1-$2") - } - - const handlePhotoUpload = (event: React.ChangeEvent) => { - const file = event.target.files?.[0] - if (file) { - if (file.size > 5 * 1024 * 1024) { - // 5MB limit - setErrors((prev) => ({ ...prev, photo: "Arquivo muito grande. Máximo 5MB." })) - return - } - - handleInputChange("photo", file) - const reader = new FileReader() - reader.onload = (e) => { - setPhotoPreview(e.target?.result as string) - } - reader.readAsDataURL(file) - } - } - - const handleAnexoUpload = (event: React.ChangeEvent) => { - const files = Array.from(event.target.files || []) - - const validFiles = files.filter((file) => { - if (file.size > 10 * 1024 * 1024) { - // 10MB limit per file - setErrors((prev) => ({ ...prev, anexos: `Arquivo ${file.name} muito grande. Máximo 10MB por arquivo.` })) - return false - } - return true - }) - - handleInputChange("anexos", [...formData.anexos, ...validFiles]) - } - - const removeAnexo = (index: number) => { - const newAnexos = formData.anexos.filter((_, i) => i !== index) - handleInputChange("anexos", newAnexos) - } - - const validateCPF = (cpf: string) => { - const cleanCPF = cpf.replace(/\D/g, "") - if (cleanCPF.length !== 11) return false - - // Check for known invalid CPFs - if (/^(\d)\1{10}$/.test(cleanCPF)) return false - - let sum = 0 - for (let i = 0; i < 9; i++) { - sum += Number.parseInt(cleanCPF.charAt(i)) * (10 - i) - } - let remainder = (sum * 10) % 11 - if (remainder === 10 || remainder === 11) remainder = 0 - if (remainder !== Number.parseInt(cleanCPF.charAt(9))) return false - - sum = 0 - for (let i = 0; i < 10; i++) { - sum += Number.parseInt(cleanCPF.charAt(i)) * (11 - i) - } - remainder = (sum * 10) % 11 - if (remainder === 10 || remainder === 11) remainder = 0 - return remainder === Number.parseInt(cleanCPF.charAt(10)) - } - - const searchCEP = async (cep: string) => { - const cleanCEP = cep.replace(/\D/g, "") - if (cleanCEP.length !== 8) return - - setIsLoadingCep(true) + async function fillFromCEP(cep: string) { + const clean = cep.replace(/\D/g, ""); + if (clean.length !== 8) return; + setSearchingCEP(true); try { - const response = await fetch(`https://viacep.com.br/ws/${cleanCEP}/json/`) - const data = await response.json() - - if (data.erro) { - setErrors((prev) => ({ ...prev, cep: "CEP não encontrado" })) + const res = await buscarCepAPI(clean); + if (res?.erro) { + setErrors((e) => ({ ...e, cep: "CEP não encontrado" })); } else { - handleInputChange("logradouro", data.logradouro || "") - handleInputChange("bairro", data.bairro || "") - handleInputChange("cidade", data.localidade || "") - handleInputChange("estado", data.uf || "") - - // Clear CEP error if successful - if (errors.cep) { - setErrors((prev) => { - const newErrors = { ...prev } - delete newErrors.cep - return newErrors - }) - } + setField("logradouro", res.logradouro ?? ""); + setField("bairro", res.bairro ?? ""); + setField("cidade", res.localidade ?? ""); + setField("estado", res.uf ?? ""); } - } catch (error) { - console.error("Erro ao buscar CEP:", error) - setErrors((prev) => ({ ...prev, cep: "Erro ao buscar CEP. Tente novamente." })) + } catch { + setErrors((e) => ({ ...e, cep: "Erro ao buscar CEP" })); } finally { - setIsLoadingCep(false) + setSearchingCEP(false); } } - const validateForm = () => { - const newErrors: Record = {} - - // Required fields - if (!formData.nome.trim()) { - newErrors.nome = "Nome é obrigatório" - } - - // CPF validation - if (formData.cpf && !validateCPF(formData.cpf)) { - newErrors.cpf = "CPF inválido" - } - - // Email validation - if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { - newErrors.email = "E-mail inválido" - } - - // Responsible CPF validation - if (formData.cpfResponsavel && !validateCPF(formData.cpfResponsavel)) { - newErrors.cpfResponsavel = "CPF do responsável inválido" - } - - // Date validation - if (formData.dataNascimento) { - const birthDate = new Date(formData.dataNascimento) - const today = new Date() - if (birthDate > today) { - newErrors.dataNascimento = "Data de nascimento não pode ser futura" - } - } - - setErrors(newErrors) - return Object.keys(newErrors).length === 0 + function validateLocal(): boolean { + const e: Record = {}; + if (!form.nome.trim()) e.nome = "Nome é obrigatório"; + if (!form.cpf.trim()) e.cpf = "CPF é obrigatório"; + setErrors(e); + return Object.keys(e).length === 0; } - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault() + function toPayload(): PacienteInput { + return { + nome: form.nome, + nome_social: form.nome_social || null, + cpf: form.cpf, + rg: form.rg || null, + sexo: form.sexo || null, + data_nascimento: form.data_nascimento || null, + telefone: form.telefone || null, + email: form.email || null, + endereco: { + cep: form.cep || null, + logradouro: form.logradouro || null, + numero: form.numero || null, + complemento: form.complemento || null, + bairro: form.bairro || null, + cidade: form.cidade || null, + estado: form.estado || null, + }, + observacoes: form.observacoes || null, + }; + } - if (!validateForm()) { - // Expand sections with errors - const errorFields = Object.keys(errors) - if (errorFields.some((field) => ["nome", "cpf", "rg", "sexo", "dataNascimento"].includes(field))) { - setExpandedSections((prev) => ({ ...prev, dadosPessoais: true })) - } - if (errorFields.some((field) => ["email", "celular"].includes(field))) { - setExpandedSections((prev) => ({ ...prev, contato: true })) - } - if (errorFields.some((field) => ["cep", "logradouro"].includes(field))) { - setExpandedSections((prev) => ({ ...prev, endereco: true })) - } - return - } - - setIsSubmitting(true) + async function handleSubmit(ev: React.FormEvent) { + ev.preventDefault(); + if (!validateLocal()) return; + // validação externa do CPF (mock → pode falhar, tratamos erro legível) try { - console.log("[v0] Saving patient data:", formData) - console.log("[v0] Mode:", mode) - console.log("[v0] Patient ID:", patientId) + const { valido, existe } = await validarCPF(form.cpf); + if (!valido) { + setErrors((e) => ({ ...e, cpf: "CPF inválido (validação externa)" })); + return; + } + if (existe && mode === "create") { + setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" })); + return; + } + } catch { + // se o mock der 404/timeout, seguimos sem bloquear + } - // Simulate network delay - await salvarPaciente(formData) + setSubmitting(true); + try { + const payload = toPayload(); - // TODO: Implement actual API call - // const response = await fetch('/api/patients', { - // method: mode === 'create' ? 'POST' : 'PUT', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify(formData) - // }) - - // Reset form if creating new patient + let saved: Paciente; if (mode === "create") { - setFormData(initialFormData) - setPhotoPreview(null) - } - - if (inline && onClose) { - onClose() + saved = await criarPaciente(payload); } else { - onOpenChange?.(false) + if (patientId == null) throw new Error("Paciente inexistente para edição"); + saved = await atualizarPaciente(String(patientId), payload); } - // Show success message (you might want to use a toast notification) - alert(mode === "create" ? "Paciente cadastrado com sucesso!" : "Paciente atualizado com sucesso!") - } catch (error) { - console.error("Erro ao salvar paciente:", error) - setErrors({ submit: "Erro ao salvar paciente. Tente novamente." }) + if (form.photo && saved?.id) { + try { + await uploadFotoPaciente(saved.id, form.photo); + } catch {} + } + + if (form.anexos.length && saved?.id) { + for (const f of form.anexos) { + try { + await adicionarAnexo(saved.id, f); + } catch {} + } + } + + onSaved?.(saved); + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + + if (inline) onClose?.(); + else onOpenChange?.(false); + + alert(mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!"); + } catch (err: any) { + setErrors({ submit: err?.message || "Erro ao salvar paciente." }); } finally { - setIsSubmitting(false) + setSubmitting(false); } } - const handleCancel = () => { - if (inline && onClose) { - onClose() - } else { - onOpenChange?.(false) + function handlePhoto(e: React.ChangeEvent) { + const f = e.target.files?.[0]; + if (!f) return; + if (f.size > 5 * 1024 * 1024) { + setErrors((e) => ({ ...e, photo: "Arquivo muito grande. Máx 5MB." })); + return; + } + setField("photo", f); + const fr = new FileReader(); + fr.onload = (ev) => setPhotoPreview(String(ev.target?.result || "")); + fr.readAsDataURL(f); + } + + function addLocalAnexos(e: React.ChangeEvent) { + const fs = Array.from(e.target.files || []); + setField("anexos", [...form.anexos, ...fs]); + } + function removeLocalAnexo(idx: number) { + const clone = [...form.anexos]; + clone.splice(idx, 1); + setField("anexos", clone); + } + + async function handleRemoverFotoServidor() { + if (mode !== "edit" || !patientId) return; + try { + await removerFotoPaciente(String(patientId)); + alert("Foto removida."); + } catch (e: any) { + alert(e?.message || "Não foi possível remover a foto."); } } - if (inline) { - return ( -
- {errors.submit && ( - - - {errors.submit} - - )} + async function handleRemoverAnexoServidor(anexoId: string | number) { + if (mode !== "edit" || !patientId) return; + try { + await removerAnexo(String(patientId), anexoId); + setServerAnexos((prev) => prev.filter((a) => String(a.id ?? a.anexo_id) !== String(anexoId))); + } catch (e: any) { + alert(e?.message || "Não foi possível remover o anexo."); + } + } -
- {/* Dados Pessoais */} - toggleSection("dadosPessoais")}> - - - - - - - Dados Pessoais - - {expandedSections.dadosPessoais ? ( - + const content = ( + <> + {errors.submit && ( + + + {errors.submit} + + )} + + + {/* DADOS PESSOAIS */} + setExpanded((s) => ({ ...s, dados: !s.dados }))}> + + + + + + + Dados Pessoais + + {expanded.dados ? : } + + + + + +
+
+ {photoPreview ? ( + // eslint-disable-next-line @next/next/no-img-element + Preview ) : ( - + )} - - - - - - {/* Foto */} -
-
- {photoPreview ? ( - Preview - ) : ( - - )} -
-
- - - {errors.photo &&

{errors.photo}

} -

Máximo 5MB

-
- - {/* Nome e Nome Social */} -
-
- - handleInputChange("nome", e.target.value)} - className={errors.nome ? "border-destructive" : ""} - /> - {errors.nome &&

{errors.nome}

} -
-
- - handleInputChange("nomeSocial", e.target.value)} - /> -
-
- - {/* CPF e RG */} -
-
- - handleCPFChange(e.target.value)} - placeholder="000.000.000-00" - maxLength={14} - className={errors.cpf ? "border-destructive" : ""} - /> - {errors.cpf &&

{errors.cpf}

} -
-
- - handleInputChange("rg", e.target.value)} /> -
-
- - {/* Outros Documentos */} -
-
- - -
-
- - handleInputChange("numeroDocumento", e.target.value)} - disabled={!formData.outroDocumento} - /> -
-
- - {/* Sexo e Data de Nascimento */} -
-
- - handleInputChange("sexo", value)}> -
- - -
-
- - -
-
- - -
-
-
-
- - handleInputChange("dataNascimento", e.target.value)} - className={errors.dataNascimento ? "border-destructive" : ""} - /> - {errors.dataNascimento &&

{errors.dataNascimento}

} -
-
- - {/* Etnia e Raça */} -
-
- - -
-
- - handleInputChange("raca", e.target.value)} - placeholder="Opcional" - /> -
-
- - {/* Naturalidade e Nacionalidade */} -
-
- - handleInputChange("naturalidade", e.target.value)} - placeholder="Cidade de nascimento" - /> -
-
- - handleInputChange("nacionalidade", e.target.value)} - /> -
-
- - {/* Profissão e Estado Civil */} -
-
- - handleInputChange("profissao", e.target.value)} - /> -
-
- - -
-
- - {/* Dados dos Pais */} -
-
- - handleInputChange("nomeMae", e.target.value)} - /> -
-
- - handleInputChange("profissaoMae", e.target.value)} - /> -
-
- -
-
- - handleInputChange("nomePai", e.target.value)} - /> -
-
- - handleInputChange("profissaoPai", e.target.value)} - /> -
-
- - {/* Responsável */} -
-
- - handleInputChange("nomeResponsavel", e.target.value)} - placeholder="Para menores ou dependentes" - /> -
-
- - handleCPFChange(e.target.value)} - placeholder="000.000.000-00" - maxLength={14} - className={errors.cpfResponsavel ? "border-destructive" : ""} - /> - {errors.cpfResponsavel &&

{errors.cpfResponsavel}

} -
-
- - {/* Esposo e Configurações */} -
-
- - handleInputChange("nomeEsposo", e.target.value)} - /> -
-
- - handleInputChange("codigoLegado", e.target.value)} - placeholder="ID de outro sistema" - /> -
-
- - {/* RN na Guia do Convênio */} -
- handleInputChange("rnGuiaConvenio", checked as boolean)} - /> - -
-
-
- - - - {/* Observações e Anexos */} - toggleSection("observacoes")}> - - - - - - - Observações e Anexos - - {expandedSections.observacoes ? ( - - ) : ( - +
+ + + {mode === "edit" && ( + )} - - - - - + {errors.photo &&

{errors.photo}

} +

Máximo 5MB

+
+
+ +
- -