/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Criado e desenvolvido por Leonardo Nohama | | Tecnologia aplicada à escuta. | Estrutura para o cuidado. | | Arquivo: src/services/DocumentGenerate.service.js | Data: 2026 | Local: São Carlos/SP — Brasil |-------------------------------------------------------------------------- | © 2026 — Todos os direitos reservados |-------------------------------------------------------------------------- */ import { supabase } from '@/lib/supabase/client'; const BUCKET = 'generated-docs'; // ── Helpers ────────────────────────────────────────────────── async function getOwnerId() { const { data, error } = await supabase.auth.getUser(); if (error) throw error; const uid = data?.user?.id; if (!uid) throw new Error('Sessão inválida.'); return uid; } async function getActiveTenantId(uid) { const { data, error } = await supabase .from('tenant_members') .select('tenant_id') .eq('user_id', uid) .eq('status', 'active') .order('created_at', { ascending: false }) .limit(1) .single(); if (error) throw error; if (!data?.tenant_id) throw new Error('Tenant não encontrado.'); return data.tenant_id; } // ── Carregar dados para preenchimento ─────────────────────── /** * Busca dados do paciente para preencher variaveis do template. */ export async function loadPatientData(patientId) { const { data, error } = await supabase .from('patients') .select(` nome_completo, nome_social, cpf, data_nascimento, telefone, email_principal, endereco, numero, bairro, cidade, estado, cep `) .eq('id', patientId) .single(); if (error) throw error; const p = data; const endereco = [p.endereco, p.numero, p.bairro, p.cidade, p.estado] .filter(Boolean).join(', '); return { paciente_nome: p.nome_completo || '', paciente_nome_social: p.nome_social || '', paciente_cpf: p.cpf || '', paciente_data_nascimento: p.data_nascimento ? new Date(p.data_nascimento).toLocaleDateString('pt-BR') : '', paciente_telefone: p.telefone || '', paciente_email: p.email_principal || '', paciente_endereco: endereco }; } /** * Busca dados da sessao (agenda_evento) para preencher variaveis. */ export async function loadSessionData(agendaEventoId) { if (!agendaEventoId) return {}; const { data, error } = await supabase .from('agenda_eventos') .select('inicio_em, fim_em, modalidade, price') .eq('id', agendaEventoId) .single(); if (error) return {}; const s = data; const inicio = s.inicio_em ? new Date(s.inicio_em) : null; const fim = s.fim_em ? new Date(s.fim_em) : null; return { data_sessao: inicio ? inicio.toLocaleDateString('pt-BR') : '', hora_inicio: inicio ? inicio.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '', hora_fim: fim ? fim.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '', modalidade: s.modalidade || '', valor: s.price ? `R$ ${Number(s.price).toFixed(2).replace('.', ',')}` : '' }; } /** * Busca dados do terapeuta (profile + tenant_member). */ export async function loadTherapistData() { const ownerId = await getOwnerId(); const { data: profile } = await supabase .from('profiles') .select('full_name, phone') .eq('id', ownerId) .single(); // Email vem de auth.users (nao existe em profiles) const { data: userData } = await supabase.auth.getUser(); const email = userData?.user?.email || ''; return { terapeuta_nome: profile?.full_name || '', terapeuta_crp: '', // CRP ainda nao existe no banco — preencher manualmente terapeuta_email: email, terapeuta_telefone: profile?.phone || '' }; } /** * Busca dados da clinica (tenant). */ export async function loadClinicData(tenantId) { // Usa select('*') pois campos de endereço (logradouro, numero, etc.) // dependem da migration 003_tenants_address_fields ter sido aplicada const { data: tenant } = await supabase .from('tenants') .select('*') .eq('id', tenantId) .maybeSingle(); if (!tenant) { return { clinica_nome: '', clinica_endereco: '', clinica_telefone: '', clinica_cnpj: '' }; } // Usa campos estruturados se disponiveis, senao cai no address texto livre const endereco = tenant.logradouro ? [tenant.logradouro, tenant.numero, tenant.bairro, tenant.cidade, tenant.estado] .filter(Boolean).join(', ') : tenant.address || ''; return { clinica_nome: tenant.name || '', clinica_endereco: endereco, clinica_telefone: tenant.phone || '', clinica_cnpj: '' }; } // ── Montar dados gerais ───────────────────────────────────── function getDateVariables() { const now = new Date(); const meses = [ 'janeiro', 'fevereiro', 'março', 'abril', 'maio', 'junho', 'julho', 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro' ]; return { data_atual: now.toLocaleDateString('pt-BR'), data_atual_extenso: `${now.getDate()} de ${meses[now.getMonth()]} de ${now.getFullYear()}` }; } /** * Carrega todos os dados necessarios para preencher um template. */ export async function loadAllVariables(patientId, agendaEventoId = null) { const ownerId = await getOwnerId(); const tenantId = await getActiveTenantId(ownerId); const [patient, session, therapist, clinic] = await Promise.all([ loadPatientData(patientId), loadSessionData(agendaEventoId), loadTherapistData(), loadClinicData(tenantId) ]); return { ...patient, ...session, ...therapist, ...clinic, ...getDateVariables(), cidade_estado: clinic.clinica_endereco ? `${clinic.clinica_endereco.split(', ').slice(-2).join('/')}` : '' }; } // ── Preencher template ────────────────────────────────────── /** * Substitui {{variavel}} no HTML pelos valores fornecidos. */ export function fillTemplate(html, variables = {}) { return String(html || '').replace(/\{\{(\w+)\}\}/g, (match, key) => { return variables[key] !== undefined ? String(variables[key]) : match; }); } /** * Monta o HTML completo do documento (cabecalho + corpo + rodape). */ export function buildFullHtml(template, variables = {}) { const cabecalho = fillTemplate(template.cabecalho_html || '', variables); const corpo = fillTemplate(template.corpo_html || '', variables); const rodape = fillTemplate(template.rodape_html || '', variables); return `
${cabecalho}
${corpo}
`.trim(); } // ── Gerar PDF (jsPDF + html2canvas via pdf.service) ──────── import { htmlToPdfBlob, htmlToPdfDownload, htmlToPdfOpen } from '@/services/pdf.service'; /** * Gera um Blob PDF a partir do template preenchido. */ export async function generatePdfBlob(template, variables = {}) { const html = buildFullHtml(template, variables); return await htmlToPdfBlob(html); } /** * Gera PDF e dispara download automatico. */ export async function generateAndDownloadPdf(template, variables = {}, filename = 'documento.pdf') { const html = buildFullHtml(template, variables); await htmlToPdfDownload(html, filename); } /** * Abre o PDF em nova aba para impressao. */ export async function printDocument(template, variables = {}) { const html = buildFullHtml(template, variables); await htmlToPdfOpen(html); } // ── Salvar documento gerado ───────────────────────────────── /** * Registra um documento gerado na tabela document_generated. * O PDF deve ser passado como Blob (gerado client-side ou server-side). * * @param {object} params * @param {string} params.templateId * @param {string} params.patientId * @param {object} params.dadosPreenchidos - snapshot dos dados usados * @param {Blob} params.pdfBlob - PDF gerado (opcional — pode ser null se so print) * @returns {object} registro criado */ export async function saveGeneratedDocument({ templateId, patientId, dadosPreenchidos, pdfBlob, templateNome }) { const ownerId = await getOwnerId(); const tenantId = await getActiveTenantId(ownerId); let pdfPath = ''; const timestamp = Date.now(); const safeNome = (templateNome || 'documento') .normalize('NFD').replace(/[\u0300-\u036f]/g, '') // remove acentos .replace(/[^a-zA-Z0-9_-]/g, '_'); const filename = `${safeNome}_${timestamp}.pdf`; // Se tiver um blob PDF, faz upload ao Storage if (pdfBlob) { pdfPath = `${tenantId}/${patientId}/${filename}`; const { error: upErr } = await supabase.storage .from(BUCKET) .upload(pdfPath, pdfBlob, { contentType: 'application/pdf' }); if (upErr) throw upErr; } // Registra na tabela document_generated const { data, error } = await supabase .from('document_generated') .insert({ template_id: templateId, patient_id: patientId, tenant_id: tenantId, dados_preenchidos: dadosPreenchidos || {}, pdf_path: pdfPath, storage_bucket: BUCKET, gerado_por: ownerId }) .select('*') .single(); if (error) throw error; // Registra na tabela documents para aparecer na lista do paciente // Reutiliza o mesmo arquivo do bucket generated-docs (sem upload duplicado) if (pdfPath) { await supabase .from('documents') .insert({ owner_id: ownerId, tenant_id: tenantId, patient_id: patientId, bucket_path: pdfPath, storage_bucket: BUCKET, nome_original: filename.replace(/_/g, ' ').replace('.pdf', '') + '.pdf', mime_type: 'application/pdf', tamanho_bytes: pdfBlob?.size || null, tipo_documento: 'laudo', descricao: `Gerado a partir do template: ${templateNome || 'documento'}`, tags: ['gerado'], visibilidade: 'privado', status_revisao: 'aprovado', uploaded_by: ownerId }); } return data; } /** * Lista documentos gerados de um paciente. */ export async function listGeneratedDocuments(patientId) { const ownerId = await getOwnerId(); const { data, error } = await supabase .from('document_generated') .select('*, document_templates(nome_template, tipo)') .eq('gerado_por', ownerId) .eq('patient_id', patientId) .order('gerado_em', { ascending: false }); if (error) throw error; return data || []; }