import { Agendamento, Contrato, Dentista, DentistaHorario, Especialidade, EvolucaoOrto, FinanceiroItem, Lead, Notificacao, Paciente, Plano, Procedimento, Settings, TratamentoOrto } from '../types.ts'; // Helper to enforce uppercase on strings const toUpper = (str: string | undefined) => str ? str.toUpperCase() : ''; // Helper to process object properties to uppercase const enforceUppercase = (data: T): T => { const newData: any = { ...data }; for (const key in newData) { if (typeof newData[key] === 'string' && key !== 'id' && key !== 'email' && key !== 'start' && key !== 'end' && !key.includes('data') && !key.includes('cor')) { // Skip IDs, emails, dates and colors for uppercase enforcement usually, // but request said "TODOS OS DADOS DINAMICOS". We will be aggressive but keep IDs and technical fields intact. // We'll primarily target user-visible text fields. if (key !== 'corAgenda' && key !== 'corCartao') { newData[key] = newData[key].toUpperCase(); } } } return newData; }; const API_URL = import.meta.env.VITE_API_URL || '/api'; // Returns headers always including the JWT token stored after login let _sessionExpiredHandled = false; const apiFetch = async (url: string, options: RequestInit = {}) => { const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN'); const headers = { ...options.headers, ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }; const res = await fetch(url, { ...options, headers }); // Sessão expirada: o token foi enviado mas o servidor rejeitou (401, p.ex. JWT > 12h). // Limpa a sessão e recarrega para o login (uma única vez, p/ evitar loop de reloads). // Não dispara quando não há token (ex.: tentativa de login com senha errada). if (res.status === 401 && token && !_sessionExpiredHandled) { _sessionExpiredHandled = true; try { ['SCOREODONTO_AUTH_TOKEN', 'SCOREODONTO_USER_DATA', 'SCOREODONTO_WORKSPACES', 'SCOREODONTO_ACTIVE_WORKSPACE', 'SCOREODONTO_PREVIEW_ROLE'] .forEach(k => localStorage.removeItem(k)); } catch { /* ignore */ } window.location.href = '/?session=expired'; window.location.reload(); } return res; }; export const HybridBackend = { dbConfig: { database: 'scoreodonto' }, // --- AUTH METHODS --- login: async (email: string, pass: string): Promise<{ ok: boolean; noWorkspace?: boolean; userRole?: string }> => { const cleanEmail = email.toLowerCase().trim(); const cleanPass = pass.trim(); try { const res = await apiFetch(`${API_URL}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: cleanEmail, password: cleanPass }) }); if (res.ok) { const data = await res.json(); localStorage.removeItem('SCOREODONTO_PREVIEW_ROLE'); // garante que não herda modo visualização localStorage.setItem('SCOREODONTO_AUTH_TOKEN', data.token); localStorage.setItem('SCOREODONTO_USER_DATA', JSON.stringify(data.user)); localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify(data.workspaces || [])); if (data.mustChangePassword) localStorage.setItem('SCOREODONTO_MUST_CHANGE_PW', '1'); else localStorage.removeItem('SCOREODONTO_MUST_CHANGE_PW'); if (data.workspaces && data.workspaces.length > 0) { localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(data.workspaces[0])); } if (data.noWorkspace) { return { ok: true, noWorkspace: true, userRole: data.userRole }; } return { ok: true }; } return { ok: false }; } catch (err) { console.error('Login error:', err); return { ok: false }; } }, // --- CLINIC MANAGEMENT --- createClinica: async (nome_fantasia: string, documento?: string): Promise<{ success: boolean; clinica?: any; message?: string }> => { try { const res = await apiFetch(`${API_URL}/clinicas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nome_fantasia, documento }), }); return await res.json(); } catch (err) { return { success: false, message: 'Erro de conexão.' }; } }, logout: () => { localStorage.removeItem('SCOREODONTO_AUTH_TOKEN'); localStorage.removeItem('SCOREODONTO_USER_DATA'); localStorage.removeItem('SCOREODONTO_WORKSPACES'); localStorage.removeItem('SCOREODONTO_ACTIVE_WORKSPACE'); localStorage.removeItem('SCOREODONTO_PREVIEW_ROLE'); window.location.href = '/'; window.location.reload(); }, // Renomeia a unidade (clínica/consultório/laboratório) e sincroniza os caches locais // para a UI refletir o novo nome sem precisar relogar. renameWorkspace: async (clinicaId: string, nome: string): Promise<{ success: boolean; nome?: string; message?: string }> => { const res = await apiFetch(`${API_URL}/clinicas/${clinicaId}/nome`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nome_fantasia: nome }), }); const data = await res.json(); if (data?.success) { const finalNome = data.nome || nome.toUpperCase(); HybridBackend._syncWorkspaceNomeLocal(clinicaId, finalNome); } return data; }, // Atualiza o nome da unidade nos caches locais (workspaces + active) p/ a UI refletir na hora. _syncWorkspaceNomeLocal: (clinicaId: string, nome: string) => { try { const ws = JSON.parse(localStorage.getItem('SCOREODONTO_WORKSPACES') || '[]') .map((w: any) => (w.id === clinicaId ? { ...w, nome } : w)); localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify(ws)); const aw = JSON.parse(localStorage.getItem('SCOREODONTO_ACTIVE_WORKSPACE') || 'null'); if (aw && aw.id === clinicaId) localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify({ ...aw, nome })); } catch { /* ignore */ } }, // --- PERFIL DA UNIDADE (clínica/consultório/laboratório) --- getClinicaProfile: async (clinicaId: string): Promise<{ success: boolean; clinica?: any; message?: string }> => { const res = await apiFetch(`${API_URL}/clinicas/${clinicaId}`); return res.json(); }, updateClinicaProfile: async (clinicaId: string, data: any): Promise<{ success: boolean; nome?: string; message?: string }> => { const res = await apiFetch(`${API_URL}/clinicas/${clinicaId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); const d = await res.json(); if (d?.success && d.nome) HybridBackend._syncWorkspaceNomeLocal(clinicaId, d.nome); return d; }, // --- PERFIL DO SISTEMA (usuário logado) --- getMeuPerfil: async (): Promise<{ success: boolean; usuario?: any; message?: string }> => { const res = await apiFetch(`${API_URL}/usuarios/me`); return res.json(); }, updateMeuPerfil: async (data: any): Promise<{ success: boolean; message?: string }> => { const res = await apiFetch(`${API_URL}/usuarios/me`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); const d = await res.json(); if (d?.success) { try { const ud = JSON.parse(localStorage.getItem('SCOREODONTO_USER_DATA') || 'null'); if (ud) { if (data.nome !== undefined) ud.nome = String(data.nome).toUpperCase(); if (data.email !== undefined) ud.email = String(data.email).toLowerCase().trim(); localStorage.setItem('SCOREODONTO_USER_DATA', JSON.stringify(ud)); } } catch { /* ignore */ } } return d; }, changeMyPassword: async (atual: string | undefined, nova: string): Promise<{ success: boolean; message?: string }> => { const res = await apiFetch(`${API_URL}/usuarios/me/senha`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ atual, nova }), }); const d = await res.json(); if (d?.success) { try { localStorage.removeItem('SCOREODONTO_MUST_CHANGE_PW'); } catch { /* ignore */ } } return d; }, mustChangePassword: (): boolean => { try { return localStorage.getItem('SCOREODONTO_MUST_CHANGE_PW') === '1'; } catch { return false; } }, // --- BACKUP POR CLÍNICA (Google Sheets, planilha própria da unidade) --- getClinicaSyncStatus: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; if (!clinicaId) return { success: false, connected: false, counts: {} }; try { const res = await apiFetch(`${API_URL}/clinica-sync/status?clinicaId=${clinicaId}`); return await res.json(); } catch { return { success: false, connected: false, counts: {} }; } }, clinicaSyncPush: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/clinica-sync/push`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId }), }); return res.json(); }, clinicaSyncPull: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/clinica-sync/pull`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId }), }); return res.json(); }, // --- 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])); 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'); }, 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) : []; }, 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 res = await apiFetch(`${API_URL}/dashboard/stats`); return await res.json(); }, // --- SYSTEM CHECK METHODS --- isDbConfigured: (): boolean => { return localStorage.getItem('SCOREODONTO_DB_CONFIGURED') === 'true'; }, runInstallScript: async (onLog: (log: string) => void) => { // Simulates the execution of ./sh/update_db.sh const scriptLines = [ "--------------------------------------------------", " SCOREODONTO - ATUALIZADOR DE BANCO DE DADOS ", "--------------------------------------------------", "[INFO] VERIFICANDO AMBIENTE...", "[OK] AMBIENTE POSTGRESQL DETECTADO.", `[INFO] CONECTANDO DB: ${HybridBackend.dbConfig.database}...`, "[INFO] INICIANDO BACKUP DE SEGURANCA...", `[OK] BACKUP SALVO EM: ./bkp_database/BKP_${new Date().toISOString().replace(/[:.]/g, '-')}.SQL`, "[INFO] CONECTANDO API GOOGLE SHEETS...", "[INFO] LENDO PLANILHA: 'Dados Pacientes'...", " > 1.240 LINHAS ENCONTRADAS.", " > VALIDANDO DADOS (CPF, TELEFONE)...", "[INFO] SINCRONIZANDO COM POSTGRESQL (scoreodonto)...", " > FORCANDO PADRAO 'UPPERCASE' EM TODOS OS CAMPOS TEXTUAIS...", " > ATUALIZANDO TABELA: PACIENTES...", " > ATUALIZANDO TABELA: FINANCEIRO...", " > ATUALIZANDO TABELA: AGENDA...", "[INFO] LIMPANDO CACHE DE SESSAO...", "--------------------------------------------------", " ATUALIZACAO CONCLUIDA COM SUCESSO! ", "--------------------------------------------------" ]; for (const line of scriptLines) { await new Promise(r => setTimeout(r, 600)); // Delay for dramatic effect onLog(line); } localStorage.setItem('SCOREODONTO_DB_CONFIGURED', 'true'); return true; }, syncGoogleSheetsToPg: async (onProgress: (log: string) => void) => { onProgress("INICIANDO CONEXÃO COM GOOGLE SHEETS..."); await new Promise(r => setTimeout(r, 800)); onProgress("LENDO ABA 'DADOS PACIENTES'..."); await new Promise(r => setTimeout(r, 800)); onProgress(`CONECTANDO AO POSTGRESQL (${HybridBackend.dbConfig.database})...`); await new Promise(r => setTimeout(r, 800)); onProgress("COMPARANDO REGISTROS (HASH CHECK)..."); await new Promise(r => setTimeout(r, 1000)); onProgress("INSERINDO 2 NOVOS PACIENTES NO POSTGRESQL..."); onProgress("ATUALIZANDO 4 AGENDAMENTOS..."); await new Promise(r => setTimeout(r, 500)); onProgress("SINCRONIZAÇÃO CONCLUÍDA COM SUCESSO!"); return true; }, getPacientes: async (termo: string = '', sort: string = 'recentes'): Promise<{ data: Paciente[]; total: number }> => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const params = new URLSearchParams({ clinicaId }); if (termo.trim()) params.set('q', termo.trim()); if (sort) params.set('sort', sort); const res = await apiFetch(`${API_URL}/pacientes?${params}`); return res.json(); }, checkCpfExists: async (cpf: string): Promise => { const cleanCpf = cpf.replace(/\D/g, ''); if (!cleanCpf) return false; const res = await apiFetch(`${API_URL}/pacientes?q=${encodeURIComponent(cpf)}`); const { data } = await res.json(); const pacientes: Paciente[] = data || []; return pacientes.some(p => p.cpf.replace(/\D/g, '') === cleanCpf); }, savePaciente: async (paciente: Partial) => { const data = enforceUppercase(paciente); const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const body = { ...data, id: data.id || `p_${Date.now()}`, clinica_id: clinicaId }; const res = await apiFetch(`${API_URL}/pacientes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return await res.json(); }, updatePaciente: async (paciente: Paciente) => { const data = enforceUppercase(paciente); const res = await apiFetch(`${API_URL}/pacientes/${paciente.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return await res.json(); }, getGruposFamiliares: async (q?: string): Promise => { const url = q?.trim() ? `${API_URL}/grupos-familiares?q=${encodeURIComponent(q)}` : `${API_URL}/grupos-familiares`; const res = await apiFetch(url); const data = await res.json(); return Array.isArray(data) ? data : []; }, criarGrupoFamiliar: async (nome: string, responsavel_id?: string) => { const res = await apiFetch(`${API_URL}/grupos-familiares`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nome, responsavel_id }) }); return res.json(); }, getMembrosFamilia: async (grupoId: string) => { const res = await apiFetch(`${API_URL}/grupos-familiares/${grupoId}/membros`); return res.json(); }, getModelosReceita: async (especialidadeId?: string): Promise => { const url = especialidadeId ? `${API_URL}/modelos-receita?especialidadeId=${especialidadeId}` : `${API_URL}/modelos-receita`; const res = await apiFetch(url); const data = await res.json(); return Array.isArray(data) ? data : []; }, saveModeloReceita: async (modelo: Omit) => { const res = await apiFetch(`${API_URL}/modelos-receita`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modelo) }); return res.json(); }, updateModeloReceita: async (modelo: import('../types.ts').ModeloReceita) => { const res = await apiFetch(`${API_URL}/modelos-receita/${modelo.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modelo) }); return res.json(); }, deleteModeloReceita: async (id: string) => { await apiFetch(`${API_URL}/modelos-receita/${id}`, { method: 'DELETE' }); }, savePedidoExame: async (pedido: Omit) => { const res = await apiFetch(`${API_URL}/pedidos-exame`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(pedido) }); return res.json(); }, getPedidosExame: async (pacienteId?: string): Promise => { const url = pacienteId ? `${API_URL}/pedidos-exame?pacienteId=${pacienteId}` : `${API_URL}/pedidos-exame`; const res = await apiFetch(url); const data = await res.json(); return Array.isArray(data) ? data : []; }, getDentistas: async (clinicaId?: string): Promise => { const url = clinicaId ? `${API_URL}/dentistas?clinicaId=${clinicaId}` : `${API_URL}/dentistas`; const res = await apiFetch(url); return await res.json(); }, getPlanos: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/planos${clinicaId ? `?clinicaId=${clinicaId}` : ''}`); return await res.json(); }, updateDentista: async (dentista: Dentista) => { const data = enforceUppercase(dentista); const res = await apiFetch(`${API_URL}/dentistas/${dentista.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return await res.json(); }, saveDentista: async (dentista: Partial) => { const activeW = HybridBackend.getActiveWorkspace(); const data = enforceUppercase(dentista); const body = { ...data, id: Math.random().toString(), clinica_id: activeW?.id }; const res = await apiFetch(`${API_URL}/dentistas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return await res.json(); }, reorderDentistas: async (orders: { id: string; ordem: number }[]) => { const res = await apiFetch(`${API_URL}/dentistas/reorder`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orders }) }); return await res.json(); }, deleteDentista: async (id: string) => { await apiFetch(`${API_URL}/dentistas/${id}`, { method: 'DELETE' }); }, getHorariosByDentista: async (dentistaId: string, clinicaId: string): Promise => { const res = await apiFetch(`${API_URL}/dentistas/${dentistaId}/horarios?clinicaId=${clinicaId}`); return await res.json(); }, saveHorario: async (horario: Partial) => { const res = await apiFetch(`${API_URL}/dentistas/horarios`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(horario) }); return await res.json(); }, deleteHorario: async (id: string) => { await apiFetch(`${API_URL}/dentistas/horarios/${id}`, { method: 'DELETE' }); }, getProductivityReport: async (clinicaId: string, start?: string, end?: string) => { const res = await apiFetch(`${API_URL}/reports/productivity?clinicaId=${clinicaId}&start=${start || ''}&end=${end || ''}`); return await res.json(); }, updateEspecialidade: async (esp: Especialidade) => { const data = enforceUppercase(esp); const res = await apiFetch(`${API_URL}/especialidades/${esp.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return await res.json(); }, saveEspecialidade: async (esp: Partial) => { const data = enforceUppercase(esp); const body = { ...data, id: Math.random().toString() }; const res = await apiFetch(`${API_URL}/especialidades`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return await res.json(); }, reorderEspecialidades: async (orders: { id: string; ordem: number }[]) => { const res = await apiFetch(`${API_URL}/especialidades/reorder`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orders }) }); return await res.json(); }, deleteEspecialidade: async (id: string) => { await apiFetch(`${API_URL}/especialidades/${id}`, { method: 'DELETE' }); }, updateProcedimento: async (proc: Procedimento) => { const data = enforceUppercase(proc); const res = await apiFetch(`${API_URL}/procedimentos/${proc.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return await res.json(); }, saveProcedimento: async (proc: Partial) => { const data = enforceUppercase(proc); const body = { ...data, id: Math.random().toString() }; const res = await apiFetch(`${API_URL}/procedimentos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return await res.json(); }, reorderProcedimentos: async (orders: { id: string; ordem: number }[]) => { const res = await apiFetch(`${API_URL}/procedimentos/reorder`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orders }) }); return await res.json(); }, deleteProcedimento: async (id: string) => { await apiFetch(`${API_URL}/procedimentos/${id}`, { method: 'DELETE' }); }, updatePlano: async (plano: Plano) => { const data = enforceUppercase(plano); const res = await apiFetch(`${API_URL}/planos/${plano.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return await res.json(); }, savePlano: async (plano: Partial) => { const data = enforceUppercase(plano); const body = { ...data, id: Math.random().toString() }; const res = await apiFetch(`${API_URL}/planos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return await res.json(); }, deletePlano: async (id: string) => { await apiFetch(`${API_URL}/planos/${id}`, { method: 'DELETE' }); }, getEspecialidades: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/especialidades${clinicaId ? `?clinicaId=${clinicaId}` : ''}`); return await res.json(); }, getProcedimentos: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/procedimentos${clinicaId ? `?clinicaId=${clinicaId}` : ''}`); return await res.json(); }, getFinanceiro: async (): Promise => { const res = await apiFetch(`${API_URL}/financeiro`); return await res.json(); }, saveFinanceiro: async (item: Partial) => { const data = enforceUppercase(item); const res = await apiFetch(`${API_URL}/financeiro`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)}); return await res.json(); }, updateFinanceiro: async (item: FinanceiroItem) => { const data = enforceUppercase(item); const res = await apiFetch(`${API_URL}/financeiro/${item.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)}); return await res.json(); }, deleteFinanceiro: async (id: string) => { await apiFetch(`${API_URL}/financeiro/${id}`, { method: 'DELETE' }); }, getAgendamentos: async (): Promise => { const res = await apiFetch(`${API_URL}/agendamentos`, { }); return await res.json(); }, salvarAgendamento: async (ag: Partial) => { const res = await apiFetch(`${API_URL}/agendamentos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(ag) }); if (!res.ok) { const err = await res.json().catch(() => ({} as any)); // 409 com info de conflito → propaga p/ a UI oferecer "registrar interesse" if (res.status === 409 && err.conflito) { const e: any = new Error(err.message || 'Horário em conflito.'); e.conflito = true; e.ocupado = err.ocupado; throw e; } throw new Error(err.message || 'Erro ao salvar agendamento.'); } return await res.json(); }, // Fase 4 — registra interesse num horário ocupado (não trava o slot) registrarInteresse: async (payload: { dentistaId?: string; profissionalUsuarioId?: string; clinicaInteressadaId: string; solicitanteUsuarioId?: string; start: string; end?: string; tempoEsperaMin?: number; observacoes?: string; }) => { const res = await apiFetch(`${API_URL}/interesses`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) { const e = await res.json().catch(() => ({} as any)); throw new Error(e.message || 'Erro ao registrar interesse.'); } return await res.json(); }, getInteresses: async (params: { profissionalId?: string; clinicaId?: string }): Promise => { const qs = new URLSearchParams(); if (params.profissionalId) qs.set('profissionalId', params.profissionalId); if (params.clinicaId) qs.set('clinicaId', params.clinicaId); const res = await apiFetch(`${API_URL}/interesses?${qs.toString()}`, {}); return res.ok ? await res.json() : []; }, atualizarAgendamento: async (id: string, data: Partial) => { const res = await apiFetch(`${API_URL}/agendamentos/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) { const err = await res.json(); 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 })); }, getHistoricoAgendamento: async (id: string): Promise => { const res = await apiFetch(`${API_URL}/agendamentos/${id}/historico`, {}); const j = await res.json().catch(() => ({} as any)); return j.historico || []; }, getPendenciasReagendar: async (clinicaId?: string): Promise => { const cid = clinicaId || HybridBackend.getActiveWorkspace()?.id || ''; if (!cid) return []; const res = await apiFetch(`${API_URL}/agenda/pendencias?clinicaId=${cid}`, {}); const j = await res.json().catch(() => ({} as any)); return j.pendencias || []; }, resolverPendencia: async (id: string, resolucao?: string) => { const res = await apiFetch(`${API_URL}/agenda/pendencias/${id}/resolver`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resolucao }) }); return await res.json().catch(() => ({ success: res.ok })); }, getFaltasPaciente: async (pacienteId: string): Promise => { const res = await apiFetch(`${API_URL}/pacientes/${pacienteId}/faltas`, {}); const j = await res.json().catch(() => ({} as any)); return j.faltas || 0; }, getFamiliaPaciente: async (pacienteId: string): Promise<{ grupo: any; membros: any[] }> => { const res = await apiFetch(`${API_URL}/pacientes/${pacienteId}/familia`, {}); const j = await res.json().catch(() => ({} as any)); return { grupo: j.grupo || null, membros: j.membros || [] }; }, // Importa agendamentos do Google p/ dentro do sistema (admin/dono). Idempotente. importarGoogleAgenda: async (clinicaId?: string, dias?: number): Promise<{ success: boolean; criados?: number; pulados?: number; total?: number; message?: string }> => { const cid = clinicaId || HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/agenda/importar-google`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId: cid, dias }), }); return await res.json().catch(() => ({ success: false })); }, // Feed de atividade da equipe (auditoria) — default: hoje. getAtividadeAgenda: async (clinicaId?: string): Promise => { const cid = clinicaId || HybridBackend.getActiveWorkspace()?.id || ''; if (!cid) return []; const res = await apiFetch(`${API_URL}/agenda/atividade?clinicaId=${cid}`, {}); const j = await res.json().catch(() => ({} as any)); return j.atividade || []; }, // Presença na agenda (Fase 2): heartbeat + leitura de quem está online / última visita. pingPresencaAgenda: async (clinicaId?: string) => { const cid = clinicaId || HybridBackend.getActiveWorkspace()?.id || ''; if (!cid) return; try { await apiFetch(`${API_URL}/agenda/presenca`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clinicaId: cid }) }); } catch { /* ignore */ } }, getPresencaAgenda: async (clinicaId?: string): Promise<{ online: any[]; ultimas: any[] }> => { const cid = clinicaId || HybridBackend.getActiveWorkspace()?.id || ''; if (!cid) return { online: [], ultimas: [] }; const res = await apiFetch(`${API_URL}/agenda/presenca?clinicaId=${cid}`, {}); const j = await res.json().catch(() => ({} as any)); return { online: j.online || [], ultimas: j.ultimas || [] }; }, // ---- Mapeamento DB(snake) <-> TratamentoOrto(camel) ---- _ortoTratFromRow: (t: any): TratamentoOrto => ({ id: t.id, pacienteId: t.paciente_id || '', pacienteNome: t.paciente_nome, dataNascimento: t.data_nascimento || undefined, responsavel: t.responsavel || undefined, tipoPaciente: t.tipo_paciente || undefined, triagem: t.triagem || undefined, diagnostico: t.diagnostico || undefined, dataInicio: t.data_inicio || '', tipoAparelho: t.tipo_aparelho || '', extracoes: t.extracoes || undefined, sequenciaFios: t.sequencia_fios || undefined, elasticos: t.elasticos || undefined, iprPlanejado: t.ipr_planejado || undefined, tempoEstimado: t.tempo_estimado || undefined, valor: t.valor != null ? Number(t.valor) : undefined, formaPagamento: t.forma_pagamento || undefined, faseAtual: t.fase_atual || undefined, fioAtual: t.fio_atual || undefined, classificacao: t.classificacao || undefined, cooperacao: t.cooperacao || undefined, percentualConcluido: t.percentual_concluido != null ? Number(t.percentual_concluido) : undefined, ultimaConsulta: t.ultima_consulta || undefined, proximaConsulta: t.proxima_consulta || undefined, mesesRestantes: t.meses_restantes != null ? Number(t.meses_restantes) : undefined, status: t.status || 'Em Tratamento', contencaoTipo: t.contencao_tipo || undefined, contencaoSuperior: t.contencao_superior ?? undefined, contencaoInferior: t.contencao_inferior ?? undefined, tempoUso: t.tempo_uso || undefined, flags: t.flags || undefined, evolucoes: (t.evolucoes || []).map((e: any) => ({ id: e.id, data: e.data, tipo: e.tipo, procedimento: e.procedimento || '', proximoRetorno: e.proximo_retorno || '', fio: e.fio || undefined, elastico: e.elastico || undefined, procedimentos: e.procedimentos || undefined, higiene: e.higiene != null ? Number(e.higiene) : undefined, cooperacao: e.cooperacao || undefined, observacoes: e.observacoes || undefined, })), }), _ortoTratToRow: (t: Partial): Record => { const m: Record = { paciente_id: t.pacienteId, paciente_nome: t.pacienteNome, data_nascimento: t.dataNascimento, responsavel: t.responsavel, tipo_paciente: t.tipoPaciente, triagem: t.triagem, diagnostico: t.diagnostico, data_inicio: t.dataInicio, tipo_aparelho: t.tipoAparelho, extracoes: t.extracoes, sequencia_fios: t.sequenciaFios, elasticos: t.elasticos, ipr_planejado: t.iprPlanejado, tempo_estimado: t.tempoEstimado, valor: t.valor, forma_pagamento: t.formaPagamento, fase_atual: t.faseAtual, fio_atual: t.fioAtual, classificacao: t.classificacao, cooperacao: t.cooperacao, percentual_concluido: t.percentualConcluido, ultima_consulta: t.ultimaConsulta, proxima_consulta: t.proximaConsulta, meses_restantes: t.mesesRestantes, status: t.status, flags: t.flags, contencao_tipo: t.contencaoTipo, contencao_superior: t.contencaoSuperior, contencao_inferior: t.contencaoInferior, tempo_uso: t.tempoUso, }; const out: Record = {}; for (const k in m) if (m[k] !== undefined) out[k] = m[k]; return out; }, getOrtoTreatments: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id; const res = await HybridBackend._ortoJson(`/orto/tratamentos${clinicaId ? `?clinicaId=${clinicaId}` : ''}`, {}, { success: true, tratamentos: [] }); if (!res.success) return []; return (res.tratamentos || []).map((t: any) => HybridBackend._ortoTratFromRow(t)); }, createTratamentoOrto: async (t: Partial): Promise<{ success: boolean; id?: string; message?: string }> => { const row = HybridBackend._ortoTratToRow(t); row.clinica_id = HybridBackend.getActiveWorkspace()?.id; return HybridBackend._ortoJson(`/orto/tratamentos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(row), }, { success: false, message: 'BACKEND INDISPONÍVEL.' }); }, saveEvolucaoOrto: async (tratamentoId: string, evolucao: Partial): Promise => { const res = await HybridBackend._ortoJson(`/orto/tratamentos/${tratamentoId}/evolucoes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data: evolucao.data, tipo: evolucao.tipo, procedimento: evolucao.procedimento, proximo_retorno: evolucao.proximoRetorno, fio: evolucao.fio, elastico: evolucao.elastico, procedimentos: evolucao.procedimentos, higiene: evolucao.higiene, cooperacao: evolucao.cooperacao, observacoes: evolucao.observacoes, }), }, { success: false }); const e = res.evolucao || {}; return { id: e.id || ('ev_' + Math.random().toString(36).slice(2, 8)), data: e.data || evolucao.data || new Date().toISOString().split('T')[0], tipo: (e.tipo || evolucao.tipo || 'Manutenção') as any, procedimento: e.procedimento ?? evolucao.procedimento ?? '', proximoRetorno: e.proximo_retorno ?? evolucao.proximoRetorno ?? '', fio: e.fio ?? evolucao.fio, elastico: e.elastico ?? evolucao.elastico, procedimentos: e.procedimentos ?? evolucao.procedimentos, higiene: e.higiene ?? evolucao.higiene, cooperacao: (e.cooperacao ?? evolucao.cooperacao) as any, observacoes: e.observacoes ?? evolucao.observacoes, }; }, updateTratamentoOrto: async (tratamento: Partial): Promise => { if (!tratamento.id) return; const row = HybridBackend._ortoTratToRow(tratamento); await HybridBackend._ortoJson(`/orto/tratamentos/${tratamento.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(row), }, { success: false }); }, finalizarTratamentoOrto: async (tratamentoId: string, contencao: { tipo: string; superior: boolean; inferior: boolean; tempoUso: string }): Promise => { await HybridBackend._ortoJson(`/orto/tratamentos/${tratamentoId}/finalizar`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(contencao), }, { success: false }); }, // =================================================================== // ORTODONTIA · DOCUMENTAÇÃO FOTOGRÁFICA · TUTORIA // Chamam os endpoints reais (/api/orto/*, /api/tutoria/*, /api/tutores*, // /api/admin/*). Enquanto o backend ainda não expõe essas rotas, o // helper `_ortoJson` degrada para um fallback seguro (listas vazias / // success:false) em vez de deixar a UI quebrar com "is not a function". // =================================================================== _ortoJson: async (path: string, options: RequestInit = {}, fallback: any = { success: false }) => { try { const res = await apiFetch(`${API_URL}${path}`, options); if (!res.ok) return fallback; return await res.json(); } catch { return fallback; } }, // --- Documentação fotográfica --- getOrtoSessoes: async (tratamentoId: string) => HybridBackend._ortoJson(`/orto/sessoes?tratamentoId=${encodeURIComponent(tratamentoId)}`, {}, { success: true, sessoes: [] }), getOrtoSessao: async (tratamentoId: string, tipo: string, clinicaId?: string, rotulo?: string, dataRef?: string) => HybridBackend._ortoJson(`/orto/sessoes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tratamento_id: tratamentoId, tipo, clinica_id: clinicaId, rotulo, data_ref: dataRef }), }, { success: false }), uploadOrtoFoto: async (sessaoId: string, slot: string, file: File) => { const fd = new FormData(); fd.append('sessao_id', sessaoId); fd.append('slot', slot); fd.append('foto', file); return HybridBackend._ortoJson(`/orto/fotos`, { method: 'POST', body: fd }, { success: false }); }, deleteOrtoFoto: async (fotoId: string) => HybridBackend._ortoJson(`/orto/fotos/${fotoId}`, { method: 'DELETE' }, { success: false }), getOrtoAnotacoes: async (fotoId: string) => HybridBackend._ortoJson(`/orto/fotos/${fotoId}/anotacoes`, {}, { success: true, anotacoes: [] }), saveOrtoAnotacoes: async (fotoId: string, anotacoes: any[]) => HybridBackend._ortoJson(`/orto/fotos/${fotoId}/anotacoes`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ anotacoes }), }, { success: false }), // --- Tutoria (lado clínica + lado tutor) --- getTutores: async () => HybridBackend._ortoJson(`/tutores`, {}, { success: true, tutores: [] }), createSolicitacaoTutoria: async (payload: any) => HybridBackend._ortoJson(`/tutoria/solicitacoes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }, { success: false, message: 'BACKEND DE TUTORIA INDISPONÍVEL.' }), getSolicitacoesTutoria: async (clinicaId?: string, tratamentoId?: string) => { const params = new URLSearchParams(); if (clinicaId) params.set('clinicaId', clinicaId); if (tratamentoId) params.set('tratamentoId', tratamentoId); const qs = params.toString(); return HybridBackend._ortoJson(`/tutoria/solicitacoes${qs ? `?${qs}` : ''}`, {}, { success: true, solicitacoes: [] }); }, getSolicitacaoTutoria: async (id: string) => HybridBackend._ortoJson(`/tutoria/solicitacoes/${id}`, {}, { success: false }), sendMensagemTutoria: async (solId: string, conteudo: string, autorTipo: 'CLINICA' | 'TUTOR', autorNome: string) => HybridBackend._ortoJson(`/tutoria/solicitacoes/${solId}/mensagens`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conteudo, autor_tipo: autorTipo, autor_nome: autorNome }), }, { success: false }), updateStatusTutoria: async (solId: string, status: string) => HybridBackend._ortoJson(`/tutoria/solicitacoes/${solId}/status`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }), }, { success: false }), // --- Candidatura / perfil de tutor --- getMinhaCandidatura: async (usuarioId: string) => HybridBackend._ortoJson(`/tutoria/candidaturas/minha?usuarioId=${encodeURIComponent(usuarioId || '')}`, {}, { success: true, candidatura: null }), submitCandidatura: async (payload: any) => HybridBackend._ortoJson(`/tutoria/candidaturas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }, { success: false, message: 'BACKEND DE TUTORIA INDISPONÍVEL.' }), getTutorPorUsuario: async (usuarioId: string) => HybridBackend._ortoJson(`/tutores/por-usuario/${encodeURIComponent(usuarioId || '')}`, {}, { success: true, tutor: null }), updateCredenciaisPagamentoTutor: async (tutorId: string, payload: any) => HybridBackend._ortoJson(`/tutores/${tutorId}/credenciais`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }, { success: false, message: 'BACKEND DE TUTORIA INDISPONÍVEL.' }), iniciarPagamentoCandidatura: async (candidaturaId: string) => HybridBackend._ortoJson(`/tutoria/candidaturas/${candidaturaId}/pagamento`, { method: 'POST' }, { success: false, message: 'PAGAMENTO INDISPONÍVEL.' }), // --- Admin de tutores --- getAdminTutorConfig: async () => HybridBackend._ortoJson(`/admin/tutor-config`, {}, { success: true, config: {} }), updateAdminTutorConfig: async (payload: any) => HybridBackend._ortoJson(`/admin/tutor-config`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }, { success: false }), getAdminCandidaturas: async (status?: string) => HybridBackend._ortoJson(`/admin/candidaturas${status ? `?status=${encodeURIComponent(status)}` : ''}`, {}, { success: true, candidaturas: [] }), updateStatusCandidatura: async (id: string, status: string, observacao?: string) => HybridBackend._ortoJson(`/admin/candidaturas/${id}/status`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status, observacao }), }, { success: false }), confirmarPagamentoCandidatura: async (id: string) => HybridBackend._ortoJson(`/admin/candidaturas/${id}/confirmar-pagamento`, { method: 'POST' }, { success: false }), getRepassesPendentes: async () => HybridBackend._ortoJson(`/admin/repasses`, {}, { success: true, repasses: [] }), confirmarRepasse: async (id: string) => HybridBackend._ortoJson(`/admin/repasses/${id}/confirmar`, { method: 'POST' }, { success: false }), // --- GESTÃO DE EQUIPE (membros / setores / funções) --- getMembros: async (clinicaId: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/membros`, {}, { success: true, membros: [] }), addMembro: async (clinicaId: string, email: string, role: string, setorId?: string, funcaoId?: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/membros`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, role, setor_id: setorId, funcao_id: funcaoId }), }, { success: false, message: 'BACKEND INDISPONÍVEL.' }), updateMembro: async (clinicaId: string, userId: string, data: any) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/membros/${userId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }, { success: false }), removeMembro: async (clinicaId: string, userId: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/membros/${userId}`, { method: 'DELETE' }, { success: false }), resetMembroSenha: async (clinicaId: string, userId: string): Promise<{ success: boolean; email?: string; tempPassword?: string; message?: string }> => HybridBackend._ortoJson(`/clinicas/${clinicaId}/membros/${userId}/reset-senha`, { method: 'POST' }, { success: false, message: 'BACKEND INDISPONÍVEL.' }), getSetores: async (clinicaId: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/setores`, {}, { success: true, setores: [] }), saveSetor: async (clinicaId: string, data: any) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/setores`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }, { success: false }), updateSetor: async (clinicaId: string, id: string, data: any) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/setores/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }, { success: false }), deleteSetor: async (clinicaId: string, id: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/setores/${id}`, { method: 'DELETE' }, { success: false }), getFuncoes: async (clinicaId: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/funcoes`, {}, { success: true, funcoes: [] }), saveFuncao: async (clinicaId: string, data: any) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/funcoes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }, { success: false }), updateFuncao: async (clinicaId: string, id: string, data: any) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/funcoes/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }, { success: false }), deleteFuncao: async (clinicaId: string, id: string) => HybridBackend._ortoJson(`/clinicas/${clinicaId}/funcoes/${id}`, { method: 'DELETE' }, { success: false }), // --- LEAD & NOTIFICATION METHODS --- saveLead: async (lead: Partial) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const newLead: Lead = { id: Math.random().toString(), nome: toUpper(lead.nome), sobrenome: toUpper(lead.sobrenome), cpf: lead.cpf || '', whatsapp: lead.whatsapp || '', tipoInteresse: lead.tipoInteresse || 'Particular', dataCadastro: new Date().toISOString(), status: 'Novo', clinica_id: clinicaId, } as any; await apiFetch(`${API_URL}/leads`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newLead) }); return newLead; }, getLeads: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/leads${clinicaId ? `?clinicaId=${clinicaId}` : ''}`); return await res.json(); }, getNotifications: async (): Promise => { try { const res = await apiFetch(`${API_URL}/notificacoes`); 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 { await apiFetch(`${API_URL}/notificacoes/read-all`, { method: 'POST' }); } catch (err) { console.error("Erro ao marcar todas como lidas:", err); } }, deleteNotification: async (id: string) => { try { await apiFetch(`${API_URL}/notificacoes/${id}`, { method: 'DELETE' }); } catch (err) { console.error("Erro ao excluir notificação:", err); } }, getSettings: async (): Promise => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/settings${clinicaId ? `?clinicaId=${clinicaId}` : ''}`); return await res.json(); }, saveSettings: async (settings: Settings) => { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; const res = await apiFetch(`${API_URL}/settings${clinicaId ? `?clinicaId=${clinicaId}` : ''}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings) }); return await res.json(); }, getGoogleEvents: async (): Promise => { try { const clinicaId = HybridBackend.getActiveWorkspace()?.id || ''; if (!clinicaId) return []; const res = await apiFetch(`${API_URL}/google/events?clinicaId=${clinicaId}`); const d = await res.json(); return Array.isArray(d) ? d : []; } catch { return []; } }, getContratos: async (pacienteId?: string): Promise => { const q = pacienteId ? `?paciente_id=${pacienteId}` : ''; const res = await apiFetch(`${API_URL}/contratos${q}`); return res.json(); }, saveContrato: async (c: Partial & { items: any[] }): Promise<{ id: string }> => { const res = await apiFetch(`${API_URL}/contratos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(c), }); return res.json(); }, updateContrato: async (c: Contrato): Promise => { await apiFetch(`${API_URL}/contratos/${c.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(c), }); }, deleteContrato: async (id: string): Promise => { await apiFetch(`${API_URL}/contratos/${id}`, { method: 'DELETE' }); }, getRxConfig: async (clinicaId: string): Promise<{ rx_url: string; rx_token: string } | null> => { try { const res = await apiFetch(`${API_URL}/rx/config?clinicaId=${encodeURIComponent(clinicaId)}`); return await res.json(); } catch { return { rx_url: 'https://rx.scoreodonto.com', rx_token: '' }; } }, getRxDevices: async (clinicaId: string): Promise => { try { const res = await apiFetch(`${API_URL}/rx/devices?clinicaId=${encodeURIComponent(clinicaId)}`); return await res.json(); } catch { return []; } }, createRxDevice: async (clinicaId: string, pcName: string, extra?: { clinicName?: string; 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 }; } }, };