From 0c52acdc9ce2b0f5e9ea14f18bcb32cc3f9202e5 Mon Sep 17 00:00:00 2001 From: M-Gabrielly Date: Sun, 14 Sep 2025 19:39:16 -0300 Subject: [PATCH] feat: add doctor register --- .../app/dashboard/medicos/novo/page.tsx | 25 + susconecta/app/dashboard/medicos/page.tsx | 63 +++ susconecta/app/profissional/page.tsx | 21 +- .../forms/doctor-registration-form.tsx | 499 ++++++++++++++++++ 4 files changed, 606 insertions(+), 2 deletions(-) create mode 100644 susconecta/app/dashboard/medicos/novo/page.tsx create mode 100644 susconecta/app/dashboard/medicos/page.tsx create mode 100644 susconecta/components/forms/doctor-registration-form.tsx diff --git a/susconecta/app/dashboard/medicos/novo/page.tsx b/susconecta/app/dashboard/medicos/novo/page.tsx new file mode 100644 index 0000000..cfed08d --- /dev/null +++ b/susconecta/app/dashboard/medicos/novo/page.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { DoctorRegistrationForm } from '@/components/forms/doctor-registration-form'; + +export default function NovoMedicoPage() { + return ( +
+
+

Novo médico

+
+ + { + console.log("Saved", p); + }} + onClose={() => { + // In a real app, you'd likely redirect + window.history.back(); + }} + /> +
+ ); +} diff --git a/susconecta/app/dashboard/medicos/page.tsx b/susconecta/app/dashboard/medicos/page.tsx new file mode 100644 index 0000000..9cf2c4c --- /dev/null +++ b/susconecta/app/dashboard/medicos/page.tsx @@ -0,0 +1,63 @@ + +"use client"; + +import React from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Plus } from "lucide-react"; + +const medicos = [ + { + id: 1, + nome: "Dr. Carlos Andrade", + especialidade: "Cardiologia", + crm: "123456/SP", + }, + { + id: 2, + nome: "Dra. Ana Souza", + especialidade: "Pediatria", + crm: "654321/RJ", + }, +]; + +export default function MedicosPage() { + return ( +
+
+
+

Médicos

+

Gerencie os médicos da sua clínica

+
+ + + +
+ +
+ + + + Nome + Especialidade + CRM + + + + {medicos.map((medico) => ( + + {medico.nome} + {medico.especialidade} + {medico.crm} + + ))} + +
+
+
+ ); +} diff --git a/susconecta/app/profissional/page.tsx b/susconecta/app/profissional/page.tsx index 6211e23..f55b558 100644 --- a/susconecta/app/profissional/page.tsx +++ b/susconecta/app/profissional/page.tsx @@ -30,6 +30,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form"; import dynamic from "next/dynamic"; @@ -70,6 +71,7 @@ const ProfissionalPage = () => { const [editingEvent, setEditingEvent] = useState(null); const [showPopup, setShowPopup] = useState(false); const [showActionModal, setShowActionModal] = useState(false); + const [showDoctorModal, setShowDoctorModal] = useState(false); const [step, setStep] = useState(1); const [newEvent, setNewEvent] = useState({ title: "", @@ -294,9 +296,9 @@ const ProfissionalPage = () => { Comunicação - @@ -752,6 +754,21 @@ const ProfissionalPage = () => { )} + { + setShowDoctorModal(false); + }} + /> + )} + { + setShowDoctorModal(false); + }} + /> ); diff --git a/susconecta/components/forms/doctor-registration-form.tsx b/susconecta/components/forms/doctor-registration-form.tsx new file mode 100644 index 0000000..ce8cbf0 --- /dev/null +++ b/susconecta/components/forms/doctor-registration-form.tsx @@ -0,0 +1,499 @@ +"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"; + +// Mock data and types since API is not used for now +export type Medico = { + id: string; + nome?: string; + nome_social?: string | null; + cpf?: string; + rg?: string | null; + sexo?: string | null; + data_nascimento?: string | null; + telefone?: string; + email?: string; + crm?: string; + especialidade?: string; + observacoes?: string | null; + foto_url?: string | null; +}; + +type Mode = "create" | "edit"; + +export interface DoctorRegistrationFormProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; + doctorId?: number | null; + inline?: boolean; + mode?: Mode; + onSaved?: (medico: Medico) => void; + onClose?: () => void; +} + +type FormData = { + photo: File | null; + nome: string; + nome_social: string; + crm: string; + especialidade: 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: "", + crm: "", + especialidade: "", + cpf: "", + rg: "", + sexo: "", + data_nascimento: "", + email: "", + telefone: "", + cep: "", + logradouro: "", + numero: "", + complemento: "", + bairro: "", + cidade: "", + estado: "", + observacoes: "", + anexos: [], +}; + +export function DoctorRegistrationForm({ + open = true, + onOpenChange, + doctorId = null, + inline = false, + mode = "create", + onSaved, + onClose, +}: DoctorRegistrationFormProps) { + 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 title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]); + + useEffect(() => { + // Data loading logic would go here in a real scenario + if (mode === "edit" && doctorId) { + console.log("Loading doctor data for ID:", doctorId); + // Example: setForm(loadedDoctorData); + } + }, [mode, doctorId]); + + function setField(k: T, v: FormData[T]) { + setForm((s) => ({ ...s, [k]: v })); + if (errors[k as string]) setErrors((e) => ({ ...e, [k]: "" })); + } + + 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)); + } + + 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 : ""}`); + } + async function fillFromCEP(cep: string) { + const clean = cep.replace(/\D/g, ""); + if (clean.length !== 8) return; + setSearchingCEP(true); + try { + // Mocking API call + console.log("Searching CEP:", clean); + // In a real app: const res = await buscarCepAPI(clean); + // Mock response: + const res = { logradouro: "Rua Fictícia", bairro: "Bairro dos Sonhos", localidade: "Cidade Exemplo", uf: "EX" }; + if (res) { + setField("logradouro", res.logradouro ?? ""); + setField("bairro", res.bairro ?? ""); + setField("cidade", res.localidade ?? ""); + setField("estado", res.uf ?? ""); + } + } catch { + setErrors((e) => ({ ...e, cep: "Erro ao buscar CEP" })); + } finally { + setSearchingCEP(false); + } + } + + function validateLocal(): boolean { + const e: Record = {}; + if (!form.nome.trim()) e.nome = "Nome é obrigatório"; + if (!form.cpf.trim()) e.cpf = "CPF é obrigatório"; + if (!form.crm.trim()) e.crm = "CRM é obrigatório"; + if (!form.especialidade.trim()) e.especialidade = "Especialidade é obrigatória"; + setErrors(e); + return Object.keys(e).length === 0; + } + + async function handleSubmit(ev: React.FormEvent) { + ev.preventDefault(); + if (!validateLocal()) return; + + setSubmitting(true); + console.log("Submitting form with data:", form); + + // Simulate API call + setTimeout(() => { + setSubmitting(false); + const savedData: Medico = { + id: doctorId ? String(doctorId) : String(Date.now()), + ...form, + }; + onSaved?.(savedData); + alert(mode === "create" ? "Médico cadastrado com sucesso! (simulado)" : "Médico atualizado com sucesso! (simulado)"); + if (inline) onClose?.(); + else onOpenChange?.(false); + }, 1000); + } + + 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); + } + + const content = ( + <> + {errors.submit && ( + + + {errors.submit} + + )} + +
+ setExpanded((s) => ({ ...s, dados: !s.dados }))}> + + + + + + + Dados Pessoais e Profissionais + + {expanded.dados ? : } + + + + + +
+
+ {photoPreview ? ( + Preview + ) : ( + + )} +
+
+ + + {errors.photo &&

{errors.photo}

} +

Máximo 5MB

+
+
+ +
+
+ + setField("nome", e.target.value)} className={errors.nome ? "border-destructive" : ""} /> + {errors.nome &&

{errors.nome}

} +
+
+ + setField("nome_social", e.target.value)} /> +
+
+ +
+
+ + setField("crm", e.target.value)} className={errors.crm ? "border-destructive" : ""} /> + {errors.crm &&

{errors.crm}

} +
+
+ + setField("especialidade", e.target.value)} className={errors.especialidade ? "border-destructive" : ""} /> + {errors.especialidade &&

{errors.especialidade}

} +
+
+ +
+
+ + handleCPFChange(e.target.value)} + placeholder="000.000.000-00" + maxLength={14} + className={errors.cpf ? "border-destructive" : ""} + /> + {errors.cpf &&

{errors.cpf}

} +
+
+ + setField("rg", e.target.value)} /> +
+
+ +
+
+ + setField("sexo", v)}> +
+ + +
+
+ + +
+
+ + +
+
+
+
+ + setField("data_nascimento", e.target.value)} /> +
+
+
+
+
+
+ + setExpanded((s) => ({ ...s, contato: !s.contato }))}> + + + + + Contato + {expanded.contato ? : } + + + + + +
+
+ + setField("email", e.target.value)} /> +
+
+ + setField("telefone", e.target.value)} /> +
+
+
+
+
+
+ + setExpanded((s) => ({ ...s, endereco: !s.endereco }))}> + + + + + Endereço + {expanded.endereco ? : } + + + + + +
+
+ +
+ { + const v = formatCEP(e.target.value); + setField("cep", v); + if (v.replace(/\D/g, "").length === 8) fillFromCEP(v); + }} + placeholder="00000-000" + maxLength={9} + disabled={isSearchingCEP} + className={errors.cep ? "border-destructive" : ""} + /> + {isSearchingCEP && } +
+ {errors.cep &&

{errors.cep}

} +
+
+ + setField("logradouro", e.target.value)} /> +
+
+ + setField("numero", e.target.value)} /> +
+
+ +
+
+ + setField("complemento", e.target.value)} /> +
+
+ + setField("bairro", e.target.value)} /> +
+
+ +
+
+ + setField("cidade", e.target.value)} /> +
+
+ + setField("estado", e.target.value)} placeholder="UF" /> +
+
+
+
+
+
+ + setExpanded((s) => ({ ...s, obs: !s.obs }))}> + + + + + Observações e Anexos + {expanded.obs ? : } + + + + + +
+ +