Files
scoreodonto.com/frontend/services/backend.ts
T
VPS 4 Builder dc5a2d89ae feat(protese): agenda & coleta do laboratório (integrada à custódia)
Novo módulo de logística do lab (doc/AGENDA-COLETA-LAB):
- protese_coleta: agendamento de coleta (lab busca) / entrega (lab leva), com data/hora,
  endereço, responsável, status. Ligado à OS.
- POST /protese/os/:id/coleta (agendar), GET /protese/lab/agenda (cronológica),
  POST /protese/coleta/:id/status. 'realizada' MOVE a custódia automaticamente
  (coleta→laboratorio, entrega→clinica) — fonte única do movimento físico. GET detalhe traz coletas[].
- UI: tela AGENDA (lab-agenda) agrupada por dia com "marcar realizada"; seção Coleta/entrega na
  aba Monitoramento da OS (clínica e lab).

Validado em DEV: agenda coleta → realizada → custódia=laboratorio; lab vê na agenda.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:11:24 +02:00

2114 lines
101 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = <T extends object>(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<any> => {
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<any> => {
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<any> => {
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<any> => {
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<void> => {
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<any> => {
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<string[]> => {
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<boolean> => {
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<void> => {
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<any[]> => {
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<boolean> => {
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<Paciente>) => {
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<import('../types.ts').GrupoFamiliar[]> => {
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<import('../types.ts').ModeloReceita[]> => {
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<import('../types.ts').ModeloReceita, 'id'>) => {
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<import('../types.ts').PedidoExame, 'id'>) => {
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<import('../types.ts').PedidoExame[]> => {
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<Dentista[]> => {
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<Plano[]> => {
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<Dentista>) => {
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<DentistaHorario[]> => {
const res = await apiFetch(`${API_URL}/dentistas/${dentistaId}/horarios?clinicaId=${clinicaId}`);
return await res.json();
},
saveHorario: async (horario: Partial<DentistaHorario>) => {
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<Especialidade>) => {
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<Procedimento>) => {
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<Plano>) => {
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<Especialidade[]> => {
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<string[]> => {
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<boolean> => {
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<string[]> => {
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<boolean> => {
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<boolean> => {
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<Procedimento[]> => {
const clinicaId = HybridBackend.getActiveWorkspace()?.id || '';
const res = await apiFetch(`${API_URL}/procedimentos${clinicaId ? `?clinicaId=${clinicaId}` : ''}`);
return await res.json();
},
getFinanceiro: async (): Promise<FinanceiroItem[]> => {
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<any> => {
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<any[]> => {
const res = await apiFetch(`${API_URL}/protese/lab/clientes`);
return await res.json().catch(() => []);
},
getLabPagamentos: async (): Promise<any[]> => {
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<any> => {
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<any[]> => {
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<any> => {
const res = await apiFetch(`${API_URL}/protese/lab/indicadores`);
return await res.json().catch(() => ({}));
},
// Agenda & Coleta do laboratório
agendarColeta: async (osId: string, body: { tipo: 'coleta' | 'entrega'; data_hora?: string; endereco?: string; responsavel?: string; observacao?: string }) => {
const res = await apiFetch(`${API_URL}/protese/os/${osId}/coleta`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao agendar');
return await res.json();
},
getLabAgenda: async (): Promise<any[]> => {
const res = await apiFetch(`${API_URL}/protese/lab/agenda`);
return await res.json().catch(() => []);
},
statusColeta: async (coletaId: string, status: 'realizada' | 'cancelada') => {
const res = await apiFetch(`${API_URL}/protese/coleta/${coletaId}/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 atualizar');
return await res.json();
},
// 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<any[]> => {
const cid = HybridBackend.getActiveWorkspace()?.id || '';
const res = await apiFetch(`${API_URL}/protese/rfq?clinicaId=${encodeURIComponent(cid)}`);
return await res.json().catch(() => []);
},
getRfqRecebidas: async (): Promise<any[]> => {
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<any[]> => {
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<FinanceiroItem>) => {
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<Agendamento[]> => {
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<Agendamento>) => {
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<any[]> => {
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<Agendamento>) => {
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<any[]> => {
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<any[]> => {
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<any[]> => {
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<number> => {
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<any[]> => {
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<TratamentoOrto>): Record<string, any> => {
const m: Record<string, any> = {
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<string, any> = {};
for (const k in m) if (m[k] !== undefined) out[k] = m[k];
return out;
},
getOrtoTreatments: async (): Promise<TratamentoOrto[]> => {
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<TratamentoOrto>): 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<EvolucaoOrto>): Promise<EvolucaoOrto> => {
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<TratamentoOrto>): Promise<void> => {
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<void> => {
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<Lead>) => {
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<Lead[]> => {
const clinicaId = HybridBackend.getActiveWorkspace()?.id || '';
const res = await apiFetch(`${API_URL}/leads${clinicaId ? `?clinicaId=${clinicaId}` : ''}`);
return await res.json();
},
getNotifications: async (): Promise<Notificacao[]> => {
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<Settings> => {
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<any[]> => {
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<Orcamento[]> => {
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<Orcamento> & { 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<any> => {
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<void> => {
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<any[]> => {
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<void> => {
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<any[]> => {
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<any | null> => {
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<any> => {
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<void> => {
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<any> => {
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<any[]> => {
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<any | null> => {
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<any> => {
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<any> => {
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<void> => {
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<any[]> => {
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<any[]> => {
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<any[]> => {
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 [];
}
},
};