// lib/api.ts export type ApiOk = { success?: boolean; data: T; message?: string; pagination?: { current_page?: number; per_page?: number; total_pages?: number; total?: number; }; }; // ===== TIPOS COMUNS ===== export type Endereco = { cep?: string; logradouro?: string; numero?: string; complemento?: string; bairro?: string; cidade?: string; estado?: string; }; // ===== PACIENTES ===== export type Paciente = { id: string; full_name: string; social_name?: string | null; cpf?: string; rg?: string | null; sex?: string | null; birth_date?: string | null; phone_mobile?: string; email?: string; cep?: string | null; street?: string | null; number?: string | null; complement?: string | null; neighborhood?: string | null; city?: string | null; state?: string | null; notes?: string | null; }; export type PacienteInput = { full_name: string; social_name?: string | null; cpf: string; rg?: string | null; sex?: string | null; birth_date?: string | null; phone_mobile?: string | null; email?: string | null; cep?: string | null; street?: string | null; number?: string | null; complement?: string | null; neighborhood?: string | null; city?: string | null; state?: string | null; notes?: string | null; }; // ===== MÉDICOS ===== export type FormacaoAcademica = { instituicao: string; curso: string; ano_conclusao: string; }; export type DadosBancarios = { banco: string; agencia: string; conta: string; tipo_conta: string; }; // ===== MÉDICOS ===== export type Medico = { id: string; full_name: string; // Altere 'nome' para 'full_name' 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; active?: boolean; cep?: string; city?: string; complement?: string; neighborhood?: string; number?: string; phone2?: string; state?: string; street?: string; created_at?: string; created_by?: string; updated_at?: string; updated_by?: string; user_id?: string; }; // ===== MÉDICOS ===== // ...existing code... export type MedicoInput = { user_id?: string | null; crm: string; crm_uf: string; specialty: string; full_name: string; cpf: string; email: string; phone_mobile: string; phone2?: string | null; cep: string; street: string; number: string; complement?: string; neighborhood?: string; city: string; state: string; birth_date: string | null; rg?: string | null; active?: boolean; created_by?: string | null; updated_by?: string | null; }; // ===== CONFIG ===== const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "https://yuanqfswhberkoevtmfr.supabase.co"; const REST = `${API_BASE}/rest/v1`; // 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") ); } // 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 (err) { console.error("Erro ao parsear a resposta:", err); } 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); } // Nova função para busca avançada de pacientes export async function buscarPacientes(termo: string): Promise { if (!termo || termo.trim().length < 2) { return []; } const searchTerm = termo.toLowerCase().trim(); const digitsOnly = searchTerm.replace(/\D/g, ''); // Monta queries para buscar em múltiplos campos const queries = []; // Busca por ID se parece com UUID if (searchTerm.includes('-') && searchTerm.length > 10) { queries.push(`id=eq.${searchTerm}`); } // Busca por CPF (com e sem formatação) if (digitsOnly.length >= 11) { queries.push(`cpf=eq.${digitsOnly}`); } else if (digitsOnly.length >= 3) { queries.push(`cpf=ilike.*${digitsOnly}*`); } // Busca por nome (usando ilike para busca case-insensitive) if (searchTerm.length >= 2) { queries.push(`full_name=ilike.*${searchTerm}*`); queries.push(`social_name=ilike.*${searchTerm}*`); } // Busca por email se contém @ if (searchTerm.includes('@')) { queries.push(`email=ilike.*${searchTerm}*`); } const results: Paciente[] = []; const seenIds = new Set(); // Executa as buscas e combina resultados únicos for (const query of queries) { try { const url = `${REST}/patients?${query}&limit=10`; const res = await fetch(url, { method: "GET", headers: baseHeaders() }); const arr = await parse(res); if (arr?.length > 0) { for (const paciente of arr) { if (!seenIds.has(paciente.id)) { seenIds.add(paciente.id); results.push(paciente); } } } } catch (error) { console.warn(`Erro na busca com query: ${query}`, error); } } return results.slice(0, 20); // Limita a 20 resultados } 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); } // Nova função para busca avançada de médicos export async function buscarMedicos(termo: string): Promise { if (!termo || termo.trim().length < 2) { return []; } const searchTerm = termo.toLowerCase().trim(); const digitsOnly = searchTerm.replace(/\D/g, ''); // Monta queries para buscar em múltiplos campos const queries = []; // Busca por ID se parece com UUID if (searchTerm.includes('-') && searchTerm.length > 10) { queries.push(`id=eq.${searchTerm}`); } // Busca por CRM (com e sem formatação) if (digitsOnly.length >= 3) { queries.push(`crm=ilike.*${digitsOnly}*`); } // Busca por nome (usando ilike para busca case-insensitive) if (searchTerm.length >= 2) { queries.push(`full_name=ilike.*${searchTerm}*`); queries.push(`nome_social=ilike.*${searchTerm}*`); } // Busca por email se contém @ if (searchTerm.includes('@')) { queries.push(`email=ilike.*${searchTerm}*`); } // Busca por especialidade if (searchTerm.length >= 2) { queries.push(`specialty=ilike.*${searchTerm}*`); } const results: Medico[] = []; const seenIds = new Set(); // Executa as buscas e combina resultados únicos for (const query of queries) { try { const url = `${REST}/doctors?${query}&limit=10`; const res = await fetch(url, { method: "GET", headers: baseHeaders() }); const arr = await parse(res); if (arr?.length > 0) { for (const medico of arr) { if (!seenIds.has(medico.id)) { seenIds.add(medico.id); results.push(medico); } } } } catch (error) { console.warn(`Erro na busca com query: ${query}`, error); } } return results.slice(0, 20); // Limita a 20 resultados } export async function buscarMedicoPorId(id: string | number): Promise { // Primeiro tenta buscar no Supabase (dados reais) try { const url = `${REST}/doctors?id=eq.${id}`; const res = await fetch(url, { method: "GET", headers: baseHeaders() }); const arr = await parse(res); if (arr && arr.length > 0) { console.log('✅ Médico encontrado no Supabase:', arr[0]); console.log('🔍 Campo especialidade no médico:', { especialidade: arr[0].especialidade, specialty: (arr[0] as any).specialty, hasEspecialidade: !!arr[0].especialidade, hasSpecialty: !!((arr[0] as any).specialty) }); return arr[0]; } } catch (error) { console.warn('⚠️ Erro ao buscar no Supabase, tentando mock API:', error); } // Se não encontrar no Supabase, tenta o mock API try { const url = `https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctors/${id}`; const res = await fetch(url, { method: "GET", headers: { "Accept": "application/json" } }); if (!res.ok) { if (res.status === 404) { throw new Error("404: Médico não encontrado"); } throw new Error(`Erro ao buscar médico: ${res.status} ${res.statusText}`); } const medico = await res.json(); console.log('✅ Médico encontrado no Mock API:', medico); return medico as Medico; } catch (error) { console.error('❌ Erro ao buscar médico em ambas as APIs:', error); throw new Error("404: Médico não encontrado"); } } // Dentro de lib/api.ts export async function criarMedico(input: MedicoInput): Promise { console.log("Enviando os dados para a API:", input); // Log para depuração const url = `${REST}/doctors`; // Endpoint de médicos const res = await fetch(url, { method: "POST", headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"), body: JSON.stringify(input), // Enviando os dados padronizados }); const arr = await parse(res); // Resposta da API return Array.isArray(arr) ? arr[0] : (arr as Medico); // Retorno do médico } export async function atualizarMedico(id: string | number, input: MedicoInput): Promise { console.log(`🔄 Tentando atualizar médico ID: ${id}`); console.log(`📤 Payload original:`, input); // Criar um payload limpo apenas com campos básicos que sabemos que existem const cleanPayload = { full_name: input.full_name, crm: input.crm, specialty: input.specialty, email: input.email, phone_mobile: input.phone_mobile, cpf: input.cpf, cep: input.cep, street: input.street, number: input.number, city: input.city, state: input.state, active: input.active ?? true }; console.log(`📤 Payload limpo:`, cleanPayload); // Atualizar apenas no Supabase (dados reais) try { const url = `${REST}/doctors?id=eq.${id}`; console.log(`🌐 URL de atualização: ${url}`); const res = await fetch(url, { method: "PATCH", headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"), body: JSON.stringify(cleanPayload), }); console.log(`📡 Resposta do servidor: ${res.status} ${res.statusText}`); if (res.ok) { const arr = await parse(res); const result = Array.isArray(arr) ? arr[0] : (arr as Medico); console.log('✅ Médico atualizado no Supabase:', result); return result; } else { // Vamos tentar ver o erro detalhado const errorText = await res.text(); console.error(`❌ Erro detalhado do Supabase:`, { status: res.status, statusText: res.statusText, response: errorText, headers: Object.fromEntries(res.headers.entries()) }); throw new Error(`Supabase error: ${res.status} ${res.statusText} - ${errorText}`); } } catch (error) { console.error('❌ Erro ao atualizar médico:', error); throw error; } } export async function excluirMedico(id: string | number): Promise { const url = `${REST}/doctors?id=eq.${id}`; const res = await fetch(url, { method: "DELETE", headers: baseHeaders() }); await parse(res); } // ===== 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 }; } } // ===== 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 {}