3e64514b08
Financeiro V1 (Épicos 1–4): - FKs de origem no lançamento (paciente/profissional/procedimento/agendamento/contrato/realizado), soft delete e cron "Atrasado" - prontuário de procedimentos realizados (fotos antes/depois) + regra sem-comissão (garantia/reabertura do mesmo profissional) - normalizeFinanceiro: corrige CHECK capitalizado vs enforceUppercase (lançamentos não salvavam pela UI) - baixa de pagamento (pago_em), despesas (fornecedor/centro de custo/recorrente), competência por data de conclusão - relatório financeiro (período, paciente, profissional, forma, convênio, glosa) - orçamento Assinado -> contas a receber; contrato vigente -> cobrança recorrente; renovação automática de contrato Financeiro V2 (Comissão/Repasse): - regras de % (padrão/procedimento/override por profissional) + custo de laboratório - base selecionável (recebido/produção), fechamento persistido, pagar (gera despesa) + comprovante + estorno Glosas de convênio: por item de GTO, recurso/protocolo/prazo, recuperar/estornar, impacto no relatório GTO -> Financeiro: finalizar gera RECEITA (convênio a receber); LancarGTO passa a persistir; convênios reais (planos) Contratos: modelo "Atendimento a Convênios / Repasse (Dentista Credenciado)" + variáveis de convênio Salas (redesenho): perfil "Dono de Sala"; locação sempre ativa; menus MINHAS/ALUGAR SALAS; sala como workspace isolado (seletor agrupado Clínicas x Salas, tenantGuard por dono); financeiro de locação (reserva confirmada -> recebível); Painel da Sala (KPIs, agenda visual, recebíveis, relatório por sala) Docs: BACKLOG-FINANCEIRO-V1, AUDITORIA-FUNCIONAL-SCOREODONTO, CHECKLIST-DEPLOY-PRODUCAO Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1823 lines
83 KiB
TypeScript
1823 lines
83 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' });
|
|
},
|
|
|
|
// ── 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: [] }));
|
|
},
|
|
|
|
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 [];
|
|
}
|
|
},
|
|
}; |