diff --git a/susconecta/app/dashboard/doutores/page.tsx b/susconecta/app/dashboard/doutores/page.tsx new file mode 100644 index 0000000..8a8944f --- /dev/null +++ b/susconecta/app/dashboard/doutores/page.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { 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, Edit, Trash2, ArrowLeft, Eye } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { DoctorRegistrationForm, Medico } from "@/components/forms/doctor-registration-form"; + +// Mock data for doctors +const initialDoctors: Medico[] = [ + { + id: "1", + nome: "Dr. João Silva", + especialidade: "Cardiologia", + crm: "12345-SP", + email: "joao.silva@example.com", + telefone: "(11) 99999-1234", + }, + { + id: "2", + nome: "Dra. Maria Oliveira", + especialidade: "Pediatria", + crm: "54321-RJ", + email: "maria.oliveira@example.com", + telefone: "(21) 98888-5678", + }, +]; + +export default function DoutoresPage() { + const [doctors, setDoctors] = useState(initialDoctors); + const [search, setSearch] = useState(""); + const [showForm, setShowForm] = useState(false); + const [editingId, setEditingId] = useState(null); + + const filtered = useMemo(() => { + if (!search.trim()) return doctors; + const q = search.toLowerCase(); + return doctors.filter((d) => { + const byName = (d.nome || "").toLowerCase().includes(q); + const byCrm = (d.crm || "").toLowerCase().includes(q); + const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q); + return byName || byCrm || byEspecialidade; + }); + }, [doctors, search]); + + function handleAdd() { + setEditingId(null); + setShowForm(true); + } + + function handleEdit(id: string) { + setEditingId(id); + setShowForm(true); + } + + function handleDelete(id: string) { + if (!confirm("Excluir este médico?")) return; + setDoctors((prev) => prev.filter((x) => String(x.id) !== String(id))); + } + + function handleSaved(medico: Medico) { + const saved = medico; + setDoctors((prev) => { + // Se não houver ID, é um novo médico + if (!saved.id) { + return [{ ...saved, id: String(Date.now()) }, ...prev]; + } + // Se houver ID, é uma edição + const i = prev.findIndex((x) => String(x.id) === String(saved.id)); + if (i < 0) return [{ ...saved, id: String(Date.now()) }, ...prev]; // Caso não encontre, adiciona + const clone = [...prev]; + clone[i] = saved; + return clone; + }); + setShowForm(false); + } + + if (showForm) { + return ( +
+
+ +

{editingId ? "Editar Médico" : "Novo Médico"}

+
+ + setShowForm(false)} + /> +
+ ); + } + + return ( +
+
+
+

Médicos

+

Gerencie os médicos da sua clínica

+
+ +
+
+ + setSearch(e.target.value)} + /> +
+ +
+
+ +
+ + + + Nome + Especialidade + CRM + Contato + Ações + + + + {filtered.length > 0 ? ( + filtered.map((doctor) => ( + + {doctor.nome} + + {doctor.especialidade} + + {doctor.crm} + +
+ {doctor.email} + {doctor.telefone} +
+
+ + + + + + + alert(JSON.stringify(doctor, null, 2))}> + + Ver + + handleEdit(String(doctor.id))}> + + Editar + + handleDelete(String(doctor.id))} className="text-destructive"> + + Excluir + + + + +
+ )) + ) : ( + + + Nenhum médico encontrado + + + )} +
+
+
+
Mostrando {filtered.length} de {doctors.length}
+
+ ); +} \ No newline at end of file diff --git a/susconecta/app/globals.css b/susconecta/app/globals.css index 3c112c3..817775e 100644 --- a/susconecta/app/globals.css +++ b/susconecta/app/globals.css @@ -6,7 +6,7 @@ :root { --background: #ffffff; --foreground: #475569; - --card: #f8fafc; + --card: #ffffff; --card-foreground: #334155; --popover: #ffffff; --popover-foreground: #475569; diff --git a/susconecta/components/dashboard/sidebar.tsx b/susconecta/components/dashboard/sidebar.tsx index bf291ad..8d0d1d3 100644 --- a/susconecta/components/dashboard/sidebar.tsx +++ b/susconecta/components/dashboard/sidebar.tsx @@ -33,7 +33,7 @@ const navigation = [ { name: "Dashboard", href: "/dashboard", icon: Home }, { name: "Calendario", href: "/calendar", icon: Calendar }, { name: "Pacientes", href: "/dashboard/pacientes", icon: Users }, - { name: "Médicos", href: "/dashboard/medicos", icon: User }, + { name: "Médicos", href: "/dashboard/doutores", 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/doctor-registration-form.tsx b/susconecta/components/forms/doctor-registration-form.tsx new file mode 100644 index 0000000..a7d6396 --- /dev/null +++ b/susconecta/components/forms/doctor-registration-form.tsx @@ -0,0 +1,808 @@ +"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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +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 { Checkbox } from "@/components/ui/checkbox"; +import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; + +// Mock data and types since API is not used for now + +type FormacaoAcademica = { + instituicao: string; + curso: string; + ano_conclusao: string; +}; + +type DadosBancarios = { + banco: string; + agencia: string; + conta: string; + tipo_conta: string; +}; + +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; + celular?: string; + contato_emergencia?: string; + email?: string; + crm?: string; + estado_crm?: string; + rqe?: string; + formacao_academica?: FormacaoAcademica[]; + curriculo_url?: string | null; + especialidade?: string; + observacoes?: string | null; + foto_url?: string | null; + tipo_vinculo?: string; + dados_bancarios?: DadosBancarios; + + agenda_horario?: string; + valor_consulta?: number | string; +}; + +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; + estado_crm: string; + rqe: string; + formacao_academica: FormacaoAcademica[]; + curriculo: File | null; + especialidade: string; + cpf: string; + rg: string; + sexo: string; + data_nascimento: string; + email: string; + telefone: string; + celular: string; + contato_emergencia: string; + cep: string; + logradouro: string; + numero: string; + complemento: string; + bairro: string; + cidade: string; + estado: string; + observacoes: string; + anexos: File[]; + tipo_vinculo: string; + dados_bancarios: DadosBancarios; + + agenda_horario: string; + valor_consulta: string; +}; + +const initial: FormData = { + photo: null, + nome: "", + nome_social: "", + crm: "", + estado_crm: "", + rqe: "", + formacao_academica: [], + curriculo: null, + especialidade: "", + cpf: "", + rg: "", + sexo: "", + data_nascimento: "", + email: "", + telefone: "", + celular: "", + contato_emergencia: "", + cep: "", + logradouro: "", + numero: "", + complemento: "", + bairro: "", + cidade: "", + estado: "", + observacoes: "", + anexos: [], + tipo_vinculo: "", + dados_bancarios: { + banco: "", + agencia: "", + conta: "", + tipo_conta: "", + }, + agenda_horario: "", + valor_consulta: "", +}; + + + +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, formacao: false, admin: 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 addFormacao() { + setField("formacao_academica", [ + ...form.formacao_academica, + { instituicao: "", curso: "", ano_conclusao: "" }, + ]); + } + + function removeFormacao(index: number) { + const newFormacao = [...form.formacao_academica]; + newFormacao.splice(index, 1); + setField("formacao_academica", newFormacao); + } + + function handleFormacaoChange(index: number, field: keyof FormacaoAcademica, value: string) { + const newFormacao = [...form.formacao_academica]; + newFormacao[index][field] = value; + setField("formacao_academica", newFormacao); + } + + function formatPhone(v: string) { + const n = v.replace(/\D/g, "").slice(0, 11); + if (n.length > 6) { + return n.replace(/(\d{2})(\d{5})(\d{0,4})/, "($1) $2-$3"); + } else if (n.length > 2) { + return n.replace(/(\d{2})(\d{0,5})/, "($1) $2"); + } + return n; + } + + function formatRG(v: string) { + v = v.replace(/\D/g, "").slice(0, 9); + v = v.replace(/(\d{2})(\d)/, "$1.$2"); + v = v.replace(/(\d{3})(\d)/, "$1.$2"); + v = v.replace(/(\d{3})(\d{1,2})$/, "$1-$2"); + return v; + } + + 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("estado_crm", e.target.value)} /> +
+
+ +
+
+ + setField("especialidade", e.target.value)} className={errors.especialidade ? "border-destructive" : ""} /> + {errors.especialidade &&

{errors.especialidade}

} +
+
+ + setField("rqe", e.target.value)} /> +
+
+ +
+ +
+ + setField("curriculo", e.target.files?.[0] || null)} + accept=".pdf,.doc,.docx" + /> + {form.curriculo && {form.curriculo.name}} +
+
+ +
+
+ + handleCPFChange(e.target.value)} + placeholder="000.000.000-00" + maxLength={14} + className={errors.cpf ? "border-destructive" : ""} + /> + {errors.cpf &&

{errors.cpf}

} +
+
+ + setField("rg", formatRG(e.target.value))} + maxLength={12} + /> +
+
+ +
+
+ + +
+
+ + setField("data_nascimento", e.target.value)} /> +
+
+
+
+
+
+ + setExpanded((s) => ({ ...s, formacao: !s.formacao }))}> + + + + + + + Formação Acadêmica + + {expanded.formacao ? : } + + + + + + {form.formacao_academica.map((formacao, index) => ( +
+
+ + + handleFormacaoChange(index, "instituicao", e.target.value) + } + /> +
+
+ + + handleFormacaoChange(index, "curso", e.target.value) + } + /> +
+
+ + + handleFormacaoChange(index, "ano_conclusao", e.target.value) + } + /> +
+ +
+ ))} + +
+
+
+
+ + setExpanded((s) => ({ ...s, contato: !s.contato }))}> + + + + + Contato + {expanded.contato ? : } + + + + + +
+
+ + setField("email", e.target.value)} /> +
+
+ + setField("telefone", formatPhone(e.target.value))} + placeholder="(XX) XXXXX-XXXX" + /> +
+
+
+
+ + setField("celular", formatPhone(e.target.value))} + placeholder="(XX) XXXXX-XXXX" + /> +
+
+ + setField("contato_emergencia", formatPhone(e.target.value))} + placeholder="(XX) XXXXX-XXXX" + /> +
+
+
+
+
+
+ + setExpanded((s) => ({ ...s, admin: !s.admin }))}> + + + + + + + Dados Administrativos e Financeiros + + {expanded.admin ? : } + + + + + +
+
+ + +
+
+ + setField("valor_consulta", e.target.value)} + placeholder="R$ 0,00" + /> +
+
+ +
+ +