feat(api): add doctors and patients API integration #25

Merged
Jonasbomfim merged 1 commits from feature/api-med-pac into develop 2025-09-30 17:01:37 +00:00
7 changed files with 326 additions and 337 deletions
Showing only changes of commit 84cb4c36eb - Show all commits

View File

@ -14,6 +14,35 @@ import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-f
import { listarMedicos, excluirMedico, Medico } from "@/lib/api"; 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() { export default function DoutoresPage() {
const [doctors, setDoctors] = useState<Medico[]>([]); const [doctors, setDoctors] = useState<Medico[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -27,7 +56,8 @@ export default function DoutoresPage() {
setLoading(true); setLoading(true);
try { try {
const list = await listarMedicos({ limit: 50 }); const list = await listarMedicos({ limit: 50 });
setDoctors(list ?? []); setDoctors((list ?? []).map(normalizeMedico));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -53,6 +83,8 @@ export default function DoutoresPage() {
setShowForm(true); setShowForm(true);
} }
function handleEdit(id: string) { function handleEdit(id: string) {
setEditingId(id); setEditingId(id);
setShowForm(true); setShowForm(true);
@ -70,10 +102,29 @@ export default function DoutoresPage() {
} }
async function handleSaved() { function handleSaved(savedDoctor?: Medico) {
setShowForm(false); setShowForm(false);
await load();
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) { if (showForm) {
return ( return (

View File

@ -17,30 +17,31 @@ import { PatientRegistrationForm } from "@/components/forms/patient-registration
function normalizePaciente(p: any): Paciente { function normalizePaciente(p: any): Paciente {
const endereco: Endereco = { const endereco: Endereco = {
cep: p.endereco?.cep ?? p.cep ?? "", cep: p.endereco?.cep ?? p.cep ?? "",
logradouro: p.endereco?.logradouro ?? p.logradouro ?? "", logradouro: p.endereco?.logradouro ?? p.street ?? "",
numero: p.endereco?.numero ?? p.numero ?? "", numero: p.endereco?.numero ?? p.number ?? "",
complemento: p.endereco?.complemento ?? p.complemento ?? "", complemento: p.endereco?.complemento ?? p.complement ?? "",
bairro: p.endereco?.bairro ?? p.bairro ?? "", bairro: p.endereco?.bairro ?? p.neighborhood ?? "",
cidade: p.endereco?.cidade ?? p.cidade ?? "", cidade: p.endereco?.cidade ?? p.city ?? "",
estado: p.endereco?.estado ?? p.estado ?? "", estado: p.endereco?.estado ?? p.state ?? "",
}; };
return { return {
id: String(p.id ?? p.uuid ?? p.paciente_id ?? ""), id: String(p.id ?? p.uuid ?? p.paciente_id ?? ""),
nome: p.nome ?? "", nome: p.full_name ?? "", // 👈 troca nome → full_name
nome_social: p.nome_social ?? null, nome_social: p.social_name ?? null, // 👈 Supabase usa social_name
cpf: p.cpf ?? "", cpf: p.cpf ?? "",
rg: p.rg ?? null, rg: p.rg ?? p.document_number ?? null, // 👈 às vezes vem como document_number
sexo: p.sexo ?? null, sexo: p.sexo ?? p.sex ?? null, // 👈 Supabase usa sex
data_nascimento: p.data_nascimento ?? null, data_nascimento: p.data_nascimento ?? p.birth_date ?? null,
telefone: p.telefone ?? "", telefone: p.telefone ?? p.phone_mobile ?? "",
email: p.email ?? "", email: p.email ?? "",
endereco, endereco,
observacoes: p.observacoes ?? null, observacoes: p.observacoes ?? p.notes ?? null,
foto_url: p.foto_url ?? null, foto_url: p.foto_url ?? null,
}; };
} }
export default function PacientesPage() { export default function PacientesPage() {
const [patients, setPatients] = useState<Paciente[]>([]); const [patients, setPatients] = useState<Paciente[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);

View File

@ -4,9 +4,9 @@ import { AuthProvider } from "@/hooks/useAuth"
import "./globals.css" import "./globals.css"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "MediConecta - Conectando Pacientes e Profissionais de Saúde", title: "MediConnect - Conectando Pacientes e Profissionais de Saúde",
description: 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", keywords: "saúde, médicos, pacientes, agendamento, telemedicina, SUS",
generator: 'v0.app' generator: 'v0.app'
} }

View File

@ -61,7 +61,7 @@ export function Sidebar() {
{/* este span some no modo ícone */} {/* este span some no modo ícone */}
<span className="text-lg font-semibold text-sidebar-foreground group-data-[collapsible=icon]:hidden"> <span className="text-lg font-semibold text-sidebar-foreground group-data-[collapsible=icon]:hidden">
MediConecta MediConnect
</span> </span>
</Link> </Link>
</SidebarHeader> </SidebarHeader>

View File

@ -17,7 +17,6 @@ import {
Paciente, Paciente,
PacienteInput, PacienteInput,
buscarCepAPI, buscarCepAPI,
validarCPF,
criarPaciente, criarPaciente,
atualizarPaciente, atualizarPaciente,
uploadFotoPaciente, uploadFotoPaciente,
@ -28,6 +27,11 @@ import {
buscarPacientePorId, buscarPacientePorId,
} from "@/lib/api"; } from "@/lib/api";
import { validarCPFLocal } from "@/lib/utils";
import { verificarCpfDuplicado } from "@/lib/api";
type Mode = "create" | "edit"; type Mode = "create" | "edit";
export interface PatientRegistrationFormProps { export interface PatientRegistrationFormProps {
@ -192,13 +196,13 @@ export function PatientRegistrationForm({
telefone: form.telefone || null, telefone: form.telefone || null,
email: form.email || null, email: form.email || null,
endereco: { endereco: {
cep: form.cep || null, cep: form.cep || undefined,
logradouro: form.logradouro || null, logradouro: form.logradouro || undefined,
numero: form.numero || null, numero: form.numero || undefined,
complemento: form.complemento || null, complemento: form.complemento || undefined,
bairro: form.bairro || null, bairro: form.bairro || undefined,
cidade: form.cidade || null, cidade: form.cidade || undefined,
estado: form.estado || null, estado: form.estado || undefined,
}, },
observacoes: form.observacoes || null, observacoes: form.observacoes || null,
}; };
@ -210,18 +214,24 @@ export function PatientRegistrationForm({
try { try {
const { valido, existe } = await validarCPF(form.cpf); // 1) validação local
if (!valido) { if (!validarCPFLocal(form.cpf)) {
setErrors((e) => ({ ...e, cpf: "CPF inválido (validação externa)" })); setErrors((e) => ({ ...e, cpf: "CPF inválido" }));
return; return;
} }
if (existe && mode === "create") {
// 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" })); setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" }));
return; return;
} }
} catch {
} }
} catch (err) {
console.error("Erro ao validar CPF", err);
}
setSubmitting(true); setSubmitting(true);
try { try {

View File

@ -1,7 +1,7 @@
// lib/api.ts
export type ApiOk<T = any> = { export type ApiOk<T = any> = {
success: boolean; success?: boolean;
data: T; data: T;
message?: string; message?: string;
pagination?: { pagination?: {
@ -12,6 +12,7 @@ export type ApiOk<T = any> = {
}; };
}; };
// ===== TIPOS COMUNS =====
export type Endereco = { export type Endereco = {
cep?: string; cep?: string;
logradouro?: string; logradouro?: string;
@ -22,6 +23,7 @@ export type Endereco = {
estado?: string; estado?: string;
}; };
// ===== PACIENTES =====
export type Paciente = { export type Paciente = {
id: string; id: string;
nome?: string; nome?: string;
@ -46,241 +48,11 @@ export type PacienteInput = {
data_nascimento?: string | null; data_nascimento?: string | null;
telefone?: string | null; telefone?: string | null;
email?: string | null; email?: string | null;
endereco?: { endereco?: Endereco;
cep?: string | null;
logradouro?: string | null;
numero?: string | null;
complemento?: string | null;
bairro?: string | null;
cidade?: string | null;
estado?: string | null;
};
observacoes?: string | null; observacoes?: string | null;
}; };
// ===== MÉDICOS =====
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<string, string> {
const h: Record<string, string> = {};
// 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<T>(res: Response): Promise<T> {
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<Paciente[]> {
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<ApiOk<Paciente[]>>(res);
logAPI("listarPacientes", { url, result: data });
return data?.data ?? (data as any);
}
export async function buscarPacientePorId(id: string | number): Promise<Paciente> {
const url = `${API_BASE}${PATHS.pacienteId(id)}`;
const res = await fetch(url, { method: "GET", headers: headers("json") });
const data = await parse<ApiOk<Paciente>>(res);
logAPI("buscarPacientePorId", { url, result: data });
return data?.data ?? (data as any);
}
export async function criarPaciente(input: PacienteInput): Promise<Paciente> {
const url = `${API_BASE}${PATHS.pacientes}`;
const res = await fetch(url, { method: "POST", headers: headers("json"), body: JSON.stringify(input) });
const data = await parse<ApiOk<Paciente>>(res);
logAPI("criarPaciente", { url, payload: input, result: data });
return data?.data ?? (data as any);
}
export async function atualizarPaciente(id: string | number, input: PacienteInput): Promise<Paciente> {
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<ApiOk<Paciente>>(res);
logAPI("atualizarPaciente", { url, payload: input, result: data });
return data?.data ?? (data as any);
}
export async function excluirPaciente(id: string | number): Promise<void> {
const url = `${API_BASE}${PATHS.pacienteId(id)}`;
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
await parse<any>(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<ApiOk<{ foto_url?: string; thumbnail_url?: string }>>(res);
logAPI("uploadFotoPaciente", { url, payload: { file: file.name }, result: data });
return data?.data ?? (data as any);
}
export async function removerFotoPaciente(id: string | number): Promise<void> {
const url = `${API_BASE}${PATHS.foto(id)}`;
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
await parse<any>(res);
logAPI("removerFotoPaciente", { url, result: { ok: true } });
}
//
// Anexos
//
export async function listarAnexos(id: string | number): Promise<any[]> {
const url = `${API_BASE}${PATHS.anexos(id)}`;
const res = await fetch(url, { method: "GET", headers: headers("json") });
const data = await parse<ApiOk<any[]>>(res);
logAPI("listarAnexos", { url, result: data });
return data?.data ?? (data as any);
}
export async function adicionarAnexo(id: string | number, file: File): Promise<any> {
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<ApiOk<any>>(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<void> {
const url = `${API_BASE}${PATHS.anexoId(id, anexoId)}`;
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
await parse<any>(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<ApiOk<{ valido: boolean; existe: boolean; paciente_id: string | null }>>(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<any>(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 <<<
export type FormacaoAcademica = { export type FormacaoAcademica = {
instituicao: string; instituicao: string;
curso: string; curso: string;
@ -344,85 +116,221 @@ export type MedicoInput = {
valor_consulta?: number | string | null; valor_consulta?: number | string | null;
}; };
// // ===== CONFIG =====
// MÉDICOS (CRUD) const API_BASE =
// process.env.NEXT_PUBLIC_API_BASE ?? "https://yuanqfswhberkoevtmfr.supabase.co";
// ======= MÉDICOS (forçando usar rotas de PACIENTES no mock) ======= const REST = `${API_BASE}/rest/v1`;
export async function listarMedicos(params?: { page?: number; limit?: number; q?: string }): Promise<Medico[]> { // Token salvo no browser (aceita auth_token ou token)
const query = new URLSearchParams(); function getAuthToken(): string | null {
if (params?.page) query.set("page", String(params.page)); if (typeof window === "undefined") return null;
if (params?.limit) query.set("limit", String(params.limit)); return (
if (params?.q) query.set("q", params.q); localStorage.getItem("auth_token") ||
localStorage.getItem("token") ||
sessionStorage.getItem("auth_token") ||
sessionStorage.getItem("token")
);
}
// FORÇA /pacientes // Cabeçalhos base
const url = `${API_BASE}/pacientes${query.toString() ? `?${query.toString()}` : ""}`; function baseHeaders(): Record<string, string> {
const res = await fetch(url, { method: "GET", headers: headers("json") }); const h: Record<string, string> = {
const data = await parse<ApiOk<Medico[]>>(res); apikey:
return (data as any)?.data ?? (data as any); "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<string, string>, prefer: string) {
return { ...h, Prefer: prefer };
}
// Parse genérico
async function parse<T>(res: Response): Promise<T> {
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<string, string> {
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<Paciente[]> {
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<Paciente[]>(res);
}
export async function buscarPacientePorId(id: string | number): Promise<Paciente> {
const url = `${REST}/patients?id=eq.${id}`;
const res = await fetch(url, { method: "GET", headers: baseHeaders() });
const arr = await parse<Paciente[]>(res);
if (!arr?.length) throw new Error("404: Paciente não encontrado");
return arr[0];
}
export async function criarPaciente(input: PacienteInput): Promise<Paciente> {
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<Paciente[] | Paciente>(res);
return Array.isArray(arr) ? arr[0] : (arr as Paciente);
}
export async function atualizarPaciente(id: string | number, input: PacienteInput): Promise<Paciente> {
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<Paciente[] | Paciente>(res);
return Array.isArray(arr) ? arr[0] : (arr as Paciente);
}
export async function excluirPaciente(id: string | number): Promise<void> {
const url = `${REST}/patients?id=eq.${id}`;
const res = await fetch(url, { method: "DELETE", headers: baseHeaders() });
await parse<any>(res);
}
// ===== PACIENTES (Extra: verificação de CPF duplicado) =====
export async function verificarCpfDuplicado(cpf: string): Promise<boolean> {
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<Medico[]> {
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<Medico[]>(res);
} }
export async function buscarMedicoPorId(id: string | number): Promise<Medico> { export async function buscarMedicoPorId(id: string | number): Promise<Medico> {
const url = `${API_BASE}/pacientes/${id}`; // FORÇA /pacientes const url = `${REST}/doctors?id=eq.${id}`;
const res = await fetch(url, { method: "GET", headers: headers("json") }); const res = await fetch(url, { method: "GET", headers: baseHeaders() });
const data = await parse<ApiOk<Medico>>(res); const arr = await parse<Medico[]>(res);
return (data as any)?.data ?? (data as any); if (!arr?.length) throw new Error("404: Médico não encontrado");
return arr[0];
} }
export async function criarMedico(input: MedicoInput): Promise<Medico> { export async function criarMedico(input: MedicoInput): Promise<Medico> {
const url = `${API_BASE}/pacientes`; // FORÇA /pacientes const url = `${REST}/doctors`;
const res = await fetch(url, { method: "POST", headers: headers("json"), body: JSON.stringify(input) }); const res = await fetch(url, {
const data = await parse<ApiOk<Medico>>(res); method: "POST",
return (data as any)?.data ?? (data as any); headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"),
body: JSON.stringify(input),
});
const arr = await parse<Medico[] | Medico>(res);
return Array.isArray(arr) ? arr[0] : (arr as Medico);
} }
export async function atualizarMedico(id: string | number, input: MedicoInput): Promise<Medico> { export async function atualizarMedico(id: string | number, input: MedicoInput): Promise<Medico> {
const url = `${API_BASE}/pacientes/${id}`; // FORÇA /pacientes const url = `${REST}/doctors?id=eq.${id}`;
const res = await fetch(url, { method: "PUT", headers: headers("json"), body: JSON.stringify(input) }); const res = await fetch(url, {
const data = await parse<ApiOk<Medico>>(res); method: "PATCH",
return (data as any)?.data ?? (data as any); headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"),
body: JSON.stringify(input),
});
const arr = await parse<Medico[] | Medico>(res);
return Array.isArray(arr) ? arr[0] : (arr as Medico);
} }
export async function excluirMedico(id: string | number): Promise<void> { export async function excluirMedico(id: string | number): Promise<void> {
const url = `${API_BASE}/pacientes/${id}`; // FORÇA /pacientes const url = `${REST}/doctors?id=eq.${id}`;
const res = await fetch(url, { method: "DELETE", headers: headers("json") }); const res = await fetch(url, { method: "DELETE", headers: baseHeaders() });
await parse<any>(res); await parse<any>(res);
} }
export async function uploadFotoMedico(id: string | number, file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> { // ===== CEP (usado nos formulários) =====
const url = `${API_BASE}/pacientes/${id}/foto`; // FORÇA /pacientes export async function buscarCepAPI(cep: string): Promise<{
const fd = new FormData(); logradouro?: string;
fd.append("foto", file); bairro?: string;
const res = await fetch(url, { method: "POST", headers: headers("form"), body: fd }); localidade?: string;
const data = await parse<ApiOk<{ foto_url?: string; thumbnail_url?: string }>>(res); uf?: string;
return (data as any)?.data ?? (data as any); 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<void> { // ===== Stubs pra não quebrar imports dos forms (sem rotas de storage na doc) =====
const url = `${API_BASE}/pacientes/${id}/foto`; // FORÇA /pacientes export async function listarAnexos(_id: string | number): Promise<any[]> { return []; }
const res = await fetch(url, { method: "DELETE", headers: headers("json") }); export async function adicionarAnexo(_id: string | number, _file: File): Promise<any> { return {}; }
await parse<any>(res); export async function removerAnexo(_id: string | number, _anexoId: string | number): Promise<void> {}
} 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<void> {}
export async function listarAnexosMedico(id: string | number): Promise<any[]> { export async function listarAnexosMedico(_id: string | number): Promise<any[]> { return []; }
const url = `${API_BASE}/pacientes/${id}/anexos`; // FORÇA /pacientes export async function adicionarAnexoMedico(_id: string | number, _file: File): Promise<any> { return {}; }
const res = await fetch(url, { method: "GET", headers: headers("json") }); export async function removerAnexoMedico(_id: string | number, _anexoId: string | number): Promise<void> {}
const data = await parse<ApiOk<any[]>>(res); export async function uploadFotoMedico(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> { return {}; }
return (data as any)?.data ?? (data as any); export async function removerFotoMedico(_id: string | number): Promise<void> {}
}
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 =======

View File

@ -4,3 +4,22 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) 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;
}