From 84cb4c36ebcbbaa91988122949a7f09ab2de40d7 Mon Sep 17 00:00:00 2001 From: Jonas Francisco Date: Tue, 30 Sep 2025 13:57:43 -0300 Subject: [PATCH] feat(api): add doctors and patients API integration --- .../app/(main-routes)/doutores/page.tsx | 61 ++- .../app/(main-routes)/pacientes/page.tsx | 27 +- susconecta/app/layout.tsx | 4 +- susconecta/components/dashboard/sidebar.tsx | 2 +- .../forms/patient-registration-form.tsx | 48 +- susconecta/lib/api.ts | 502 +++++++----------- susconecta/lib/utils.ts | 19 + 7 files changed, 326 insertions(+), 337 deletions(-) diff --git a/susconecta/app/(main-routes)/doutores/page.tsx b/susconecta/app/(main-routes)/doutores/page.tsx index 588e7f9..feb1798 100644 --- a/susconecta/app/(main-routes)/doutores/page.tsx +++ b/susconecta/app/(main-routes)/doutores/page.tsx @@ -14,6 +14,35 @@ import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-f import { listarMedicos, excluirMedico, Medico } from "@/lib/api"; +function normalizeMedico(m: any): Medico { + return { + id: String(m.id ?? m.uuid ?? ""), + nome: m.nome ?? m.full_name ?? "", // 👈 Supabase usa full_name + nome_social: m.nome_social ?? m.social_name ?? null, + cpf: m.cpf ?? "", + rg: m.rg ?? m.document_number ?? null, + sexo: m.sexo ?? m.sex ?? null, + data_nascimento: m.data_nascimento ?? m.birth_date ?? null, + telefone: m.telefone ?? m.phone_mobile ?? "", + celular: m.celular ?? m.phone2 ?? null, + contato_emergencia: m.contato_emergencia ?? null, + email: m.email ?? "", + crm: m.crm ?? "", + estado_crm: m.estado_crm ?? m.crm_state ?? null, + rqe: m.rqe ?? null, + formacao_academica: m.formacao_academica ?? [], + curriculo_url: m.curriculo_url ?? null, + especialidade: m.especialidade ?? m.specialty ?? "", + observacoes: m.observacoes ?? m.notes ?? null, + foto_url: m.foto_url ?? null, + tipo_vinculo: m.tipo_vinculo ?? null, + dados_bancarios: m.dados_bancarios ?? null, + agenda_horario: m.agenda_horario ?? null, + valor_consulta: m.valor_consulta ?? null, + }; +} + + export default function DoutoresPage() { const [doctors, setDoctors] = useState([]); const [loading, setLoading] = useState(false); @@ -26,8 +55,9 @@ export default function DoutoresPage() { async function load() { setLoading(true); try { - const list = await listarMedicos({ limit: 50 }); - setDoctors(list ?? []); + const list = await listarMedicos({ limit: 50 }); +setDoctors((list ?? []).map(normalizeMedico)); + } finally { setLoading(false); } @@ -53,6 +83,8 @@ export default function DoutoresPage() { setShowForm(true); } + + function handleEdit(id: string) { setEditingId(id); setShowForm(true); @@ -70,10 +102,29 @@ export default function DoutoresPage() { } - async function handleSaved() { - setShowForm(false); - await load(); + function handleSaved(savedDoctor?: Medico) { + setShowForm(false); + + if (savedDoctor) { + const normalized = normalizeMedico(savedDoctor); + setDoctors((prev) => { + const i = prev.findIndex((d) => String(d.id) === String(normalized.id)); + if (i < 0) { + // Novo médico → adiciona no topo + return [normalized, ...prev]; + } else { + // Médico editado → substitui na lista + const clone = [...prev]; + clone[i] = normalized; + return clone; + } + }); + } else { + // fallback → recarrega tudo + load(); } +} + if (showForm) { return ( diff --git a/susconecta/app/(main-routes)/pacientes/page.tsx b/susconecta/app/(main-routes)/pacientes/page.tsx index d4a25ff..bacb207 100644 --- a/susconecta/app/(main-routes)/pacientes/page.tsx +++ b/susconecta/app/(main-routes)/pacientes/page.tsx @@ -17,30 +17,31 @@ import { PatientRegistrationForm } from "@/components/forms/patient-registration 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 ?? "", + logradouro: p.endereco?.logradouro ?? p.street ?? "", + numero: p.endereco?.numero ?? p.number ?? "", + complemento: p.endereco?.complemento ?? p.complement ?? "", + bairro: p.endereco?.bairro ?? p.neighborhood ?? "", + cidade: p.endereco?.cidade ?? p.city ?? "", + estado: p.endereco?.estado ?? p.state ?? "", }; return { id: String(p.id ?? p.uuid ?? p.paciente_id ?? ""), - nome: p.nome ?? "", - nome_social: p.nome_social ?? null, + nome: p.full_name ?? "", // 👈 troca nome → full_name + nome_social: p.social_name ?? null, // 👈 Supabase usa social_name cpf: p.cpf ?? "", - rg: p.rg ?? null, - sexo: p.sexo ?? null, - data_nascimento: p.data_nascimento ?? null, - telefone: p.telefone ?? "", + rg: p.rg ?? p.document_number ?? null, // 👈 às vezes vem como document_number + sexo: p.sexo ?? p.sex ?? null, // 👈 Supabase usa sex + data_nascimento: p.data_nascimento ?? p.birth_date ?? null, + telefone: p.telefone ?? p.phone_mobile ?? "", email: p.email ?? "", endereco, - observacoes: p.observacoes ?? null, + observacoes: p.observacoes ?? p.notes ?? null, foto_url: p.foto_url ?? null, }; } + export default function PacientesPage() { const [patients, setPatients] = useState([]); const [loading, setLoading] = useState(true); diff --git a/susconecta/app/layout.tsx b/susconecta/app/layout.tsx index d900efc..f5b69fe 100644 --- a/susconecta/app/layout.tsx +++ b/susconecta/app/layout.tsx @@ -4,9 +4,9 @@ import { AuthProvider } from "@/hooks/useAuth" import "./globals.css" export const metadata: Metadata = { - title: "MediConecta - Conectando Pacientes e Profissionais de Saúde", + title: "MediConnect - Conectando Pacientes e Profissionais de Saúde", description: - "Plataforma inovadora que conecta pacientes e médicos de forma prática, segura e humanizada. Experimente o futuro dos agendamentos médicos.", + "Plataforma inovadora que conecta pacientes, clínicas, e médicos de forma prática, segura e humanizada. Experimente o futuro dos agendamentos médicos.", keywords: "saúde, médicos, pacientes, agendamento, telemedicina, SUS", generator: 'v0.app' } diff --git a/susconecta/components/dashboard/sidebar.tsx b/susconecta/components/dashboard/sidebar.tsx index 81b1c83..e7feeec 100644 --- a/susconecta/components/dashboard/sidebar.tsx +++ b/susconecta/components/dashboard/sidebar.tsx @@ -61,7 +61,7 @@ export function Sidebar() { {/* este span some no modo ícone */} - MediConecta + MediConnect diff --git a/susconecta/components/forms/patient-registration-form.tsx b/susconecta/components/forms/patient-registration-form.tsx index b0912be..4ceb247 100644 --- a/susconecta/components/forms/patient-registration-form.tsx +++ b/susconecta/components/forms/patient-registration-form.tsx @@ -17,7 +17,6 @@ import { Paciente, PacienteInput, buscarCepAPI, - validarCPF, criarPaciente, atualizarPaciente, uploadFotoPaciente, @@ -28,6 +27,11 @@ import { buscarPacientePorId, } from "@/lib/api"; +import { validarCPFLocal } from "@/lib/utils"; +import { verificarCpfDuplicado } from "@/lib/api"; + + + type Mode = "create" | "edit"; export interface PatientRegistrationFormProps { @@ -192,13 +196,13 @@ export function PatientRegistrationForm({ 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, + cep: form.cep || undefined, + logradouro: form.logradouro || undefined, + numero: form.numero || undefined, + complemento: form.complemento || undefined, + bairro: form.bairro || undefined, + cidade: form.cidade || undefined, + estado: form.estado || undefined, }, observacoes: form.observacoes || null, }; @@ -210,18 +214,24 @@ export function PatientRegistrationForm({ try { - 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 { - + // 1) validação local + if (!validarCPFLocal(form.cpf)) { + setErrors((e) => ({ ...e, cpf: "CPF inválido" })); + return; + } + + // 2) checar duplicidade no banco (apenas se criando novo paciente) + if (mode === "create") { + const existe = await verificarCpfDuplicado(form.cpf); + if (existe) { + setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" })); + return; } + } +} catch (err) { + console.error("Erro ao validar CPF", err); +} + setSubmitting(true); try { diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index 960b734..ef8cfce 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -1,7 +1,7 @@ - +// lib/api.ts export type ApiOk = { - success: boolean; + success?: boolean; data: T; message?: string; pagination?: { @@ -12,6 +12,7 @@ export type ApiOk = { }; }; +// ===== TIPOS COMUNS ===== export type Endereco = { cep?: string; logradouro?: string; @@ -22,6 +23,7 @@ export type Endereco = { estado?: string; }; +// ===== PACIENTES ===== export type Paciente = { id: string; nome?: string; @@ -46,241 +48,11 @@ export type PacienteInput = { data_nascimento?: string | null; telefone?: string | null; email?: string | null; - endereco?: { - cep?: string | null; - logradouro?: string | null; - numero?: string | null; - complemento?: string | null; - bairro?: string | null; - cidade?: string | null; - estado?: string | null; - }; + endereco?: Endereco; observacoes?: string | null; }; - - -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"; - -export const PATHS = { - // Pacientes (já existia) - pacientes: "/pacientes", - pacienteId: (id: string | number) => `/pacientes/${id}`, - foto: (id: string | number) => `/pacientes/${id}/foto`, - anexos: (id: string | number) => `/pacientes/${id}/anexos`, - 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; - - -// Função para obter o token JWT do localStorage -function getAuthToken(): string | null { - if (typeof window === 'undefined') return null; - return localStorage.getItem('auth_token'); -} - -function headers(kind: "json" | "form" = "json"): Record { - const h: Record = {}; - - // API Key da Supabase sempre necessária - h.apikey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"; - - // Bearer Token quando usuário está logado - const jwtToken = getAuthToken(); - if (jwtToken) { - h.Authorization = `Bearer ${jwtToken}`; - } - - if (kind === "json") h["Content-Type"] = "application/json"; - return h; -} - -function logAPI(title: string, info: { url?: string; payload?: any; result?: any } = {}) { - try { - console.group(`[API] ${title}`); - if (info.url) console.log("url:", info.url); - if (info.payload !== undefined) console.log("payload:", info.payload); - if (info.result !== undefined) console.log("API result:", info.result); - console.groupEnd(); - } catch {} -} - -async function parse(res: Response): Promise { - let json: any = null; - 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; - throw new Error(`${code}: ${msg}`); - } - - return (json?.data ?? json) as T; -} - - -// -// Pacientes (CRUD) -// -export async function listarPacientes(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); - const url = `${API_BASE}${PATHS.pacientes}${query.toString() ? `?${query.toString()}` : ""}`; - - const res = await fetch(url, { method: "GET", headers: headers("json") }); - const data = await parse>(res); - logAPI("listarPacientes", { url, result: data }); - return data?.data ?? (data as any); -} - -export async function buscarPacientePorId(id: string | number): Promise { - const url = `${API_BASE}${PATHS.pacienteId(id)}`; - const res = await fetch(url, { method: "GET", headers: headers("json") }); - const data = await parse>(res); - logAPI("buscarPacientePorId", { url, result: data }); - return data?.data ?? (data as any); -} - -export async function criarPaciente(input: PacienteInput): Promise { - const url = `${API_BASE}${PATHS.pacientes}`; - const res = await fetch(url, { method: "POST", headers: headers("json"), body: JSON.stringify(input) }); - const data = await parse>(res); - logAPI("criarPaciente", { url, payload: input, result: data }); - return data?.data ?? (data as any); -} - -export async function atualizarPaciente(id: string | number, input: PacienteInput): Promise { - const url = `${API_BASE}${PATHS.pacienteId(id)}`; - const res = await fetch(url, { method: "PUT", headers: headers("json"), body: JSON.stringify(input) }); - const data = await parse>(res); - logAPI("atualizarPaciente", { url, payload: input, result: data }); - return data?.data ?? (data as any); -} - -export async function excluirPaciente(id: string | number): Promise { - const url = `${API_BASE}${PATHS.pacienteId(id)}`; - const res = await fetch(url, { method: "DELETE", headers: headers("json") }); - await parse(res); - logAPI("excluirPaciente", { url, result: { ok: true } }); -} - -// -// Foto -// - -export async function uploadFotoPaciente(id: string | number, file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> { - const url = `${API_BASE}${PATHS.foto(id)}`; - const fd = new FormData(); - // nome de campo mais comum no mock - fd.append("foto", file); - const res = await fetch(url, { method: "POST", headers: headers("form"), body: fd }); - const data = await parse>(res); - logAPI("uploadFotoPaciente", { url, payload: { file: file.name }, result: data }); - return data?.data ?? (data as any); -} - -export async function removerFotoPaciente(id: string | number): Promise { - const url = `${API_BASE}${PATHS.foto(id)}`; - const res = await fetch(url, { method: "DELETE", headers: headers("json") }); - await parse(res); - logAPI("removerFotoPaciente", { url, result: { ok: true } }); -} - -// -// Anexos -// - -export async function listarAnexos(id: string | number): Promise { - const url = `${API_BASE}${PATHS.anexos(id)}`; - const res = await fetch(url, { method: "GET", headers: headers("json") }); - const data = await parse>(res); - logAPI("listarAnexos", { url, result: data }); - return data?.data ?? (data as any); -} - -export async function adicionarAnexo(id: string | number, file: File): Promise { - const url = `${API_BASE}${PATHS.anexos(id)}`; - const fd = new FormData(); - - fd.append("arquivo", file); - const res = await fetch(url, { method: "POST", body: fd, headers: headers("form") }); - const data = await parse>(res); - logAPI("adicionarAnexo", { url, payload: { file: file.name }, result: data }); - return data?.data ?? (data as any); -} - -export async function removerAnexo(id: string | number, anexoId: string | number): Promise { - const url = `${API_BASE}${PATHS.anexoId(id, anexoId)}`; - const res = await fetch(url, { method: "DELETE", headers: headers("json") }); - await parse(res); - logAPI("removerAnexo", { url, result: { ok: true } }); -} - -// -// Validações -// - -export async function validarCPF(cpf: string): Promise<{ valido: boolean; existe: boolean; paciente_id: string | null }> { - const url = `${API_BASE}${PATHS.validarCPF}`; - const payload = { cpf }; - const res = await fetch(url, { method: "POST", headers: headers("json"), body: JSON.stringify(payload) }); - const data = await parse>(res); - logAPI("validarCPF", { url, payload, result: data }); - return data?.data ?? (data as any); -} - -export async function buscarCepAPI(cep: string): Promise<{ logradouro?: string; bairro?: string; localidade?: string; uf?: string; erro?: boolean }> { - const clean = (cep || "").replace(/\D/g, ""); - const urlMock = `${API_BASE}${PATHS.cep(clean)}`; - - try { - const res = await fetch(urlMock, { method: "GET", headers: headers("json") }); - const data = await parse(res); // pode vir direto ou dentro de {data} - logAPI("buscarCEP (mock)", { url: urlMock, payload: { cep: clean }, result: data }); - const d = data?.data ?? data ?? {}; - return { - logradouro: d.logradouro ?? d.street ?? "", - bairro: d.bairro ?? d.neighborhood ?? "", - localidade: d.localidade ?? d.city ?? "", - uf: d.uf ?? d.state ?? "", - erro: false, - }; - } catch { - // fallback ViaCEP - const urlVia = `https://viacep.com.br/ws/${clean}/json/`; - const resV = await fetch(urlVia); - const jsonV = await resV.json().catch(() => ({})); - logAPI("buscarCEP (ViaCEP/fallback)", { url: urlVia, payload: { cep: clean }, result: jsonV }); - if (jsonV?.erro) return { erro: true }; - return { - logradouro: jsonV.logradouro ?? "", - bairro: jsonV.bairro ?? "", - localidade: jsonV.localidade ?? "", - uf: jsonV.uf ?? "", - erro: false, - }; - } -} - -// >>> ADICIONE (ou mova) ESTES TIPOS <<< +// ===== MÉDICOS ===== export type FormacaoAcademica = { instituicao: string; curso: string; @@ -344,85 +116,221 @@ export type MedicoInput = { valor_consulta?: number | string | null; }; -// -// MÉDICOS (CRUD) -// -// ======= MÉDICOS (forçando usar rotas de PACIENTES no mock) ======= +// ===== CONFIG ===== +const API_BASE = + process.env.NEXT_PUBLIC_API_BASE ?? "https://yuanqfswhberkoevtmfr.supabase.co"; +const REST = `${API_BASE}/rest/v1`; -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); +// Token salvo no browser (aceita auth_token ou token) +function getAuthToken(): string | null { + if (typeof window === "undefined") return null; + return ( + localStorage.getItem("auth_token") || + localStorage.getItem("token") || + sessionStorage.getItem("auth_token") || + sessionStorage.getItem("token") + ); +} - // 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); +// Cabeçalhos base +function baseHeaders(): Record { + const h: Record = { + apikey: + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ", + Accept: "application/json", + }; + const jwt = getAuthToken(); + if (jwt) h.Authorization = `Bearer ${jwt}`; + return h; +} + +// Para POST/PATCH/DELETE e para GET com count +function withPrefer(h: Record, prefer: string) { + return { ...h, Prefer: prefer }; +} + +// Parse genérico +async function parse(res: Response): Promise { + let json: any = null; + try { + json = await res.json(); + } catch {} + if (!res.ok) { + console.error("[API ERROR]", res.url, res.status, json); + const code = (json && (json.error?.code || json.code)) ?? res.status; + const msg = (json && (json.error?.message || json.message)) ?? res.statusText; + throw new Error(`${code}: ${msg}`); + } + return (json?.data ?? json) as T; +} + +// Helper de paginação (Range/Range-Unit) +function rangeHeaders(page?: number, limit?: number): Record { + if (!page || !limit) return {}; + const start = (page - 1) * limit; + const end = start + limit - 1; + return { Range: `${start}-${end}`, "Range-Unit": "items" }; +} + +// ===== PACIENTES (CRUD) ===== +export async function listarPacientes(params?: { + page?: number; + limit?: number; + q?: string; +}): Promise { + const qs = new URLSearchParams(); + if (params?.q) qs.set("q", params.q); + + const url = `${REST}/patients${qs.toString() ? `?${qs.toString()}` : ""}`; + const res = await fetch(url, { + method: "GET", + headers: { + ...baseHeaders(), + ...rangeHeaders(params?.page, params?.limit), + }, + }); + return await parse(res); +} + +export async function buscarPacientePorId(id: string | number): Promise { + const url = `${REST}/patients?id=eq.${id}`; + const res = await fetch(url, { method: "GET", headers: baseHeaders() }); + const arr = await parse(res); + if (!arr?.length) throw new Error("404: Paciente não encontrado"); + return arr[0]; +} + +export async function criarPaciente(input: PacienteInput): Promise { + const url = `${REST}/patients`; + const res = await fetch(url, { + method: "POST", + headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"), + body: JSON.stringify(input), + }); + const arr = await parse(res); + return Array.isArray(arr) ? arr[0] : (arr as Paciente); +} + +export async function atualizarPaciente(id: string | number, input: PacienteInput): Promise { + const url = `${REST}/patients?id=eq.${id}`; + const res = await fetch(url, { + method: "PATCH", + headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"), + body: JSON.stringify(input), + }); + const arr = await parse(res); + return Array.isArray(arr) ? arr[0] : (arr as Paciente); +} + +export async function excluirPaciente(id: string | number): Promise { + const url = `${REST}/patients?id=eq.${id}`; + const res = await fetch(url, { method: "DELETE", headers: baseHeaders() }); + await parse(res); +} +// ===== PACIENTES (Extra: verificação de CPF duplicado) ===== +export async function verificarCpfDuplicado(cpf: string): Promise { + const clean = (cpf || "").replace(/\D/g, ""); + const url = `${API_BASE}/rest/v1/patients?cpf=eq.${clean}&select=id`; + + const res = await fetch(url, { + method: "GET", + headers: baseHeaders(), + }); + + const data = await res.json().catch(() => []); + return Array.isArray(data) && data.length > 0; +} + + +// ===== MÉDICOS (CRUD) ===== +export async function listarMedicos(params?: { + page?: number; + limit?: number; + q?: string; +}): Promise { + const qs = new URLSearchParams(); + if (params?.q) qs.set("q", params.q); + + const url = `${REST}/doctors${qs.toString() ? `?${qs.toString()}` : ""}`; + const res = await fetch(url, { + method: "GET", + headers: { + ...baseHeaders(), + ...rangeHeaders(params?.page, params?.limit), + }, + }); + return await parse(res); } 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); + const url = `${REST}/doctors?id=eq.${id}`; + const res = await fetch(url, { method: "GET", headers: baseHeaders() }); + const arr = await parse(res); + if (!arr?.length) throw new Error("404: Médico não encontrado"); + return arr[0]; } 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); + const url = `${REST}/doctors`; + const res = await fetch(url, { + method: "POST", + headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"), + body: JSON.stringify(input), + }); + const arr = await parse(res); + return Array.isArray(arr) ? arr[0] : (arr as Medico); } 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); + const url = `${REST}/doctors?id=eq.${id}`; + const res = await fetch(url, { + method: "PATCH", + headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"), + body: JSON.stringify(input), + }); + const arr = await parse(res); + return Array.isArray(arr) ? arr[0] : (arr as Medico); } 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") }); + const url = `${REST}/doctors?id=eq.${id}`; + const res = await fetch(url, { method: "DELETE", headers: baseHeaders() }); 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); +// ===== CEP (usado nos formulários) ===== +export async function buscarCepAPI(cep: string): Promise<{ + logradouro?: string; + bairro?: string; + localidade?: string; + uf?: string; + erro?: boolean; +}> { + const clean = (cep || "").replace(/\D/g, ""); + try { + const res = await fetch(`https://viacep.com.br/ws/${clean}/json/`); + const json = await res.json(); + if (json?.erro) return { erro: true }; + return { + logradouro: json.logradouro ?? "", + bairro: json.bairro ?? "", + localidade: json.localidade ?? "", + uf: json.uf ?? "", + erro: false, + }; + } catch { + return { erro: true }; + } } -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 ======= +// ===== Stubs pra não quebrar imports dos forms (sem rotas de storage na doc) ===== +export async function listarAnexos(_id: string | number): Promise { return []; } +export async function adicionarAnexo(_id: string | number, _file: File): Promise { return {}; } +export async function removerAnexo(_id: string | number, _anexoId: string | number): Promise {} +export async function uploadFotoPaciente(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> { return {}; } +export async function removerFotoPaciente(_id: string | number): Promise {} +export async function listarAnexosMedico(_id: string | number): Promise { return []; } +export async function adicionarAnexoMedico(_id: string | number, _file: File): Promise { return {}; } +export async function removerAnexoMedico(_id: string | number, _anexoId: string | number): Promise {} +export async function uploadFotoMedico(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> { return {}; } +export async function removerFotoMedico(_id: string | number): Promise {} diff --git a/susconecta/lib/utils.ts b/susconecta/lib/utils.ts index bd0c391..f2ae213 100644 --- a/susconecta/lib/utils.ts +++ b/susconecta/lib/utils.ts @@ -4,3 +4,22 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +export function validarCPFLocal(cpf: string): boolean { + if (!cpf) return false; + cpf = cpf.replace(/[^\d]+/g, ""); + if (cpf.length !== 11) return false; + if (/^(\d)\1{10}$/.test(cpf)) return false; + + let soma = 0, resto = 0; + for (let i = 1; i <= 9; i++) soma += parseInt(cpf.substring(i - 1, i)) * (11 - i); + resto = (soma * 10) % 11; if (resto === 10 || resto === 11) resto = 0; + if (resto !== parseInt(cpf.substring(9, 10))) return false; + + soma = 0; + for (let i = 1; i <= 10; i++) soma += parseInt(cpf.substring(i - 1, i)) * (12 - i); + resto = (soma * 10) % 11; if (resto === 10 || resto === 11) resto = 0; + if (resto !== parseInt(cpf.substring(10, 11))) return false; + + return true; +} -- 2.47.2