Merge pull request 'feat(api): implementação e integração das APIs de médicos' (#12) from feature/api-medicos into develop

Reviewed-on: #12
This commit is contained in:
Jonasbomfim 2025-09-18 17:00:52 +00:00
commit 913fd6ad64
3 changed files with 351 additions and 86 deletions

View File

@ -1,40 +1,39 @@
"use client"; "use client";
import { useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye } from "lucide-react"; import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye } from "lucide-react";
import { Badge } from "@/components/ui/badge"; 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 // >>> IMPORTES DA API <<<
const initialDoctors: Medico[] = [ import { listarMedicos, excluirMedico, Medico } from "@/lib/api";
{
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() { export default function DoutoresPage() {
const [doctors, setDoctors] = useState<Medico[]>(initialDoctors); const [doctors, setDoctors] = useState<Medico[]>([]);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState<string | null>(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(() => { const filtered = useMemo(() => {
if (!search.trim()) return doctors; if (!search.trim()) return doctors;
const q = search.toLowerCase(); const q = search.toLowerCase();
@ -56,26 +55,17 @@ export default function DoutoresPage() {
setShowForm(true); setShowForm(true);
} }
function handleDelete(id: string) { // Excluir via API e recarregar
async function handleDelete(id: string) {
if (!confirm("Excluir este médico?")) return; 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) { // Após salvar/criar/editar no form, fecha e recarrega
const saved = medico; async function handleSaved() {
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); setShowForm(false);
await load();
} }
if (showForm) { if (showForm) {
@ -117,7 +107,7 @@ export default function DoutoresPage() {
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
</div> </div>
<Button onClick={handleAdd}> <Button onClick={handleAdd} disabled={loading}>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Novo Médico Novo Médico
</Button> </Button>
@ -136,7 +126,13 @@ export default function DoutoresPage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filtered.length > 0 ? ( {loading ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground">
Carregando
</TableCell>
</TableRow>
) : filtered.length > 0 ? (
filtered.map((doctor) => ( filtered.map((doctor) => (
<TableRow key={doctor.id}> <TableRow key={doctor.id}>
<TableCell className="font-medium">{doctor.nome}</TableCell> <TableCell className="font-medium">{doctor.nome}</TableCell>
@ -186,7 +182,9 @@ export default function DoutoresPage() {
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
<div className="text-sm text-muted-foreground">Mostrando {filtered.length} de {doctors.length}</div> <div className="text-sm text-muted-foreground">
Mostrando {filtered.length} de {doctors.length}
</div>
</div> </div>
); );
} }

View File

@ -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 { AlertCircle, ChevronDown, ChevronUp, FileImage, Loader2, Save, Upload, User, X, XCircle, Trash2 } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; 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 // Mock data and types since API is not used for now
@ -162,13 +174,57 @@ export function DoctorRegistrationForm({
const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]); const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]);
useEffect(() => { useEffect(() => {
// Data loading logic would go here in a real scenario let alive = true;
async function load() {
if (mode === "edit" && doctorId) { if (mode === "edit" && doctorId) {
console.log("Loading doctor data for ID:", doctorId); const medico = await buscarMedicoPorId(doctorId);
// Example: setForm(loadedDoctorData); 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 {}
} }
}
load();
return () => { alive = false; };
}, [mode, doctorId]); }, [mode, doctorId]);
function setField<T extends keyof FormData>(k: T, v: FormData[T]) { function setField<T extends keyof FormData>(k: T, v: FormData[T]) {
setForm((s) => ({ ...s, [k]: v })); setForm((s) => ({ ...s, [k]: v }));
if (errors[k as string]) setErrors((e) => ({ ...e, [k]: "" })); if (errors[k as string]) setErrors((e) => ({ ...e, [k]: "" }));
@ -229,16 +285,14 @@ export function DoctorRegistrationForm({
if (clean.length !== 8) return; if (clean.length !== 8) return;
setSearchingCEP(true); setSearchingCEP(true);
try { try {
// Mocking API call const res = await buscarCepAPI(clean);
console.log("Searching CEP:", clean); if (res && !res.erro) {
// 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("logradouro", res.logradouro ?? "");
setField("bairro", res.bairro ?? ""); setField("bairro", res.bairro ?? "");
setField("cidade", res.localidade ?? ""); setField("cidade", res.localidade ?? "");
setField("estado", res.uf ?? ""); setField("estado", res.uf ?? "");
} else {
setErrors((e) => ({ ...e, cep: "CEP não encontrado" }));
} }
} catch { } catch {
setErrors((e) => ({ ...e, cep: "Erro ao buscar CEP" })); setErrors((e) => ({ ...e, cep: "Erro ao buscar CEP" }));
@ -247,6 +301,7 @@ export function DoctorRegistrationForm({
} }
} }
function validateLocal(): boolean { function validateLocal(): boolean {
const e: Record<string, string> = {}; const e: Record<string, string> = {};
if (!form.nome.trim()) e.nome = "Nome é obrigatório"; if (!form.nome.trim()) e.nome = "Nome é obrigatório";
@ -262,21 +317,71 @@ export function DoctorRegistrationForm({
if (!validateLocal()) return; if (!validateLocal()) return;
setSubmitting(true); setSubmitting(true);
console.log("Submitting form with data:", form); setErrors((e) => ({ ...e, submit: "" }));
// Simulate API call try {
setTimeout(() => { // monta o payload esperado pela API
setSubmitting(false); const payload: MedicoInput = {
const savedData: Medico = { nome: form.nome,
id: doctorId ? String(doctorId) : String(Date.now()), nome_social: form.nome_social || null,
...form, 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,
}; };
onSaved?.(savedData);
alert(mode === "create" ? "Médico cadastrado com sucesso! (simulado)" : "Médico atualizado com sucesso! (simulado)"); // 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?.(); if (inline) onClose?.();
else onOpenChange?.(false); else onOpenChange?.(false);
}, 1000); } catch (err: any) {
setErrors((e) => ({ ...e, submit: err?.message || "Erro ao salvar médico" }));
} finally {
setSubmitting(false);
} }
}
function handlePhoto(e: React.ChangeEvent<HTMLInputElement>) { function handlePhoto(e: React.ChangeEvent<HTMLInputElement>) {
const f = e.target.files?.[0]; const f = e.target.files?.[0];

View File

@ -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 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", pacientes: "/pacientes",
pacienteId: (id: string | number) => `/pacientes/${id}`, pacienteId: (id: string | number) => `/pacientes/${id}`,
foto: (id: string | number) => `/pacientes/${id}/foto`, foto: (id: string | number) => `/pacientes/${id}/foto`,
@ -70,8 +72,16 @@ const PATHS = {
anexoId: (id: string | number, anexoId: string | number) => `/pacientes/${id}/anexos/${anexoId}`, anexoId: (id: string | number, anexoId: string | number) => `/pacientes/${id}/anexos/${anexoId}`,
validarCPF: "/pacientes/validar-cpf", validarCPF: "/pacientes/validar-cpf",
cep: (cep: string) => `/utils/cep/${cep}`, 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; } as const;
function headers(kind: "json" | "form" = "json"): Record<string, string> { function headers(kind: "json" | "form" = "json"): Record<string, string> {
const h: Record<string, string> = {}; const h: Record<string, string> = {};
const token = process.env.NEXT_PUBLIC_API_TOKEN?.trim(); const token = process.env.NEXT_PUBLIC_API_TOKEN?.trim();
@ -95,9 +105,13 @@ async function parse<T>(res: Response): Promise<T> {
try { try {
json = await res.json(); json = await res.json();
} catch { } catch {
// ignora erro de parse vazio
} }
if (!res.ok) { if (!res.ok) {
// 🔴 ADICIONE ESSA LINHA AQUI:
console.error("[API ERROR]", res.url, res.status, json);
const code = json?.apidogError?.code ?? res.status; 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}`); throw new Error(`${code}: ${msg}`);
@ -106,6 +120,7 @@ async function parse<T>(res: Response): Promise<T> {
return (json?.data ?? json) as T; return (json?.data ?? json) as T;
} }
// //
// Pacientes (CRUD) // 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<Medico[]> {
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<ApiOk<Medico[]>>(res);
return (data as any)?.data ?? (data as any);
}
export async function buscarMedicoPorId(id: string | number): Promise<Medico> {
const url = `${API_BASE}/pacientes/${id}`; // FORÇA /pacientes
const res = await fetch(url, { method: "GET", headers: headers("json") });
const data = await parse<ApiOk<Medico>>(res);
return (data as any)?.data ?? (data as any);
}
export async function criarMedico(input: MedicoInput): Promise<Medico> {
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<ApiOk<Medico>>(res);
return (data as any)?.data ?? (data as any);
}
export async function atualizarMedico(id: string | number, input: MedicoInput): Promise<Medico> {
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<ApiOk<Medico>>(res);
return (data as any)?.data ?? (data as any);
}
export async function excluirMedico(id: string | number): Promise<void> {
const url = `${API_BASE}/pacientes/${id}`; // FORÇA /pacientes
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
await parse<any>(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<ApiOk<{ foto_url?: string; thumbnail_url?: string }>>(res);
return (data as any)?.data ?? (data as any);
}
export async function removerFotoMedico(id: string | number): Promise<void> {
const url = `${API_BASE}/pacientes/${id}/foto`; // FORÇA /pacientes
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
await parse<any>(res);
}
export async function listarAnexosMedico(id: string | number): Promise<any[]> {
const url = `${API_BASE}/pacientes/${id}/anexos`; // FORÇA /pacientes
const res = await fetch(url, { method: "GET", headers: headers("json") });
const data = await parse<ApiOk<any[]>>(res);
return (data as any)?.data ?? (data as any);
}
export async function adicionarAnexoMedico(id: string | number, file: File): Promise<any> {
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<ApiOk<any>>(res);
return (data as any)?.data ?? (data as any);
}
export async function removerAnexoMedico(id: string | number, anexoId: string | number): Promise<void> {
const url = `${API_BASE}/pacientes/${id}/anexos/${anexoId}`; // FORÇA /pacientes
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
await parse<any>(res);
}
// ======= FIM: médicos usando rotas de pacientes =======