import { Agendamento, Orcamento, Dentista, DentistaHorario, Especialidade, EvolucaoOrto, FinanceiroItem, Lead, Notificacao, Paciente, Plano, Procedimento, Settings, TratamentoOrto } from '../types.ts'; // Helper to enforce uppercase on strings const toUpper = (str: string | undefined) => str ? str.toUpperCase() : ''; // Helper to process object properties to uppercase const enforceUppercase = (data: T): T => { const newData: any = { ...data }; for (const key in newData) { if (typeof newData[key] === 'string' && key !== 'id' && key !== 'email' && key !== 'start' && key !== 'end' && !key.includes('data') && !key.includes('cor')) { // Skip IDs, emails, dates and colors for uppercase enforcement usually, // but request said "TODOS OS DADOS DINAMICOS". We will be aggressive but keep IDs and technical fields intact. // We'll primarily target user-visible text fields. if (key !== 'corAgenda' && key !== 'corCartao') { newData[key] = newData[key].toUpperCase(); } } } return newData; }; const API_URL = import.meta.env.VITE_API_URL || '/api'; // Returns headers always including the JWT token stored after login let _sessionExpiredHandled = false; const apiFetch = async (url: string, options: RequestInit = {}) => { const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN'); const headers = { ...options.headers, ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }; const res = await fetch(url, { ...options, headers }); // Sessão expirada: o token foi enviado mas o servidor rejeitou (401, p.ex. JWT > 12h). // Limpa a sessão e recarrega para o login (uma única vez, p/ evitar loop de reloads). // Não dispara quando não há token (ex.: tentativa de login com senha errada). if (res.status === 401 && token && !_sessionExpiredHandled) { _sessionExpiredHandled = true; try { ['SCOREODONTO_AUTH_TOKEN', 'SCOREODONTO_USER_DATA', 'SCOREODONTO_WORKSPACES', 'SCOREODONTO_ACTIVE_WORKSPACE', 'SCOREODONTO_PREVIEW_ROLE'] .forEach(k => localStorage.removeItem(k)); } catch { /* ignore */ } window.location.href = '/?session=expired'; window.location.reload(); } return res; }; export const HybridBackend = { dbConfig: { database: 'scoreodonto' }, // --- AUTH METHODS --- login: async (email: string, pass: string): Promise<{ ok: boolean; noWorkspace?: boolean; userRole?: string }> => { const cleanEmail = email.toLowerCase().trim(); const cleanPass = pass.trim(); try { const res = await apiFetch(`${API_URL}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: cleanEmail, password: cleanPass }) }); if (res.ok) { const data = await res.json(); localStorage.removeItem('SCOREODONTO_PREVIEW_ROLE'); // garante que não herda modo visualização localStorage.setItem('SCOREODONTO_AUTH_TOKEN', data.token); localStorage.setItem('SCOREODONTO_USER_DATA', JSON.stringify(data.user)); localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify(data.workspaces || [])); if (data.mustChangePassword) localStorage.setItem('SCOREODONTO_MUST_CHANGE_PW', '1'); else localStorage.removeItem('SCOREODONTO_MUST_CHANGE_PW'); if (data.workspaces && data.workspaces.length > 0) { localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(data.workspaces[0])); } // Hidrata os plugins ativados na conta do usuário (fonte da verdade no servidor). await HybridBackend.hydrateMyPlugins(); if (data.noWorkspace) { return { ok: true, noWorkspace: true, userRole: data.userRole }; } return { ok: true }; } return { ok: false }; } catch (err) { console.error('Login error:', err); return { ok: false }; } }, // --- CLINIC MANAGEMENT --- createClinica: async (nome_fantasia: string, documento?: string): Promise<{ success: boolean; clinica?: any; message?: string }> => { try { const res = await apiFetch(`${API_URL}/clinicas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nome_fantasia, documento }), }); return await res.json(); } catch (err) { return { success: false, message: 'Erro de conexão.' }; } }, logout: () => { localStorage.removeItem('SCOREODONTO_AUTH_TOKEN'); localStorage.removeItem('SCOREODONTO_USER_DATA'); localStorage.removeItem('SCOREODONTO_WORKSPACES'); localStorage.removeItem('SCOREODONTO_ACTIVE_WORKSPACE'); localStorage.removeItem('SCOREODONTO_PREVIEW_ROLE'); window.location.href = '/'; window.location.reload(); }, // Renomeia a unidade (clínica/consultório/laboratório) e sincroniza os caches locais // para a UI refletir o novo nome sem precisar relogar. renameWorkspace: async (clinicaId: string, nome: string): Promise<{ success: boolean; nome?: string; message?: string }> => { const res = await apiFetch(`${API_URL}/clinicas/${clinicaId}/nome`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nome_fantasia: nome }), }); const data = await res.json(); if (data?.success) { const finalNome = data.nome || nome.toUpperCase(); HybridBackend._syncWorkspaceNomeLocal(clinicaId, finalNome); } return data; }, // Atualiza o nome da unidade nos caches locais (workspaces + active) p/ a UI refletir na hora. _syncWorkspaceNomeLocal: (clinicaId: string, nome: string) => { try { const ws = JSON.parse(localStorage.getItem('SCOREODONTO_WORKSPACES') || '[]') .map((w: any) => (w.id === clinicaId ? { ...w, nome } : w)); localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify(ws)); const aw = JSON.parse(localStorage.getItem('SCOREODONTO_ACTIVE_WORKSPACE') || 'null'); if (aw && aw.id === clinicaId) localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify({ ...aw, nome })); } catch { /* ignore */ } }, // --- PERFIL DA UNIDADE (clínica/consultório/laboratório) --- getClinicaProfile: async (clinicaId: string): Promise<{ success: boolean; clinica?: any; message?: string }> => { const res = await apiFetch(`${API_URL}/clinicas/${clinicaId}`); return res.json(); }, updateClinicaProfile: async (clinicaId: string, data: any): Promise<{ success: boolean; nome?: string; message?: string }> => { const res = await apiFetch(`${API_URL}/clinicas/${clinicaId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); const d = await res.json(); if (d?.success && d.nome) HybridBackend._syncWorkspaceNomeLocal(clinicaId, d.nome); return d; }, // --- PERFIL DO SISTEMA (usuário logado) --- getMeuPerfil: async (): Promise<{ success: boolean; usuario?: any; message?: string }> => { const res = await apiFetch(`${API_URL}/usuarios/me`); return res.json(); }, updateMeuPerfil: async (data: any): Promise<{ success: boolean; message?: string }> => { const res = await apiFetch(`${API_URL}/usuarios/me`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); const d = await res.json(); if (d?.success) { try { const ud = JSON.parse(localStorage.getItem('SCOREODONTO_USER_DATA') || 'null'); if (ud) { if (data.nome !== undefined) ud.nome = String(data.nome).toUpperCase(); if (data.email !== undefined) ud.email = String(data.email).toLowerCase().trim(); localStorage.setItem('SCOREODONTO_USER_DATA', JSON.stringify(ud)); } } catch { /* ignore */ } } return d; }, changeMyPassword: async (atual: string | undefined, nova: string): Promise<{ success: boolean; message?: string }> => { const res = await apiFetch(`${API_URL}/usuarios/me/senha`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ atual, nova }), }); const d = await res.json(); if (d?.success) { try { localStorage.removeItem('SCOREODONTO_MUST_CHANGE_PW'); } catch { /* ignore */ } } return d; }, mustChangePassword: (): boolean => { try { return localStorage.getItem('SCOREODONTO_MUST_CHANGE_PW') === '1'; } catch { return false; } }, // --- BACKUP POR CLÍNICA (Google Sheets, planilha própria da unidade) --- getClinicaSyncStatus: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; if (!clinicaId) return { success: false, connected: false, counts: {} }; try { const res = await apiFetch(`${API_URL}/clinica-sync/status?clinicaId=${clinicaId}`); return await res.json(); } catch { return { success: false, connected: false, counts: {} }; } }, converterLead: async (leadId: string, payload: { data?: string; hora?: string; dentistaId?: string }): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/leads/${leadId}/converter`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...payload, clinicaId }), }); return await res.json().catch(() => ({ success: res.ok })); }, clinicaSyncPush: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/clinica-sync/push`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId }), }); return res.json(); }, clinicaSyncPull: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/clinica-sync/pull`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId }), }); return res.json(); }, // Exporta os pacientes da unidade como CSV (download). Não precisa de Google. exportPacientesCsv: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/pacientes/export?clinicaId=${clinicaId}`, {}); if (!res.ok) throw new Error('Falha ao exportar.'); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `pacientes_${new Date().toISOString().slice(0, 10)}.csv`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); }, // Importa SOMENTE pacientes de uma planilha EXISTENTE (ID/URL), sem alterar o backup. importPacientesDaPlanilha: async (sheetsId: string): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/clinica-sync/import-pacientes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId, sheetsId }), }); return res.json(); }, // --- SUPERADMIN: MODO VISUALIZAÇÃO ("ver como" perfil, sem dados de terceiros) --- // Cria um workspace sintético com a role escolhida. Não há clínica real associada // (id inexistente), então as telas escopadas por clínica vêm vazias — nenhum dado // de outros usuários é exposto. O token continua sendo o do superadmin. isPreviewMode: (): boolean => !!localStorage.getItem('SCOREODONTO_PREVIEW_ROLE'), getPreviewRole: (): string | null => localStorage.getItem('SCOREODONTO_PREVIEW_ROLE'), enterPreview: (role: string) => { const synthetic = { id: '00000000-0000-0000-0000-000000000000', // UUID inexistente → consultas retornam vazio nome: `VISUALIZAÇÃO: ${role.toUpperCase()}`, role, cor: '#dc2626', _preview: true, }; localStorage.setItem('SCOREODONTO_PREVIEW_ROLE', role); localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(synthetic)); localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify([synthetic])); // Os plugins ativos no preview vêm de getActivePlugins() (preview-aware): todos // ficam ativos enquanto SCOREODONTO_PREVIEW_ROLE existir. Nada a mutar aqui. const path = role === 'paciente' ? '/tratamentos' : '/dashboard'; window.location.assign(path); }, exitPreview: () => { localStorage.removeItem('SCOREODONTO_PREVIEW_ROLE'); localStorage.removeItem('SCOREODONTO_ACTIVE_WORKSPACE'); localStorage.removeItem('SCOREODONTO_WORKSPACES'); window.location.assign('/superadmin/overview'); }, // --- PLUGINS POR USUÁRIO (add-on por conta; fonte da verdade no servidor) --- getMyPlugins: async (): Promise => { try { const res = await apiFetch(`${API_URL}/me/plugins`); if (!res.ok) return []; const d = await res.json(); return Array.isArray(d?.plugins) ? d.plugins : []; } catch { return []; } }, setMyPlugin: async (pluginId: string, ativo: boolean): Promise => { try { const res = await apiFetch(`${API_URL}/me/plugins`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pluginId, ativo }), }); return res.ok; } catch { return false; } }, // Reconcilia o cache local (SCOREODONTO_ACTIVE_PLUGINS) com o servidor. // Escopo atual: só 'locacao-salas' é server-backed; demais plugins seguem locais. // Não mexe durante o modo visualização (preview sobrescreve os plugins temporariamente). hydrateMyPlugins: async (): Promise => { if (localStorage.getItem('SCOREODONTO_PREVIEW_ROLE')) return; const server = await HybridBackend.getMyPlugins(); let local: string[] = []; try { local = JSON.parse(localStorage.getItem('SCOREODONTO_ACTIVE_PLUGINS') || '[]'); } catch { local = []; } const next = local.filter(p => p !== 'locacao-salas'); if (server.includes('locacao-salas')) next.push('locacao-salas'); localStorage.setItem('SCOREODONTO_ACTIVE_PLUGINS', JSON.stringify(next)); }, isAuthenticated: (): boolean => { return !!localStorage.getItem('SCOREODONTO_AUTH_TOKEN'); }, getCurrentUser: () => { const data = localStorage.getItem('SCOREODONTO_USER_DATA'); if (!data) return null; try { return JSON.parse(data).email; } catch { return null; } }, getCurrentUserName: () => { const data = localStorage.getItem('SCOREODONTO_USER_DATA'); if (!data) return 'USUÁRIO'; try { return JSON.parse(data).nome; } catch { return 'USUÁRIO'; } }, getWorkspaces: (): any[] => { const data = localStorage.getItem('SCOREODONTO_WORKSPACES'); return data ? JSON.parse(data) : []; }, // Recarrega a lista de workspaces (clínicas + salas) do servidor — ex.: após criar uma sala. refreshWorkspaces: async (): Promise => { try { const res = await apiFetch(`${API_URL}/me/workspaces`); const d = await res.json().catch(() => ({})); if (d?.success) { localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify(d.workspaces || [])); if (!localStorage.getItem('SCOREODONTO_ACTIVE_WORKSPACE') && (d.workspaces || []).length) { localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(d.workspaces[0])); } return d.workspaces || []; } } catch { /* ignore */ } return HybridBackend.getWorkspaces(); }, getActiveWorkspace: (): any | null => { const data = localStorage.getItem('SCOREODONTO_ACTIVE_WORKSPACE'); return data ? JSON.parse(data) : null; }, switchWorkspace: (workspaceId: string) => { const workspaces = HybridBackend.getWorkspaces(); const workspace = workspaces.find(w => w.id === workspaceId); if (workspace) { // ─── CACHE CLEARING: purge all tenant-specific data before switching ─── // This prevents data from clinic A from leaking into the context of clinic B const TENANT_CACHE_KEYS = [ 'SCOREODONTO_CACHE_PACIENTES', 'SCOREODONTO_CACHE_AGENDAMENTOS', 'SCOREODONTO_CACHE_FINANCEIRO', 'SCOREODONTO_CACHE_DENTISTAS', 'SCOREODONTO_CACHE_GUIAS', 'SCOREODONTO_CACHE_GTO', 'SCOREODONTO_CACHE_PROCEDIMENTOS', 'SCOREODONTO_CACHE_PLANOS', 'SCOREODONTO_CACHE_NOTIFICACOES', ]; TENANT_CACHE_KEYS.forEach(k => localStorage.removeItem(k)); console.log(`[SECURITY] Cache cleared for tenant switch to: ${workspaceId}`); // ─── Set new active workspace and reload to flush React state completely ─── localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(workspace)); window.location.reload(); } }, getCurrentRole: (): string => { const workspace = HybridBackend.getActiveWorkspace(); if (workspace?.role) return workspace.role; // Superadmin não pertence a nenhuma clínica (sem workspace ativo) → // usa o role da conta armazenado no login. try { const data = localStorage.getItem('SCOREODONTO_USER_DATA'); const accountRole = data ? JSON.parse(data).role : null; if (accountRole === 'superadmin') return 'superadmin'; } catch { /* ignore */ } return 'paciente'; }, getDashboardStats: async () => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/dashboard/stats${cid ? `?clinicaId=${encodeURIComponent(cid)}` : ''}`); return await res.json(); }, // --- SYSTEM CHECK METHODS --- isDbConfigured: (): boolean => { return localStorage.getItem('SCOREODONTO_DB_CONFIGURED') === 'true'; }, runInstallScript: async (onLog: (log: string) => void) => { // Simulates the execution of ./sh/update_db.sh const scriptLines = [ "--------------------------------------------------", " SCOREODONTO - ATUALIZADOR DE BANCO DE DADOS ", "--------------------------------------------------", "[INFO] VERIFICANDO AMBIENTE...", "[OK] AMBIENTE POSTGRESQL DETECTADO.", `[INFO] CONECTANDO DB: ${HybridBackend.dbConfig.database}...`, "[INFO] INICIANDO BACKUP DE SEGURANCA...", `[OK] BACKUP SALVO EM: ./bkp_database/BKP_${new Date().toISOString().replace(/[:.]/g, '-')}.SQL`, "[INFO] CONECTANDO API GOOGLE SHEETS...", "[INFO] LENDO PLANILHA: 'Dados Pacientes'...", " > 1.240 LINHAS ENCONTRADAS.", " > VALIDANDO DADOS (CPF, TELEFONE)...", "[INFO] SINCRONIZANDO COM POSTGRESQL (scoreodonto)...", " > FORCANDO PADRAO 'UPPERCASE' EM TODOS OS CAMPOS TEXTUAIS...", " > ATUALIZANDO TABELA: PACIENTES...", " > ATUALIZANDO TABELA: FINANCEIRO...", " > ATUALIZANDO TABELA: AGENDA...", "[INFO] LIMPANDO CACHE DE SESSAO...", "--------------------------------------------------", " ATUALIZACAO CONCLUIDA COM SUCESSO! ", "--------------------------------------------------" ]; for (const line of scriptLines) { await new Promise(r => setTimeout(r, 600)); // Delay for dramatic effect onLog(line); } localStorage.setItem('SCOREODONTO_DB_CONFIGURED', 'true'); return true; }, syncGoogleSheetsToPg: async (onProgress: (log: string) => void) => { onProgress("INICIANDO CONEXÃO COM GOOGLE SHEETS..."); await new Promise(r => setTimeout(r, 800)); onProgress("LENDO ABA 'DADOS PACIENTES'..."); await new Promise(r => setTimeout(r, 800)); onProgress(`CONECTANDO AO POSTGRESQL (${HybridBackend.dbConfig.database})...`); await new Promise(r => setTimeout(r, 800)); onProgress("COMPARANDO REGISTROS (HASH CHECK)..."); await new Promise(r => setTimeout(r, 1000)); onProgress("INSERINDO 2 NOVOS PACIENTES NO POSTGRESQL..."); onProgress("ATUALIZANDO 4 AGENDAMENTOS..."); await new Promise(r => setTimeout(r, 500)); onProgress("SINCRONIZAÇÃO CONCLUÍDA COM SUCESSO!"); return true; }, getPacientes: async (termo: string = '', sort: string = 'recentes'): Promise<{ data: Paciente[]; total: number }> => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const params = new URLSearchParams({ clinicaId }); if (termo.trim()) params.set('q', termo.trim()); if (sort) params.set('sort', sort); const res = await apiFetch(`${API_URL}/pacientes?${params}`); return res.json(); }, checkCpfExists: async (cpf: string): Promise => { const cleanCpf = cpf.replace(/\D/g, ''); if (!cleanCpf) return false; const res = await apiFetch(`${API_URL}/pacientes?q=${encodeURIComponent(cpf)}`); const { data } = await res.json(); const pacientes: Paciente[] = data || []; return pacientes.some(p => p.cpf.replace(/\D/g, '') === cleanCpf); }, savePaciente: async (paciente: Partial) => { const data = enforceUppercase(paciente); const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const body = { ...data, id: data.id || `p_${Date.now()}`, clinica_id: clinicaId }; const res = await apiFetch(`${API_URL}/pacientes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return await res.json(); }, updatePaciente: async (paciente: Paciente) => { const data = enforceUppercase(paciente); const res = await apiFetch(`${API_URL}/pacientes/${paciente.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return await res.json(); }, getGruposFamiliares: async (q?: string): Promise => { const url = q?.trim() ? `${API_URL}/grupos-familiares?q=${encodeURIComponent(q)}` : `${API_URL}/grupos-familiares`; const res = await apiFetch(url); const data = await res.json(); return Array.isArray(data) ? data : []; }, criarGrupoFamiliar: async (nome: string, responsavel_id?: string) => { const res = await apiFetch(`${API_URL}/grupos-familiares`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nome, responsavel_id }) }); return res.json(); }, getMembrosFamilia: async (grupoId: string) => { const res = await apiFetch(`${API_URL}/grupos-familiares/${grupoId}/membros`); return res.json(); }, getModelosReceita: async (especialidadeId?: string): Promise => { const url = especialidadeId ? `${API_URL}/modelos-receita?especialidadeId=${especialidadeId}` : `${API_URL}/modelos-receita`; const res = await apiFetch(url); const data = await res.json(); return Array.isArray(data) ? data : []; }, saveModeloReceita: async (modelo: Omit) => { const res = await apiFetch(`${API_URL}/modelos-receita`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modelo) }); return res.json(); }, updateModeloReceita: async (modelo: import('../types.ts').ModeloReceita) => { const res = await apiFetch(`${API_URL}/modelos-receita/${modelo.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modelo) }); return res.json(); }, deleteModeloReceita: async (id: string) => { await apiFetch(`${API_URL}/modelos-receita/${id}`, { method: 'DELETE' }); }, savePedidoExame: async (pedido: Omit) => { const res = await apiFetch(`${API_URL}/pedidos-exame`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(pedido) }); return res.json(); }, getPedidosExame: async (pacienteId?: string): Promise => { const url = pacienteId ? `${API_URL}/pedidos-exame?pacienteId=${pacienteId}` : `${API_URL}/pedidos-exame`; const res = await apiFetch(url); const data = await res.json(); return Array.isArray(data) ? data : []; }, getDentistas: async (clinicaId?: string): Promise => { const cid = clinicaId || HybridBackend.getActiveWorkspace()?.id || ''; const url = cid ? `${API_URL}/dentistas?clinicaId=${encodeURIComponent(cid)}` : `${API_URL}/dentistas`; const res = await apiFetch(url); if (!res.ok) return []; const data = await res.json(); return Array.isArray(data) ? data : []; }, getPlanos: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/planos${clinicaId ? `?clinicaId=${clinicaId}` : ''}`); return await res.json(); }, updateDentista: async (dentista: Dentista) => { const data = enforceUppercase(dentista); const res = await apiFetch(`${API_URL}/dentistas/${dentista.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return await res.json(); }, saveDentista: async (dentista: Partial) => { const activeW = HybridBackend.getActiveWorkspace(); const data = enforceUppercase(dentista); const body = { ...data, id: Math.random().toString(), clinica_id: activeW?.id }; const res = await apiFetch(`${API_URL}/dentistas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return await res.json(); }, reorderDentistas: async (orders: { id: string; ordem: number }[]) => { const res = await apiFetch(`${API_URL}/dentistas/reorder`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orders }) }); return await res.json(); }, deleteDentista: async (id: string) => { await apiFetch(`${API_URL}/dentistas/${id}`, { method: 'DELETE' }); }, getHorariosByDentista: async (dentistaId: string, clinicaId: string): Promise => { const res = await apiFetch(`${API_URL}/dentistas/${dentistaId}/horarios?clinicaId=${clinicaId}`); return await res.json(); }, saveHorario: async (horario: Partial) => { const res = await apiFetch(`${API_URL}/dentistas/horarios`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(horario) }); return await res.json(); }, deleteHorario: async (id: string) => { await apiFetch(`${API_URL}/dentistas/horarios/${id}`, { method: 'DELETE' }); }, getProductivityReport: async (clinicaId: string, start?: string, end?: string) => { const res = await apiFetch(`${API_URL}/reports/productivity?clinicaId=${clinicaId}&start=${start || ''}&end=${end || ''}`); return await res.json(); }, // ── V2 Comissão / Repasse ────────────────────────────────────────────────── getComissaoRegras: async () => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/comissoes/regras?clinicaId=${encodeURIComponent(clinicaId)}`); return await res.json(); }, saveComissaoRegra: async (regra: { procedimento_id?: string | null; profissional_id?: string | null; percentual: number; custo_lab?: number }) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/comissoes/regras?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(regra) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao salvar regra'); return await res.json(); }, deleteComissaoRegra: async (id: string) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; await apiFetch(`${API_URL}/comissoes/regras/${id}?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'DELETE' }); }, getComissoesRelatorio: async (opts: { inicio?: string; fim?: string; base?: string; profissionalId?: string } = {}) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const qs = new URLSearchParams({ clinicaId }); if (opts.inicio) qs.set('inicio', opts.inicio); if (opts.fim) qs.set('fim', opts.fim); if (opts.base) qs.set('base', opts.base); if (opts.profissionalId) qs.set('profissionalId', opts.profissionalId); const res = await apiFetch(`${API_URL}/comissoes/relatorio?${qs.toString()}`); return await res.json(); }, fecharRepasse: async (opts: { inicio: string; fim: string; base: string; profissionalId?: string }) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/repasses/fechar?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(opts) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao fechar repasse'); return await res.json(); }, getRepasses: async () => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/repasses?clinicaId=${encodeURIComponent(clinicaId)}`); return await res.json(); }, cancelarRepasse: async (id: string) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; await apiFetch(`${API_URL}/repasses/${id}/cancelar?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); }, pagarRepasse: async (id: string, formapagamento = 'Pix') => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/repasses/${id}/pagar?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ formapagamento }) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao pagar repasse'); return await res.json(); }, uploadRepasseComprovante: async (id: string, file: File) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const fd = new FormData(); fd.append('arquivo', file); const res = await apiFetch(`${API_URL}/repasses/${id}/comprovante?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'POST', body: fd }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao anexar comprovante'); return await res.json(); }, // ── Glosas de convênio ────────────────────────────────────────────────────── getGlosas: async (status?: string) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const qs = new URLSearchParams({ clinicaId }); if (status) qs.set('status', status); const res = await apiFetch(`${API_URL}/glosas?${qs.toString()}`); return await res.json(); }, getGlosaRecebiveis: async () => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/glosas/recebiveis?clinicaId=${encodeURIComponent(clinicaId)}`); return await res.json(); }, getGlosaItens: async (financeiroId: string) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/glosas/itens/${financeiroId}?clinicaId=${encodeURIComponent(clinicaId)}`); return await res.json(); }, recorrerGlosa: async (id: string, payload: { protocolo?: string; prazo_recurso?: string }) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/glosas/${id}/recorrer?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao abrir recurso'); return await res.json(); }, registrarGlosa: async (payload: { financeiro_id: string; valor: number; motivo?: string; gto_item_id?: string; item_descricao?: string; protocolo?: string; prazo_recurso?: string }) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/glosas?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao registrar glosa'); return await res.json(); }, recuperarGlosa: async (id: string) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/glosas/${id}/recuperar?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao recuperar glosa'); return await res.json(); }, deleteGlosa: async (id: string) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; await apiFetch(`${API_URL}/glosas/${id}?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'DELETE' }); }, // ── Próteses · Ordem de Serviço (clínica ↔ laboratório/protético) ─────────── getProteseLaboratorios: async () => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/protese/laboratorios?clinicaId=${encodeURIComponent(clinicaId)}`); return await res.json(); }, getProteseOS: async (opts: { status?: string; paciente_id?: string } = {}) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const qs = new URLSearchParams({ clinicaId }); if (opts.status) qs.set('status', opts.status); if (opts.paciente_id) qs.set('paciente_id', opts.paciente_id); const res = await apiFetch(`${API_URL}/protese/os?${qs.toString()}`); return await res.json(); }, getProteseBancada: async (status?: string) => { const qs = status ? `?status=${encodeURIComponent(status)}` : ''; const res = await apiFetch(`${API_URL}/protese/bancada${qs}`); return await res.json(); }, getProteseOSDetalhe: async (id: string) => { const res = await apiFetch(`${API_URL}/protese/os/${id}`); return await res.json(); }, createProteseOS: async (payload: any) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/protese/os?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao criar OS'); return await res.json(); }, setProteseOSStatus: async (id: string, status: string, observacao?: string) => { const res = await apiFetch(`${API_URL}/protese/os/${id}/status`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status, observacao }) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao mudar status'); return await res.json(); }, uploadProteseFoto: async (id: string, file: File, legenda?: string, ocorrenciaId?: string) => { const fd = new FormData(); fd.append('foto', file); if (legenda) fd.append('legenda', legenda); if (ocorrenciaId) fd.append('ocorrencia_id', ocorrenciaId); const res = await apiFetch(`${API_URL}/protese/os/${id}/foto`, { method: 'POST', body: fd }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao anexar foto'); return await res.json(); }, // Conferência de componente (status: ok | divergente | ausente | pendente) conferirProteseComponente: async (compId: string, status_conferencia: string) => { const res = await apiFetch(`${API_URL}/protese/os/componentes/${compId}/conferir`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status_conferencia }) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao conferir'); return await res.json(); }, // Ocorrências (contraditório) abrirProteseOcorrencia: async (osId: string, body: { categoria: string; descricao: string; responsavel?: string; componente_id?: string }) => { const res = await apiFetch(`${API_URL}/protese/os/${osId}/ocorrencias`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao abrir ocorrência'); return await res.json(); }, responderProteseOcorrencia: async (ocId: string, resposta: string) => { const res = await apiFetch(`${API_URL}/protese/os/ocorrencias/${ocId}/resposta`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resposta }) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao responder'); return await res.json(); }, statusProteseOcorrencia: async (ocId: string, status: string) => { const res = await apiFetch(`${API_URL}/protese/os/ocorrencias/${ocId}/status`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao mudar status'); return await res.json(); }, deleteProteseFoto: async (fotoId: string) => { const res = await apiFetch(`${API_URL}/protese/os/fotos/${fotoId}`, { method: 'DELETE' }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao remover foto'); return await res.json(); }, // Componentes enviados ao laboratório (direcao: 'enviado' clínica→lab | 'devolvido' lab→clínica) addProteseComponente: async (osId: string, body: { nome: string; quantidade?: number; observacao?: string; direcao?: 'enviado' | 'devolvido' }) => { const res = await apiFetch(`${API_URL}/protese/os/${osId}/componentes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao adicionar componente'); return await res.json(); }, deleteProteseComponente: async (compId: string) => { const res = await apiFetch(`${API_URL}/protese/os/componentes/${compId}`, { method: 'DELETE' }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao remover componente'); return await res.json(); }, // Pagamentos/recebimentos da OS (lado: 'lab' | 'clinica') addProtesePagamento: async (osId: string, body: { lado: 'lab' | 'clinica'; valor: number; forma?: string; observacao?: string; data?: string }) => { const res = await apiFetch(`${API_URL}/protese/os/${osId}/pagamentos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao registrar pagamento'); return await res.json(); }, deleteProtesePagamento: async (pagId: string) => { const res = await apiFetch(`${API_URL}/protese/os/pagamentos/${pagId}`, { method: 'DELETE' }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao remover pagamento'); return await res.json(); }, // Custódia: registra que o trabalho mudou de mãos (para_local: 'clinica' | 'laboratorio') moverProteseCustodia: async (osId: string, para_local: 'clinica' | 'laboratorio', observacao?: string) => { const res = await apiFetch(`${API_URL}/protese/os/${osId}/custodia`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ para_local, observacao }) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao registrar movimentação'); return await res.json(); }, // Editar campos do trabalho (cor, material, dentes, observações) editarProteseOS: async (osId: string, campos: { cor?: string; material?: string; dentes?: string; observacoes?: string }) => { const res = await apiFetch(`${API_URL}/protese/os/${osId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(campos) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao editar'); return await res.json(); }, // Ajuste do valor cobrado do paciente (delta + ou −) ajustarValorProtese: async (osId: string, delta: number, observacao?: string) => { const res = await apiFetch(`${API_URL}/protese/os/${osId}/ajuste-valor`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ delta, observacao }) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao ajustar valor'); return await res.json(); }, // Trocar o trabalho por outro (produto do catálogo ou tipo livre) + motivo trocarTrabalhoProtese: async (osId: string, body: { produto_id?: string; tipo?: string; motivo: string }) => { const res = await apiFetch(`${API_URL}/protese/os/${osId}/trocar`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao trocar trabalho'); return await res.json(); }, deleteProteseOS: async (id: string) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; await apiFetch(`${API_URL}/protese/os/${id}?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'DELETE' }); }, // Catálogo do protético (preço fixo + garantia) getProteseProdutos: async () => { const res = await apiFetch(`${API_URL}/protese/produtos`); return await res.json(); }, getLaboratorioProdutos: async (proteticoId: string) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/protese/laboratorios/${proteticoId}/produtos?clinicaId=${encodeURIComponent(clinicaId)}`); return await res.json(); }, createProteseProduto: async (payload: { nome: string; preco?: number; garantia_meses?: number }) => { const res = await apiFetch(`${API_URL}/protese/produtos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao criar produto'); return await res.json(); }, updateProteseProduto: async (id: string, payload: any) => { const res = await apiFetch(`${API_URL}/protese/produtos/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao salvar produto'); return await res.json(); }, deleteProteseProduto: async (id: string) => { await apiFetch(`${API_URL}/protese/produtos/${id}`, { method: 'DELETE' }); }, // ── GTO (builder → finaliza → lança no financeiro) ────────────────────────── createGtoBuilder: async (payload: { pacienteId: string; dentistaId: string; credenciadaId: string }) => { const res = await apiFetch(`${API_URL}/gto-builder`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao criar GTO'); return await res.json(); }, addGtoItem: async (builderId: string, item: any) => { const res = await apiFetch(`${API_URL}/gto-builder/${builderId}/item`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(item) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao adicionar item'); return await res.json(); }, finalizarGto: async (builderId: string) => { const res = await apiFetch(`${API_URL}/gto-builder/${builderId}/finalizar`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao finalizar GTO'); return await res.json(); }, getFinanceiroRelatorio: async (opts: { inicio?: string; fim?: string; pacienteId?: string; profissionalId?: string } = {}) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const qs = new URLSearchParams({ clinicaId }); if (opts.inicio) qs.set('inicio', opts.inicio); if (opts.fim) qs.set('fim', opts.fim); if (opts.pacienteId) qs.set('pacienteId', opts.pacienteId); if (opts.profissionalId) qs.set('profissionalId', opts.profissionalId); const res = await apiFetch(`${API_URL}/financeiro/relatorio?${qs.toString()}`); return await res.json(); }, updateEspecialidade: async (esp: Especialidade) => { const data = enforceUppercase(esp); const res = await apiFetch(`${API_URL}/especialidades/${esp.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return await res.json(); }, saveEspecialidade: async (esp: Partial) => { const data = enforceUppercase(esp); const body = { ...data, id: Math.random().toString() }; const res = await apiFetch(`${API_URL}/especialidades`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return await res.json(); }, reorderEspecialidades: async (orders: { id: string; ordem: number }[]) => { const res = await apiFetch(`${API_URL}/especialidades/reorder`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orders }) }); return await res.json(); }, deleteEspecialidade: async (id: string) => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; await apiFetch(`${API_URL}/especialidades/${id}?clinicaId=${encodeURIComponent(cid)}`, { method: 'DELETE' }); }, updateProcedimento: async (proc: Procedimento) => { const data = enforceUppercase(proc); const res = await apiFetch(`${API_URL}/procedimentos/${proc.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return await res.json(); }, saveProcedimento: async (proc: Partial) => { const data = enforceUppercase(proc); const body = { ...data, id: Math.random().toString() }; const res = await apiFetch(`${API_URL}/procedimentos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return await res.json(); }, reorderProcedimentos: async (orders: { id: string; ordem: number }[]) => { const res = await apiFetch(`${API_URL}/procedimentos/reorder`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orders }) }); return await res.json(); }, deleteProcedimento: async (id: string) => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; await apiFetch(`${API_URL}/procedimentos/${id}?clinicaId=${encodeURIComponent(cid)}`, { method: 'DELETE' }); }, updatePlano: async (plano: Plano) => { const data = enforceUppercase(plano); const res = await apiFetch(`${API_URL}/planos/${plano.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return await res.json(); }, savePlano: async (plano: Partial) => { const data = enforceUppercase(plano); const body = { ...data, id: Math.random().toString() }; const res = await apiFetch(`${API_URL}/planos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return await res.json(); }, deletePlano: async (id: string) => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; await apiFetch(`${API_URL}/planos/${id}?clinicaId=${encodeURIComponent(cid)}`, { method: 'DELETE' }); }, getEspecialidades: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/especialidades${clinicaId ? `?clinicaId=${clinicaId}` : ''}`); return await res.json(); }, // --- ÁREAS DE ATUAÇÃO (odontologia/biomedicina/prótese) --- getAreas: async (): Promise<{ slug: string; nome: string; cor: string; icone: string }[]> => { try { const r = await apiFetch(`${API_URL}/areas`); return r.ok ? await r.json() : []; } catch { return []; } }, getMyAreas: async (): Promise => { try { const r = await apiFetch(`${API_URL}/me/areas`); const d = r.ok ? await r.json() : {}; return Array.isArray(d?.areas) ? d.areas : []; } catch { return []; } }, setMyAreas: async (areas: string[]): Promise => { try { const r = await apiFetch(`${API_URL}/me/areas`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ areas }) }); return r.ok; } catch { return false; } }, getClinicaAreas: async (clinicaId: string): Promise => { try { const r = await apiFetch(`${API_URL}/clinicas/${clinicaId}/areas`); const d = r.ok ? await r.json() : {}; return Array.isArray(d?.areas) ? d.areas : []; } catch { return []; } }, setClinicaAreas: async (clinicaId: string, areas: string[]): Promise => { try { const r = await apiFetch(`${API_URL}/clinicas/${clinicaId}/areas`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ areas }) }); return r.ok; } catch { return false; } }, // Semeia o catálogo de biomedicina estética no workspace ativo (idempotente). seedBiomedicina: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; if (!clinicaId) return false; try { const res = await apiFetch(`${API_URL}/me/seed-biomedicina`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId }), }); const d = await res.json(); return !!d?.seeded; } catch { return false; } }, getProcedimentos: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/procedimentos${clinicaId ? `?clinicaId=${clinicaId}` : ''}`); return await res.json(); }, getFinanceiro: async (): Promise => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/financeiro${cid ? `?clinicaId=${encodeURIComponent(cid)}` : ''}`); if (!res.ok) return []; return await res.json(); }, // Relatório por sala (faturamento + ocupação) no período — escopo: dono. getSalasRelatorio: async (inicio?: string, fim?: string): Promise => { const qs = new URLSearchParams(); if (inicio) qs.set('inicio', inicio); if (fim) qs.set('fim', fim); const res = await apiFetch(`${API_URL}/salas/relatorio?${qs.toString()}`); return await res.json().catch(() => ({ salas: [] })); }, // Reservas do usuário (feitas + recebidas como locador) — usado no painel da sala. getMinhasReservas: async (): Promise<{ feitas: any[]; recebidas: any[] }> => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/sala-reservas/minhas${cid ? `?clinicaId=${encodeURIComponent(cid)}` : ''}`); return await res.json().catch(() => ({ feitas: [], recebidas: [] })); }, // Laboratório (Fase 3): clientes (clínicas) com métricas + extrato de recebimentos. getLabClientes: async (): Promise => { const res = await apiFetch(`${API_URL}/protese/lab/clientes`); return await res.json().catch(() => []); }, getLabPagamentos: async (): Promise => { const res = await apiFetch(`${API_URL}/protese/lab/pagamentos`); return await res.json().catch(() => []); }, // Fase 4: dados do laboratório (PF/PJ, CPF/CNPJ) + equipe de técnicos getLabDados: async (): Promise => { const res = await apiFetch(`${API_URL}/protese/lab`); return await res.json().catch(() => ({})); }, updateLabDados: async (body: { nome_fantasia: string; documento?: string; pessoa_tipo?: 'PF' | 'PJ' }) => { const res = await apiFetch(`${API_URL}/protese/lab`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao salvar'); return await res.json(); }, getLabEquipe: async (): Promise => { const res = await apiFetch(`${API_URL}/protese/lab/equipe`); return await res.json().catch(() => []); }, addLabTecnico: async (body: { email: string; nome?: string }) => { const res = await apiFetch(`${API_URL}/protese/lab/equipe`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao adicionar técnico'); return await res.json(); }, removeLabTecnico: async (userId: string) => { const res = await apiFetch(`${API_URL}/protese/lab/equipe/${userId}`, { method: 'DELETE' }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao remover'); return await res.json(); }, // Fase 5: indicadores do laboratório (Nota ScoreOdonto + KPIs derivados) getLabIndicadores: async (): Promise => { const res = await apiFetch(`${API_URL}/protese/lab/indicadores`); return await res.json().catch(() => ({})); }, // Fatia 4: a clínica avalia o laboratório ao final da OS avaliarProteseOS: async (osId: string, body: { estrelas: number; comentario?: string }) => { const res = await apiFetch(`${API_URL}/protese/os/${osId}/avaliar`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao avaliar'); return await res.json(); }, // Modo 1: link seguro de acompanhamento da OS gerarProteseLink: async (osId: string) => { const res = await apiFetch(`${API_URL}/protese/os/${osId}/link`, { method: 'POST' }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao gerar link'); return await res.json(); }, getProteseLinkStatus: async (osId: string) => { const res = await apiFetch(`${API_URL}/protese/os/${osId}/link/status`); return await res.json().catch(() => ({ ativo: false })); }, // Cotação (RFQ) — Fatia 3 criarRfq: async (body: any) => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/protese/rfq?clinicaId=${encodeURIComponent(cid)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao pedir cotação'); return await res.json(); }, getRfqs: async (): Promise => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/protese/rfq?clinicaId=${encodeURIComponent(cid)}`); return await res.json().catch(() => []); }, getRfqRecebidas: async (): Promise => { const res = await apiFetch(`${API_URL}/protese/rfq/recebidas`); return await res.json().catch(() => []); }, responderCotacao: async (cotacaoId: string, body: { valor: number; prazo_dias?: number; observacao?: string }) => { const res = await apiFetch(`${API_URL}/protese/rfq/cotacao/${cotacaoId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao cotar'); return await res.json(); }, escolherCotacao: async (rfqId: string, cotacaoId: string) => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/protese/rfq/${rfqId}/escolher?clinicaId=${encodeURIComponent(cid)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cotacao_id: cotacaoId }) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao escolher'); return await res.json(); }, revogarProteseLink: async (osId: string) => { const res = await apiFetch(`${API_URL}/protese/os/${osId}/link/revogar`, { method: 'POST' }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao revogar'); return await res.json(); }, // Leitura PÚBLICA via token (sem login — não usa apiFetch/Authorization) getProtesePublic: async (token: string) => { const res = await fetch(`${API_URL}/p/${encodeURIComponent(token)}`); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Link inválido ou expirado.'); return await res.json(); }, // Ações PÚBLICAS pelo link (Modo 1, fatia 1b) — sem login custodiaPublic: async (token: string, para_local: 'clinica' | 'laboratorio') => { const res = await fetch(`${API_URL}/p/${encodeURIComponent(token)}/custodia`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ para_local }) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao registrar.'); return await res.json(); }, statusPublic: async (token: string, status: string) => { const res = await fetch(`${API_URL}/p/${encodeURIComponent(token)}/status`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao avançar etapa.'); return await res.json(); }, uploadFotoPublic: async (token: string, file: File) => { const fd = new FormData(); fd.append('foto', file); const res = await fetch(`${API_URL}/p/${encodeURIComponent(token)}/foto`, { method: 'POST', body: fd }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao anexar foto.'); return await res.json(); }, // Agenda de uma sala (reservas + bloqueios de manutenção) — usado pelo operador da unidade. getSalaReservas: async (salaId: string): Promise => { const res = await apiFetch(`${API_URL}/salas/${salaId}/reservas`); return await res.json().catch(() => []); }, // Bloqueio de sala para manutenção (sem cobrança; tira a sala da agenda no intervalo). criarManutencaoSala: async (salaId: string, body: { inicio: string; fim: string; motivo?: string }) => { const res = await apiFetch(`${API_URL}/salas/${salaId}/manutencao`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)}); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data?.error || 'Falha ao bloquear a sala.'); return data; }, saveFinanceiro: async (item: Partial) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const data = enforceUppercase(item); const res = await apiFetch(`${API_URL}/financeiro?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)}); return await res.json(); }, updateFinanceiro: async (item: FinanceiroItem) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const data = enforceUppercase(item); const res = await apiFetch(`${API_URL}/financeiro/${item.id}?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)}); return await res.json(); }, deleteFinanceiro: async (id: string) => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; await apiFetch(`${API_URL}/financeiro/${id}?clinicaId=${encodeURIComponent(cid)}`, { method: 'DELETE' }); }, getAgendamentos: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; if (!clinicaId) return []; const res = await apiFetch(`${API_URL}/agendamentos?clinicaId=${clinicaId}`, {}); const data = await res.json().catch(() => []); return Array.isArray(data) ? data : []; }, salvarAgendamento: async (ag: Partial) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const body = { ...ag, clinica_id: (ag as any).clinica_id || clinicaId }; const res = await apiFetch(`${API_URL}/agendamentos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) { const err = await res.json().catch(() => ({} as any)); // 409 com info de conflito → propaga p/ a UI oferecer "registrar interesse" if (res.status === 409 && err.conflito) { const e: any = new Error(err.message || 'Horário em conflito.'); e.conflito = true; e.ocupado = err.ocupado; throw e; } throw new Error(err.message || 'Erro ao salvar agendamento.'); } return await res.json(); }, // Fase 4 — registra interesse num horário ocupado (não trava o slot) registrarInteresse: async (payload: { dentistaId?: string; profissionalUsuarioId?: string; clinicaInteressadaId: string; solicitanteUsuarioId?: string; start: string; end?: string; tempoEsperaMin?: number; observacoes?: string; }) => { const res = await apiFetch(`${API_URL}/interesses`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) { const e = await res.json().catch(() => ({} as any)); throw new Error(e.message || 'Erro ao registrar interesse.'); } return await res.json(); }, getInteresses: async (params: { profissionalId?: string; clinicaId?: string }): Promise => { const qs = new URLSearchParams(); if (params.profissionalId) qs.set('profissionalId', params.profissionalId); if (params.clinicaId) qs.set('clinicaId', params.clinicaId); const res = await apiFetch(`${API_URL}/interesses?${qs.toString()}`, {}); return res.ok ? await res.json() : []; }, atualizarAgendamento: async (id: string, data: Partial) => { const res = await apiFetch(`${API_URL}/agendamentos/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) { const err = await res.json().catch(() => ({} as any)); if (res.status === 409 && err.conflito) { const e: any = new Error(err.message || 'Horário em conflito.'); e.conflito = true; e.ocupado = err.ocupado; throw e; } throw new Error(err.message || 'Erro ao atualizar agendamento.'); } return await res.json(); }, excluirAgendamento: async (id: string, motivo?: string) => { await apiFetch(`${API_URL}/agendamentos/${id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ motivo }), }); }, // ---- Agenda Fase 1: faltas, remarcar, restaurar, histórico, pendências, família ---- marcarFalta: async (id: string) => { const res = await apiFetch(`${API_URL}/agendamentos/${id}/falta`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); return await res.json().catch(() => ({ success: res.ok })); }, remarcarAgendamento: async (id: string, motivo?: string) => { const res = await apiFetch(`${API_URL}/agendamentos/${id}/remarcar`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ motivo }) }); return await res.json().catch(() => ({ success: res.ok })); }, restaurarAgendamento: async (id: string) => { const res = await apiFetch(`${API_URL}/agendamentos/${id}/restaurar`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); return await res.json().catch(() => ({ success: res.ok })); }, // Confirma um agendamento IMPORTADO: vincula paciente (exige CPF+telefone) → vira 'agendado'. confirmarAgendamento: async (id: string, payload: { pacienteId: string; cpf?: string; telefone?: string; procedimento?: string }) => { const res = await apiFetch(`${API_URL}/agendamentos/${id}/confirmar`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); return await res.json().catch(() => ({ success: res.ok })); }, reencaminharAgendamento: async (id: string, motivo?: string) => { const res = await apiFetch(`${API_URL}/agendamentos/${id}/reencaminhar`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ motivo }) }); return await res.json().catch(() => ({ success: res.ok })); }, solicitarAvaliacao: async (id: string, motivo?: string) => { const res = await apiFetch(`${API_URL}/agendamentos/${id}/solicitar-avaliacao`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ motivo }) }); return await res.json().catch(() => ({ success: res.ok })); }, getHistoricoAgendamento: async (id: string): Promise => { const res = await apiFetch(`${API_URL}/agendamentos/${id}/historico`, {}); const j = await res.json().catch(() => ({} as any)); return j.historico || []; }, // --- FASE B · Avaliações clínicas (odontograma + fotos) --- getAvaliacaoDoAgendamento: async (agendamentoId: string) => { const res = await apiFetch(`${API_URL}/agendamentos/${agendamentoId}/avaliacao`, {}); const j = await res.json().catch(() => ({} as any)); return j.avaliacao || null; }, getAvaliacao: async (id: string) => { const res = await apiFetch(`${API_URL}/avaliacoes/${id}`, {}); const j = await res.json().catch(() => ({} as any)); return j.avaliacao || null; }, getAvaliacoes: async (status?: string, clinicaId?: string): Promise => { const cid = clinicaId || HybridBackend.getActiveWorkspace()?.id || ''; if (!cid) return []; const qs = new URLSearchParams({ clinicaId: cid }); if (status) qs.set('status', status); const res = await apiFetch(`${API_URL}/avaliacoes?${qs.toString()}`, {}); const j = await res.json().catch(() => ({} as any)); return j.avaliacoes || []; }, salvarAvaliacao: async (id: string, payload: { odontograma?: any; observacoes?: string; status?: string }) => { const res = await apiFetch(`${API_URL}/avaliacoes/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); return await res.json().catch(() => ({ success: res.ok })); }, uploadAvaliacaoFoto: async (id: string, file: File, dente?: string) => { const fd = new FormData(); fd.append('foto', file); if (dente) fd.append('dente', dente); const res = await apiFetch(`${API_URL}/avaliacoes/${id}/fotos`, { method: 'POST', body: fd }); return await res.json().catch(() => ({ success: res.ok })); }, deleteAvaliacaoFoto: async (fotoId: string) => { const res = await apiFetch(`${API_URL}/avaliacoes/fotos/${fotoId}`, { method: 'DELETE' }); return await res.json().catch(() => ({ success: res.ok })); }, getPendenciasReagendar: async (clinicaId?: string): Promise => { const cid = clinicaId || HybridBackend.getActiveWorkspace()?.id || ''; if (!cid) return []; const res = await apiFetch(`${API_URL}/agenda/pendencias?clinicaId=${cid}`, {}); const j = await res.json().catch(() => ({} as any)); return j.pendencias || []; }, resolverPendencia: async (id: string, resolucao?: string) => { const res = await apiFetch(`${API_URL}/agenda/pendencias/${id}/resolver`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resolucao }) }); return await res.json().catch(() => ({ success: res.ok })); }, getFaltasPaciente: async (pacienteId: string): Promise => { const res = await apiFetch(`${API_URL}/pacientes/${pacienteId}/faltas`, {}); const j = await res.json().catch(() => ({} as any)); return j.faltas || 0; }, getScorePaciente: async (pacienteId: string): Promise<{ faltas: number; remarcacoes: number; nivel: string }> => { const res = await apiFetch(`${API_URL}/pacientes/${pacienteId}/score`, {}); const j = await res.json().catch(() => ({} as any)); return { faltas: j.faltas || 0, remarcacoes: j.remarcacoes || 0, nivel: j.nivel || 'BOM' }; }, getFamiliaPaciente: async (pacienteId: string): Promise<{ grupo: any; membros: any[] }> => { const res = await apiFetch(`${API_URL}/pacientes/${pacienteId}/familia`, {}); const j = await res.json().catch(() => ({} as any)); return { grupo: j.grupo || null, membros: j.membros || [] }; }, // Importa agendamentos do Google p/ dentro do sistema (admin/dono). Idempotente. importarGoogleAgenda: async (clinicaId?: string, dias?: number): Promise<{ success: boolean; criados?: number; pulados?: number; total?: number; message?: string }> => { const cid = clinicaId || HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/agenda/importar-google`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId: cid, dias }), }); return await res.json().catch(() => ({ success: false })); }, // Feed de atividade da equipe (auditoria) — default: hoje. getAtividadeAgenda: async (clinicaId?: string): Promise => { const cid = clinicaId || HybridBackend.getActiveWorkspace()?.id || ''; if (!cid) return []; const res = await apiFetch(`${API_URL}/agenda/atividade?clinicaId=${cid}`, {}); const j = await res.json().catch(() => ({} as any)); return j.atividade || []; }, // Presença na agenda (Fase 2): heartbeat + leitura de quem está online / última visita. pingPresencaAgenda: async (clinicaId?: string) => { const cid = clinicaId || HybridBackend.getActiveWorkspace()?.id || ''; if (!cid) return; try { await apiFetch(`${API_URL}/agenda/presenca`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId: cid }) }); } catch { /* ignore */ } }, getPresencaAgenda: async (clinicaId?: string): Promise<{ online: any[]; ultimas: any[] }> => { const cid = clinicaId || HybridBackend.getActiveWorkspace()?.id || ''; if (!cid) return { online: [], ultimas: [] }; const res = await apiFetch(`${API_URL}/agenda/presenca?clinicaId=${cid}`, {}); const j = await res.json().catch(() => ({} as any)); return { online: j.online || [], ultimas: j.ultimas || [] }; }, // ---- Mapeamento DB(snake) <-> TratamentoOrto(camel) ---- _ortoTratFromRow: (t: any): TratamentoOrto => ({ id: t.id, pacienteId: t.paciente_id || '', pacienteNome: t.paciente_nome, dataNascimento: t.data_nascimento || undefined, responsavel: t.responsavel || undefined, tipoPaciente: t.tipo_paciente || undefined, triagem: t.triagem || undefined, diagnostico: t.diagnostico || undefined, dataInicio: t.data_inicio || '', tipoAparelho: t.tipo_aparelho || '', extracoes: t.extracoes || undefined, sequenciaFios: t.sequencia_fios || undefined, elasticos: t.elasticos || undefined, iprPlanejado: t.ipr_planejado || undefined, tempoEstimado: t.tempo_estimado || undefined, valor: t.valor != null ? Number(t.valor) : undefined, formaPagamento: t.forma_pagamento || undefined, faseAtual: t.fase_atual || undefined, fioAtual: t.fio_atual || undefined, classificacao: t.classificacao || undefined, cooperacao: t.cooperacao || undefined, percentualConcluido: t.percentual_concluido != null ? Number(t.percentual_concluido) : undefined, ultimaConsulta: t.ultima_consulta || undefined, proximaConsulta: t.proxima_consulta || undefined, mesesRestantes: t.meses_restantes != null ? Number(t.meses_restantes) : undefined, status: t.status || 'Em Tratamento', contencaoTipo: t.contencao_tipo || undefined, contencaoSuperior: t.contencao_superior ?? undefined, contencaoInferior: t.contencao_inferior ?? undefined, tempoUso: t.tempo_uso || undefined, flags: t.flags || undefined, evolucoes: (t.evolucoes || []).map((e: any) => ({ id: e.id, data: e.data, tipo: e.tipo, procedimento: e.procedimento || '', proximoRetorno: e.proximo_retorno || '', fio: e.fio || undefined, elastico: e.elastico || undefined, procedimentos: e.procedimentos || undefined, higiene: e.higiene != null ? Number(e.higiene) : undefined, cooperacao: e.cooperacao || undefined, observacoes: e.observacoes || undefined, })), }), _ortoTratToRow: (t: Partial): Record => { const m: Record = { paciente_id: t.pacienteId, paciente_nome: t.pacienteNome, data_nascimento: t.dataNascimento, responsavel: t.responsavel, tipo_paciente: t.tipoPaciente, triagem: t.triagem, diagnostico: t.diagnostico, data_inicio: t.dataInicio, tipo_aparelho: t.tipoAparelho, extracoes: t.extracoes, sequencia_fios: t.sequenciaFios, elasticos: t.elasticos, ipr_planejado: t.iprPlanejado, tempo_estimado: t.tempoEstimado, valor: t.valor, forma_pagamento: t.formaPagamento, fase_atual: t.faseAtual, fio_atual: t.fioAtual, classificacao: t.classificacao, cooperacao: t.cooperacao, percentual_concluido: t.percentualConcluido, ultima_consulta: t.ultimaConsulta, proxima_consulta: t.proximaConsulta, meses_restantes: t.mesesRestantes, status: t.status, flags: t.flags, contencao_tipo: t.contencaoTipo, contencao_superior: t.contencaoSuperior, contencao_inferior: t.contencaoInferior, tempo_uso: t.tempoUso, }; const out: Record = {}; for (const k in m) if (m[k] !== undefined) out[k] = m[k]; return out; }, getOrtoTreatments: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id; const res = await HybridBackend._ortoJson(`/orto/tratamentos${clinicaId ? `?clinicaId=${clinicaId}` : ''}`, {}, { success: true, tratamentos: [] }); if (!res.success) return []; return (res.tratamentos || []).map((t: any) => HybridBackend._ortoTratFromRow(t)); }, createTratamentoOrto: async (t: Partial): Promise<{ success: boolean; id?: string; message?: string }> => { const row = HybridBackend._ortoTratToRow(t); row.clinica_id = HybridBackend.getActiveWorkspace()?.id; return HybridBackend._ortoJson(`/orto/tratamentos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(row), }, { success: false, message: 'BACKEND INDISPONÍVEL.' }); }, saveEvolucaoOrto: async (tratamentoId: string, evolucao: Partial): Promise => { const res = await HybridBackend._ortoJson(`/orto/tratamentos/${tratamentoId}/evolucoes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data: evolucao.data, tipo: evolucao.tipo, procedimento: evolucao.procedimento, proximo_retorno: evolucao.proximoRetorno, fio: evolucao.fio, elastico: evolucao.elastico, procedimentos: evolucao.procedimentos, higiene: evolucao.higiene, cooperacao: evolucao.cooperacao, observacoes: evolucao.observacoes, }), }, { success: false }); const e = res.evolucao || {}; return { id: e.id || ('ev_' + Math.random().toString(36).slice(2, 8)), data: e.data || evolucao.data || new Date().toISOString().split('T')[0], tipo: (e.tipo || evolucao.tipo || 'Manutenção') as any, procedimento: e.procedimento ?? evolucao.procedimento ?? '', proximoRetorno: e.proximo_retorno ?? evolucao.proximoRetorno ?? '', fio: e.fio ?? evolucao.fio, elastico: e.elastico ?? evolucao.elastico, procedimentos: e.procedimentos ?? evolucao.procedimentos, higiene: e.higiene ?? evolucao.higiene, cooperacao: (e.cooperacao ?? evolucao.cooperacao) as any, observacoes: e.observacoes ?? evolucao.observacoes, }; }, updateTratamentoOrto: async (tratamento: Partial): Promise => { if (!tratamento.id) return; const row = HybridBackend._ortoTratToRow(tratamento); await HybridBackend._ortoJson(`/orto/tratamentos/${tratamento.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(row), }, { success: false }); }, finalizarTratamentoOrto: async (tratamentoId: string, contencao: { tipo: string; superior: boolean; inferior: boolean; tempoUso: string }): Promise => { await HybridBackend._ortoJson(`/orto/tratamentos/${tratamentoId}/finalizar`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(contencao), }, { success: false }); }, // =================================================================== // ORTODONTIA · DOCUMENTAÇÃO FOTOGRÁFICA · TUTORIA // Chamam os endpoints reais (/api/orto/*, /api/tutoria/*, /api/tutores*, // /api/admin/*). Enquanto o backend ainda não expõe essas rotas, o // helper `_ortoJson` degrada para um fallback seguro (listas vazias / // success:false) em vez de deixar a UI quebrar com "is not a function". // =================================================================== _ortoJson: async (path: string, options: RequestInit = {}, fallback: any = { success: false }) => { try { const res = await apiFetch(`${API_URL}${path}`, options); if (!res.ok) return fallback; return await res.json(); } catch { return fallback; } }, // --- Documentação fotográfica --- getOrtoSessoes: async (tratamentoId: string) => HybridBackend._ortoJson(`/orto/sessoes?tratamentoId=${encodeURIComponent(tratamentoId)}`, {}, { success: true, sessoes: [] }), getOrtoSessao: async (tratamentoId: string, tipo: string, clinicaId?: string, rotulo?: string, dataRef?: string) => HybridBackend._ortoJson(`/orto/sessoes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tratamento_id: tratamentoId, tipo, clinica_id: clinicaId, rotulo, data_ref: dataRef }), }, { success: false }), uploadOrtoFoto: async (sessaoId: string, slot: string, file: File) => { const fd = new FormData(); fd.append('sessao_id', sessaoId); fd.append('slot', slot); fd.append('foto', file); return HybridBackend._ortoJson(`/orto/fotos`, { method: 'POST', body: fd }, { success: false }); }, deleteOrtoFoto: async (fotoId: string) => HybridBackend._ortoJson(`/orto/fotos/${fotoId}`, { method: 'DELETE' }, { success: false }), getOrtoAnotacoes: async (fotoId: string) => HybridBackend._ortoJson(`/orto/fotos/${fotoId}/anotacoes`, {}, { success: true, anotacoes: [] }), saveOrtoAnotacoes: async (fotoId: string, anotacoes: any[]) => HybridBackend._ortoJson(`/orto/fotos/${fotoId}/anotacoes`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ anotacoes }), }, { success: false }), // --- Tutoria (lado clínica + lado tutor) --- getTutores: async () => HybridBackend._ortoJson(`/tutores`, {}, { success: true, tutores: [] }), createSolicitacaoTutoria: async (payload: any) => HybridBackend._ortoJson(`/tutoria/solicitacoes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }, { success: false, message: 'BACKEND DE TUTORIA INDISPONÍVEL.' }), getSolicitacoesTutoria: async (clinicaId?: string, tratamentoId?: string) => { const params = new URLSearchParams(); if (clinicaId) params.set('clinicaId', clinicaId); if (tratamentoId) params.set('tratamentoId', tratamentoId); const qs = params.toString(); return HybridBackend._ortoJson(`/tutoria/solicitacoes${qs ? `?${qs}` : ''}`, {}, { success: true, solicitacoes: [] }); }, getSolicitacaoTutoria: async (id: string) => HybridBackend._ortoJson(`/tutoria/solicitacoes/${id}`, {}, { success: false }), sendMensagemTutoria: async (solId: string, conteudo: string, autorTipo: 'CLINICA' | 'TUTOR', autorNome: string) => HybridBackend._ortoJson(`/tutoria/solicitacoes/${solId}/mensagens`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conteudo, autor_tipo: autorTipo, autor_nome: autorNome }), }, { success: false }), updateStatusTutoria: async (solId: string, status: string) => HybridBackend._ortoJson(`/tutoria/solicitacoes/${solId}/status`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }), }, { success: false }), // --- Candidatura / perfil de tutor --- getMinhaCandidatura: async (usuarioId: string) => HybridBackend._ortoJson(`/tutoria/candidaturas/minha?usuarioId=${encodeURIComponent(usuarioId || '')}`, {}, { success: true, candidatura: null }), submitCandidatura: async (payload: any) => HybridBackend._ortoJson(`/tutoria/candidaturas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }, { success: false, message: 'BACKEND DE TUTORIA INDISPONÍVEL.' }), getTutorPorUsuario: async (usuarioId: string) => HybridBackend._ortoJson(`/tutores/por-usuario/${encodeURIComponent(usuarioId || '')}`, {}, { success: true, tutor: null }), updateCredenciaisPagamentoTutor: async (tutorId: string, payload: any) => HybridBackend._ortoJson(`/tutores/${tutorId}/credenciais`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }, { success: false, message: 'BACKEND DE TUTORIA INDISPONÍVEL.' }), iniciarPagamentoCandidatura: async (candidaturaId: string) => HybridBackend._ortoJson(`/tutoria/candidaturas/${candidaturaId}/pagamento`, { method: 'POST' }, { success: false, message: 'PAGAMENTO INDISPONÍVEL.' }), // --- Admin de tutores --- getAdminTutorConfig: async () => HybridBackend._ortoJson(`/admin/tutor-config`, {}, { success: true, config: {} }), updateAdminTutorConfig: async (payload: any) => HybridBackend._ortoJson(`/admin/tutor-config`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }, { success: false }), getAdminCandidaturas: async (status?: string) => HybridBackend._ortoJson(`/admin/candidaturas${status ? `?status=${encodeURIComponent(status)}` : ''}`, {}, { success: true, candidaturas: [] }), updateStatusCandidatura: async (id: string, status: string, observacao?: string) => HybridBackend._ortoJson(`/admin/candidaturas/${id}/status`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status, observacao }), }, { success: false }), confirmarPagamentoCandidatura: async (id: string) => HybridBackend._ortoJson(`/admin/candidaturas/${id}/confirmar-pagamento`, { method: 'POST' }, { success: false }), getRepassesPendentes: async () => HybridBackend._ortoJson(`/admin/repasses`, {}, { success: true, repasses: [] }), confirmarRepasse: async (id: string) => HybridBackend._ortoJson(`/admin/repasses/${id}/confirmar`, { method: 'POST' }, { success: false }), // --- GESTÃO DE EQUIPE (membros / setores / funções) --- getMembros: async (clinicaId: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/membros`, {}, { success: true, membros: [] }), addMembro: async (clinicaId: string, email: string, role: string, setorId?: string, funcaoId?: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/membros`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, role, setor_id: setorId, funcao_id: funcaoId }), }, { success: false, message: 'BACKEND INDISPONÍVEL.' }), updateMembro: async (clinicaId: string, userId: string, data: any) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/membros/${userId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }, { success: false }), removeMembro: async (clinicaId: string, userId: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/membros/${userId}`, { method: 'DELETE' }, { success: false }), resetMembroSenha: async (clinicaId: string, userId: string): Promise<{ success: boolean; email?: string; tempPassword?: string; message?: string }> => HybridBackend._ortoJson(`/clinicas/${clinicaId}/membros/${userId}/reset-senha`, { method: 'POST' }, { success: false, message: 'BACKEND INDISPONÍVEL.' }), getSetores: async (clinicaId: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/setores`, {}, { success: true, setores: [] }), saveSetor: async (clinicaId: string, data: any) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/setores`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }, { success: false }), updateSetor: async (clinicaId: string, id: string, data: any) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/setores/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }, { success: false }), deleteSetor: async (clinicaId: string, id: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/setores/${id}`, { method: 'DELETE' }, { success: false }), getFuncoes: async (clinicaId: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/funcoes`, {}, { success: true, funcoes: [] }), saveFuncao: async (clinicaId: string, data: any) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/funcoes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }, { success: false }), updateFuncao: async (clinicaId: string, id: string, data: any) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/funcoes/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }, { success: false }), deleteFuncao: async (clinicaId: string, id: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/funcoes/${id}`, { method: 'DELETE' }, { success: false }), // --- LEAD & NOTIFICATION METHODS --- saveLead: async (lead: Partial) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const newLead: Lead = { id: Math.random().toString(), nome: toUpper(lead.nome), sobrenome: toUpper(lead.sobrenome), cpf: lead.cpf || '', whatsapp: lead.whatsapp || '', tipoInteresse: lead.tipoInteresse || 'Particular', dataCadastro: new Date().toISOString(), status: 'Novo', clinica_id: clinicaId, } as any; await apiFetch(`${API_URL}/leads`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newLead) }); return newLead; }, getLeads: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/leads${clinicaId ? `?clinicaId=${clinicaId}` : ''}`); return await res.json(); }, getNotifications: async (): Promise => { try { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const qs = cid ? `?clinicaId=${encodeURIComponent(cid)}` : ''; const res = await apiFetch(`${API_URL}/notificacoes${qs}`); return await res.json(); } catch { return []; } }, markNotificationRead: async (id: string) => { try { await apiFetch(`${API_URL}/notificacoes/${id}/read`, { method: 'PUT' }); } catch (err) { console.error("Erro ao marcar como lida:", err); } }, markAllNotificationsRead: async () => { try { const cid = HybridBackend.getActiveWorkspace()?.id || ''; await apiFetch(`${API_URL}/notificacoes/read-all`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId: cid }), }); } catch (err) { console.error("Erro ao marcar todas como lidas:", err); } }, deleteNotification: async (id: string) => { try { await apiFetch(`${API_URL}/notificacoes/${id}`, { method: 'DELETE' }); } catch (err) { console.error("Erro ao excluir notificação:", err); } }, getSettings: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/settings${clinicaId ? `?clinicaId=${clinicaId}` : ''}`); return await res.json(); }, saveSettings: async (settings: Settings) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/settings${clinicaId ? `?clinicaId=${clinicaId}` : ''}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings) }); return await res.json(); }, getGoogleEvents: async (): Promise => { try { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; if (!clinicaId) return []; const res = await apiFetch(`${API_URL}/google/events?clinicaId=${clinicaId}`); const d = await res.json(); return Array.isArray(d) ? d : []; } catch { return []; } }, getOrcamentos: async (pacienteId?: string): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const params = new URLSearchParams({ clinicaId }); if (pacienteId) params.set('paciente_id', pacienteId); const res = await apiFetch(`${API_URL}/orcamentos?${params}`); return res.json(); }, saveOrcamento: async (c: Partial & { items: any[] }): Promise<{ id: string }> => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/orcamentos?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(c), }); return res.json(); }, updateOrcamento: async (c: Orcamento): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/orcamentos/${c.id}?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(c), }); return await res.json().catch(() => ({})); }, deleteOrcamento: async (id: string): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; await apiFetch(`${API_URL}/orcamentos/${id}?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'DELETE' }); }, // ── Motor contratual universal · biblioteca de modelos ────────────────────── getContratoVariaveis: async (): Promise<{ chave: string; desc: string }[]> => { const res = await apiFetch(`${API_URL}/contratos/variaveis`); return res.ok ? res.json() : []; }, getContratoModelos: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/contratos/modelos?clinicaId=${encodeURIComponent(clinicaId)}`); return res.ok ? res.json() : []; }, saveContratoModelo: async (m: any): Promise<{ id: string }> => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const url = m.id ? `${API_URL}/contratos/modelos/${m.id}` : `${API_URL}/contratos/modelos`; const res = await apiFetch(`${url}?clinicaId=${encodeURIComponent(clinicaId)}`, { method: m.id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(m), }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.error || 'Erro ao salvar modelo.'); return d; }, duplicarContratoModelo: async (id: string): Promise<{ id: string }> => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/contratos/modelos/${id}/duplicar?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.error || 'Erro ao duplicar.'); return d; }, deleteContratoModelo: async (id: string): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/contratos/modelos/${id}?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'DELETE' }); if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Erro ao excluir.'); } }, // ── Motor contratual · instâncias (contratos gerados) ─────────────────────── getContratoInstancias: async (): Promise => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/contratos/instancias?clinicaId=${encodeURIComponent(cid)}`); return res.ok ? res.json() : []; }, getContratoInstancia: async (id: string): Promise => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/contratos/instancias/${id}?clinicaId=${encodeURIComponent(cid)}`); return res.ok ? res.json() : null; }, saveContratoInstancia: async (c: any): Promise<{ id: string }> => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const url = c.id ? `${API_URL}/contratos/instancias/${c.id}` : `${API_URL}/contratos/instancias`; const res = await apiFetch(`${url}?clinicaId=${encodeURIComponent(cid)}`, { method: c.id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(c), }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.error || 'Erro ao salvar contrato.'); return d; }, gerarCobrancaContrato: async (id: string): Promise => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/contratos/instancias/${id}/gerar-cobranca?clinicaId=${encodeURIComponent(cid)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}', }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.error || 'Erro ao gerar cobrança.'); return d; }, deleteContratoInstancia: async (id: string): Promise => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/contratos/instancias/${id}?clinicaId=${encodeURIComponent(cid)}`, { method: 'DELETE' }); if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Erro ao excluir.'); } }, _contratoAcao: async (id: string, acao: string, body: any = {}): Promise => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/contratos/instancias/${id}/${acao}?clinicaId=${encodeURIComponent(cid)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.error || 'Erro na operação.'); return d; }, enviarContrato: (id: string) => HybridBackend._contratoAcao(id, 'enviar'), assinarContrato: (id: string, parteId: string) => HybridBackend._contratoAcao(id, 'assinar', { parteId }), cancelarContrato: (id: string) => HybridBackend._contratoAcao(id, 'cancelar'), // ── Prontuário · procedimentos realizados (reabertura/garantia + fotos antes/depois) ── getProcedimentosRealizados: async (pacienteId: string): Promise => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/procedimentos-realizados?pacienteId=${encodeURIComponent(pacienteId)}&clinicaId=${encodeURIComponent(cid)}`); return res.ok ? res.json() : []; }, getProcedimentoRealizado: async (id: string): Promise => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/procedimentos-realizados/${id}?clinicaId=${encodeURIComponent(cid)}`); return res.ok ? res.json() : null; }, saveProcedimentoRealizado: async (payload: any): Promise => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/procedimentos-realizados?clinicaId=${encodeURIComponent(cid)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.error || 'Erro ao registrar procedimento.'); return d; }, uploadProcedimentoFoto: async (id: string, momento: 'antes' | 'depois', file: File): Promise => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const fd = new FormData(); fd.append('momento', momento); fd.append('foto', file); const res = await apiFetch(`${API_URL}/procedimentos-realizados/${id}/foto?clinicaId=${encodeURIComponent(cid)}`, { method: 'POST', body: fd }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.error || 'Erro ao enviar foto.'); return d; }, deleteProcedimentoRealizado: async (id: string): Promise => { const cid = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/procedimentos-realizados/${id}?clinicaId=${encodeURIComponent(cid)}`, { method: 'DELETE' }); if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Erro ao excluir.'); } }, // Config GLOBAL do RX (definida pelo superadmin). Não expõe o token. getRxConfig: async (): Promise<{ rx_url: string; has_token: boolean; configured: boolean } | null> => { try { const res = await apiFetch(`${API_URL}/rx/config`); return await res.json(); } catch { return { rx_url: 'https://rx.scoreodonto.com', has_token: false, configured: false }; } }, // Salva a config GERAL do RX (só superadmin no backend). Token preservado se vazio. putRxConfig: async (cfg: { rx_url: string; api_token?: string }): Promise<{ success: boolean; configured?: boolean; message?: string }> => { try { const res = await apiFetch(`${API_URL}/rx/config`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg), }); return await res.json(); } catch (e: any) { return { success: false, message: e.message }; } }, getRxDevices: async (clinicaId: string): Promise => { try { const res = await apiFetch(`${API_URL}/rx/devices?clinicaId=${encodeURIComponent(clinicaId)}`); return await res.json(); } catch { return []; } }, createRxDevice: async (clinicaId: string, pcName: string, extra?: { email?: string; password?: string }): Promise<{ success: boolean; machine_token?: string; message?: string }> => { try { const res = await apiFetch(`${API_URL}/rx/devices`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId, pcName, ...extra }), }); return await res.json(); } catch (e: any) { return { success: false, message: e.message }; } }, // Leitura via proxy do backend (token só no servidor). Filtra por clinicaId = clinic_name. getRxPatients: async (clinicaId: string, search = ''): Promise => { try { const qs = new URLSearchParams({ clinicaId, search }); const res = await apiFetch(`${API_URL}/rx/patients?${qs.toString()}`); const rows = await res.json(); return Array.isArray(rows) ? rows : []; } catch { return []; } }, getRxPatientImages: async (clinicaId: string, paciente: string): Promise => { try { const qs = new URLSearchParams({ clinicaId, paciente }); const res = await apiFetch(`${API_URL}/rx/images?${qs.toString()}`); const rows = await res.json(); return Array.isArray(rows) ? rows : []; } catch { return []; } }, };