From 791d31a5a680979827d6d84857c4cb8e1f6e3db1 Mon Sep 17 00:00:00 2001 From: Jonas Francisco Date: Thu, 18 Sep 2025 13:58:52 -0300 Subject: [PATCH] =?UTF-8?q?feat(api):=20implementa=C3=A7=C3=A3o=20e=20inte?= =?UTF-8?q?gra=C3=A7=C3=A3o=20das=20APIs=20de=20m=C3=A9dicos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(main-routes)/dashboard/doutores/page.tsx | 82 ++++---- .../forms/doctor-registration-form.tsx | 185 ++++++++++++++---- susconecta/lib/api.ts | 170 +++++++++++++++- 3 files changed, 351 insertions(+), 86 deletions(-) diff --git a/susconecta/app/(main-routes)/dashboard/doutores/page.tsx b/susconecta/app/(main-routes)/dashboard/doutores/page.tsx index 8a8944f..243c4ac 100644 --- a/susconecta/app/(main-routes)/dashboard/doutores/page.tsx +++ b/susconecta/app/(main-routes)/dashboard/doutores/page.tsx @@ -1,40 +1,39 @@ "use client"; -import { useMemo, useState } from "react"; +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, Edit, Trash2, ArrowLeft, Eye } from "lucide-react"; import { Badge } from "@/components/ui/badge"; -import { DoctorRegistrationForm, Medico } from "@/components/forms/doctor-registration-form"; +import { DoctorRegistrationForm } 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", - }, -]; +// >>> IMPORTES DA API <<< +import { listarMedicos, excluirMedico, Medico } from "@/lib/api"; export default function DoutoresPage() { - const [doctors, setDoctors] = useState(initialDoctors); + const [doctors, setDoctors] = useState([]); + const [loading, setLoading] = useState(false); const [search, setSearch] = useState(""); const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState(null); + // Carrega da API + async function load() { + setLoading(true); + try { + const list = await listarMedicos({ limit: 50 }); + setDoctors(list ?? []); + } finally { + setLoading(false); + } + } + + useEffect(() => { + load(); + }, []); + const filtered = useMemo(() => { if (!search.trim()) return doctors; const q = search.toLowerCase(); @@ -56,26 +55,17 @@ export default function DoutoresPage() { setShowForm(true); } - function handleDelete(id: string) { + // Excluir via API e recarregar + async function handleDelete(id: string) { if (!confirm("Excluir este médico?")) return; - setDoctors((prev) => prev.filter((x) => String(x.id) !== String(id))); + await excluirMedico(id); + await load(); } - 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; - }); + // Após salvar/criar/editar no form, fecha e recarrega + async function handleSaved() { setShowForm(false); + await load(); } if (showForm) { @@ -117,7 +107,7 @@ export default function DoutoresPage() { onChange={(e) => setSearch(e.target.value)} /> - @@ -136,7 +126,13 @@ export default function DoutoresPage() { - {filtered.length > 0 ? ( + {loading ? ( + + + Carregando… + + + ) : filtered.length > 0 ? ( filtered.map((doctor) => ( {doctor.nome} @@ -186,7 +182,9 @@ export default function DoutoresPage() { -
Mostrando {filtered.length} de {doctors.length}
+
+ Mostrando {filtered.length} de {doctors.length} +
); -} \ No newline at end of file +} diff --git a/susconecta/components/forms/doctor-registration-form.tsx b/susconecta/components/forms/doctor-registration-form.tsx index a7d6396..a137dad 100644 --- a/susconecta/components/forms/doctor-registration-form.tsx +++ b/susconecta/components/forms/doctor-registration-form.tsx @@ -13,6 +13,18 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u 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"; +import { + criarMedico, + atualizarMedico, + buscarMedicoPorId, + uploadFotoMedico, + listarAnexosMedico, + adicionarAnexoMedico, + removerAnexoMedico, + MedicoInput, +} from "@/lib/api"; + +import { buscarCepAPI } from "@/lib/api"; // use o seu já existente // Mock data and types since API is not used for now @@ -161,13 +173,57 @@ export function DoctorRegistrationForm({ const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]); - useEffect(() => { - // Data loading logic would go here in a real scenario + useEffect(() => { + let alive = true; + async function load() { if (mode === "edit" && doctorId) { - console.log("Loading doctor data for ID:", doctorId); - // Example: setForm(loadedDoctorData); + const medico = await buscarMedicoPorId(doctorId); + if (!alive) return; + // mapeia API -> estado do formulário + setForm({ + photo: null, + nome: medico.nome ?? "", + nome_social: medico.nome_social ?? "", + crm: medico.crm ?? "", + estado_crm: medico.estado_crm ?? "", + rqe: medico.rqe ?? "", + formacao_academica: medico.formacao_academica ?? [], + curriculo: null, // se a API devolver URL, você pode exibir ao lado + especialidade: medico.especialidade ?? "", + cpf: medico.cpf ?? "", + rg: medico.rg ?? "", + sexo: medico.sexo ?? "", + data_nascimento: medico.data_nascimento ?? "", + email: medico.email ?? "", + telefone: medico.telefone ?? "", + celular: medico.celular ?? "", + contato_emergencia: medico.contato_emergencia ?? "", + cep: "", + logradouro: "", + numero: "", + complemento: "", + bairro: "", + cidade: "", + estado: "", + observacoes: medico.observacoes ?? "", + anexos: [], + tipo_vinculo: medico.tipo_vinculo ?? "", + dados_bancarios: medico.dados_bancarios ?? { banco: "", agencia: "", conta: "", tipo_conta: "" }, + agenda_horario: medico.agenda_horario ?? "", + valor_consulta: medico.valor_consulta ? String(medico.valor_consulta) : "", + }); + + // (Opcional) listar anexos que já existem no servidor + try { + const list = await listarAnexosMedico(doctorId); + setServerAnexos(list ?? []); + } catch {} } - }, [mode, doctorId]); + } + load(); + return () => { alive = false; }; +}, [mode, doctorId]); + function setField(k: T, v: FormData[T]) { setForm((s) => ({ ...s, [k]: v })); @@ -225,27 +281,26 @@ export function DoctorRegistrationForm({ 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); + const clean = cep.replace(/\D/g, ""); + if (clean.length !== 8) return; + setSearchingCEP(true); + try { + const res = await buscarCepAPI(clean); + if (res && !res.erro) { + setField("logradouro", res.logradouro ?? ""); + setField("bairro", res.bairro ?? ""); + setField("cidade", res.localidade ?? ""); + setField("estado", res.uf ?? ""); + } else { + setErrors((e) => ({ ...e, cep: "CEP não encontrado" })); } + } catch { + setErrors((e) => ({ ...e, cep: "Erro ao buscar CEP" })); + } finally { + setSearchingCEP(false); } +} + function validateLocal(): boolean { const e: Record = {}; @@ -258,25 +313,75 @@ export function DoctorRegistrationForm({ } async function handleSubmit(ev: React.FormEvent) { - ev.preventDefault(); - if (!validateLocal()) return; + ev.preventDefault(); + if (!validateLocal()) return; - setSubmitting(true); - console.log("Submitting form with data:", form); + setSubmitting(true); + setErrors((e) => ({ ...e, submit: "" })); - // 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); + try { + // monta o payload esperado pela API + const payload: MedicoInput = { + nome: form.nome, + nome_social: form.nome_social || null, + cpf: form.cpf || null, + rg: form.rg || null, + sexo: form.sexo || null, + data_nascimento: form.data_nascimento || null, + telefone: form.telefone || null, + celular: form.celular || null, + contato_emergencia: form.contato_emergencia || null, + email: form.email || null, + crm: form.crm, + estado_crm: form.estado_crm || null, + rqe: form.rqe || null, + formacao_academica: form.formacao_academica ?? [], + curriculo_url: null, // se quiser, suba arquivo do currículo num endpoint próprio e salve a URL aqui + especialidade: form.especialidade, + observacoes: form.observacoes || null, + tipo_vinculo: form.tipo_vinculo || null, + dados_bancarios: form.dados_bancarios ?? null, + agenda_horario: form.agenda_horario || null, + valor_consulta: form.valor_consulta || null, + }; + + // cria ou atualiza + const saved = mode === "create" + ? await criarMedico(payload) + : await atualizarMedico(doctorId as number, payload); + + const medicoId = saved.id; + + // foto (opcional) + if (form.photo) { + try { + await uploadFotoMedico(medicoId, form.photo); + } catch (e) { + console.warn("Falha ao enviar foto:", e); + } + } + + // anexos locais (opcional) + if (form.anexos?.length) { + for (const f of form.anexos) { + try { + await adicionarAnexoMedico(medicoId, f); + } catch (e) { + console.warn("Falha ao enviar anexo:", f.name, e); + } + } + } + + onSaved?.(saved); + if (inline) onClose?.(); + else onOpenChange?.(false); + } catch (err: any) { + setErrors((e) => ({ ...e, submit: err?.message || "Erro ao salvar médico" })); + } finally { + setSubmitting(false); } +} + function handlePhoto(e: React.ChangeEvent) { const f = e.target.files?.[0]; diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index fc11746..1bde463 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -61,8 +61,10 @@ export type PacienteInput = { const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "https://mock.apidog.com/m1/1053378-0-default"; +const MEDICOS_BASE = process.env.NEXT_PUBLIC_MEDICOS_BASE_PATH ?? "/medicos"; -const PATHS = { +export const PATHS = { + // Pacientes (já existia) pacientes: "/pacientes", pacienteId: (id: string | number) => `/pacientes/${id}`, foto: (id: string | number) => `/pacientes/${id}/foto`, @@ -70,8 +72,16 @@ const PATHS = { anexoId: (id: string | number, anexoId: string | number) => `/pacientes/${id}/anexos/${anexoId}`, validarCPF: "/pacientes/validar-cpf", cep: (cep: string) => `/utils/cep/${cep}`, + + // Médicos (APONTANDO PARA PACIENTES por enquanto) + medicos: MEDICOS_BASE, + medicoId: (id: string | number) => `${MEDICOS_BASE}/${id}`, + medicoFoto: (id: string | number) => `${MEDICOS_BASE}/${id}/foto`, + medicoAnexos: (id: string | number) => `${MEDICOS_BASE}/${id}/anexos`, + medicoAnexoId: (id: string | number, anexoId: string | number) => `${MEDICOS_BASE}/${id}/anexos/${anexoId}`, } as const; + function headers(kind: "json" | "form" = "json"): Record { const h: Record = {}; const token = process.env.NEXT_PUBLIC_API_TOKEN?.trim(); @@ -95,17 +105,22 @@ async function parse(res: Response): Promise { try { json = await res.json(); } catch { - + // ignora erro de parse vazio } + if (!res.ok) { + // 🔴 ADICIONE ESSA LINHA AQUI: + console.error("[API ERROR]", res.url, res.status, json); + const code = json?.apidogError?.code ?? res.status; - const msg = json?.apidogError?.message ?? res.statusText; + const msg = json?.apidogError?.message ?? res.statusText; throw new Error(`${code}: ${msg}`); } - + return (json?.data ?? json) as T; } + // // Pacientes (CRUD) // @@ -250,3 +265,150 @@ export async function buscarCepAPI(cep: string): Promise<{ logradouro?: string; }; } } + +// >>> ADICIONE (ou mova) ESTES TIPOS <<< +export type FormacaoAcademica = { + instituicao: string; + curso: string; + ano_conclusao: string; +}; + +export 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; +}; + +export type MedicoInput = { + nome: string; + nome_social?: string | null; + cpf?: string | null; + rg?: string | null; + sexo?: string | null; + data_nascimento?: string | null; + telefone?: string | null; + celular?: string | null; + contato_emergencia?: string | null; + email?: string | null; + crm: string; + estado_crm?: string | null; + rqe?: string | null; + formacao_academica?: FormacaoAcademica[]; + curriculo_url?: string | null; + especialidade: string; + observacoes?: string | null; + tipo_vinculo?: string | null; + dados_bancarios?: DadosBancarios | null; + agenda_horario?: string | null; + valor_consulta?: number | string | null; +}; + +// +// MÉDICOS (CRUD) +// +// ======= MÉDICOS (forçando usar rotas de PACIENTES no mock) ======= + +export async function listarMedicos(params?: { page?: number; limit?: number; q?: string }): Promise { + const query = new URLSearchParams(); + if (params?.page) query.set("page", String(params.page)); + if (params?.limit) query.set("limit", String(params.limit)); + if (params?.q) query.set("q", params.q); + + // FORÇA /pacientes + const url = `${API_BASE}/pacientes${query.toString() ? `?${query.toString()}` : ""}`; + const res = await fetch(url, { method: "GET", headers: headers("json") }); + const data = await parse>(res); + return (data as any)?.data ?? (data as any); +} + +export async function buscarMedicoPorId(id: string | number): Promise { + const url = `${API_BASE}/pacientes/${id}`; // FORÇA /pacientes + const res = await fetch(url, { method: "GET", headers: headers("json") }); + const data = await parse>(res); + return (data as any)?.data ?? (data as any); +} + +export async function criarMedico(input: MedicoInput): Promise { + const url = `${API_BASE}/pacientes`; // FORÇA /pacientes + const res = await fetch(url, { method: "POST", headers: headers("json"), body: JSON.stringify(input) }); + const data = await parse>(res); + return (data as any)?.data ?? (data as any); +} + +export async function atualizarMedico(id: string | number, input: MedicoInput): Promise { + const url = `${API_BASE}/pacientes/${id}`; // FORÇA /pacientes + const res = await fetch(url, { method: "PUT", headers: headers("json"), body: JSON.stringify(input) }); + const data = await parse>(res); + return (data as any)?.data ?? (data as any); +} + +export async function excluirMedico(id: string | number): Promise { + const url = `${API_BASE}/pacientes/${id}`; // FORÇA /pacientes + const res = await fetch(url, { method: "DELETE", headers: headers("json") }); + await parse(res); +} + +export async function uploadFotoMedico(id: string | number, file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> { + const url = `${API_BASE}/pacientes/${id}/foto`; // FORÇA /pacientes + const fd = new FormData(); + fd.append("foto", file); + const res = await fetch(url, { method: "POST", headers: headers("form"), body: fd }); + const data = await parse>(res); + return (data as any)?.data ?? (data as any); +} + +export async function removerFotoMedico(id: string | number): Promise { + const url = `${API_BASE}/pacientes/${id}/foto`; // FORÇA /pacientes + const res = await fetch(url, { method: "DELETE", headers: headers("json") }); + await parse(res); +} + +export async function listarAnexosMedico(id: string | number): Promise { + const url = `${API_BASE}/pacientes/${id}/anexos`; // FORÇA /pacientes + const res = await fetch(url, { method: "GET", headers: headers("json") }); + const data = await parse>(res); + return (data as any)?.data ?? (data as any); +} + +export async function adicionarAnexoMedico(id: string | number, file: File): Promise { + const url = `${API_BASE}/pacientes/${id}/anexos`; // FORÇA /pacientes + const fd = new FormData(); + fd.append("arquivo", file); + const res = await fetch(url, { method: "POST", headers: headers("form"), body: fd }); + const data = await parse>(res); + return (data as any)?.data ?? (data as any); +} + +export async function removerAnexoMedico(id: string | number, anexoId: string | number): Promise { + const url = `${API_BASE}/pacientes/${id}/anexos/${anexoId}`; // FORÇA /pacientes + const res = await fetch(url, { method: "DELETE", headers: headers("json") }); + await parse(res); +} +// ======= FIM: médicos usando rotas de pacientes =======