87411e1566
Atende ao pedido de CRM financeiro do protético — antes as telas Clientes/Financeiro existiam mas não distinguiam origem nem traziam relacionamento/fechamento. - lab/clientes agora traz por cliente: ORIGEM (Clínica / Consultório / Dentista / Biomédico, derivada de clinicas.tipo + role do owner), CONTATO (telefone), TICKET MÉDIO, ÚLTIMO PEDIDO, além de OS/ativas/atrasadas/faturado/recebido/a-receber. - Novo lab/fechamento: faturado x recebido por mês. - LabClientesView reformulada: aba "Clientes (CRM)" com resumo por origem + tabela (origem/contato/ticket/últ. pedido/a-receber); aba "Financeiro" com KPIs + FECHAMENTO MENSAL + extrato. Validado em DEV: cliente Clínica (a-receber 0) e Dentista avulso (a-receber 100); fechamento 06/2026 faturado 550 / recebido 450. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2130 lines
102 KiB
TypeScript
2130 lines
102 KiB
TypeScript
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(() => []);
|
||
},
|
||
getLabFechamento: async (): Promise<any[]> => {
|
||
const res = await apiFetch(`${API_URL}/protese/lab/fechamento`);
|
||
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();
|
||
},
|
||
// Cadastro de protético INTERNO (sem conta) pela clínica
|
||
cadastrarProteticoInterno: async (body: { nome: string; telefone?: string; especialidade?: string }) => {
|
||
const cid = HybridBackend.getActiveWorkspace()?.id || '';
|
||
const res = await apiFetch(`${API_URL}/protese/proteticos-internos?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 cadastrar');
|
||
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();
|
||
},
|
||
getCarteiraPublic: async (token: string) => {
|
||
const res = await fetch(`${API_URL}/p/${encodeURIComponent(token)}/carteira`);
|
||
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao carregar.');
|
||
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 [];
|
||
}
|
||
},
|
||
}; |