6ae651a8ae
ROADMAP item #1.4 #14. Fecha Fase 1.4 Fiscal minimo (parcial — #15 NFS-e fica pra depois). DocumentGenerate.service estendido: - loadTherapistData puxa registro profissional (#5 migration) e expoe terapeuta_registro auto-formatado ("CRP 12345/SP", "CRM 98765/RJ"). terapeuta_crp legacy mantido por compat — preenche somente quando tipo=CRP. - loadClinicData formata tenants.cpf_cnpj (11 ou 14 digitos) em CPF (XXX.XXX.XXX-XX) ou CNPJ (XX.XXX.XXX/XXXX-XX). - loadAllVariables aceita {extras} (valor, formaPagamento) e computa valor_extenso via novo helper utils/valorExtenso.js (pt-BR completo ate 999 milhoes). - saveGeneratedDocument ganha templateTipo + usa TEMPLATE_TYPE_TO_DOC_TYPE mapping (recibo_pagamento -> 'recibo', laudo -> 'laudo', atestado -> 'atestado' etc) em vez de hardcoded 'laudo'. - emitirReciboParaSessao(eventoId, opts) — quick path one-call: busca template recibo_pagamento global, carrega variaveis, gera PDF blob, salva no Storage + documents + document_generated, dispara download. Migration 20260521000008 substitui no template recibo_pagamento "Psicologo(a) - CRP {{terapeuta_crp}}" por "{{terapeuta_registro}}" e atualiza variaveis[]. Universal — funciona com qualquer conselho (CRP/CRM/CRFa/CREFITO/CRESS/CRN). DocumentTemplates.service.TEMPLATE_VARIABLES ganha terapeuta_ registro + _tipo + _numero + _uf (terapeuta_crp marcado legacy). useDocumentGenerate.generateAndSave passa templateTipo no save. AgendaEventoFinanceiroPanel ganha botao "Emitir recibo" (icon pi-file-pdf, outlined, full width) que aparece SOMENTE quando record.status === 'paid'. Toast de sucesso/erro. Loading state. Fluxo end-to-end: terapeuta marca sessao como paga -> botao "Emitir recibo" aparece -> click -> PDF baixado + aparece em /clinic/documents/templates do paciente como tipo 'recibo'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
543 lines
19 KiB
JavaScript
543 lines
19 KiB
JavaScript
/*
|
|
|--------------------------------------------------------------------------
|
|
| 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';
|
|
import { valorExtenso } from '@/utils/valorExtenso';
|
|
|
|
const BUCKET = 'generated-docs';
|
|
|
|
// Mapa tipo do template → tipo_documento (CHECK constraint de documents).
|
|
// Recibos vão pra 'recibo'; demais ficam em 'outro' (CHECK não tem entradas
|
|
// específicas pra cada consent form, mas 'outro' é válido).
|
|
const TEMPLATE_TYPE_TO_DOC_TYPE = {
|
|
recibo_pagamento: 'recibo',
|
|
declaracao_comparecimento: 'declaracao',
|
|
declaracao_inicio_tratamento: 'declaracao',
|
|
atestado_psicologico: 'atestado',
|
|
relatorio_acompanhamento: 'relatorio_externo',
|
|
laudo_psicologico: 'laudo',
|
|
parecer_psicologico: 'laudo',
|
|
encaminhamento: 'declaracao'
|
|
};
|
|
|
|
function mapTipoDocumento(templateTipo) {
|
|
return TEMPLATE_TYPE_TO_DOC_TYPE[templateTipo] || 'outro';
|
|
}
|
|
|
|
/**
|
|
* Formata o registro profissional: "CRP 12345/SP".
|
|
* Funciona com qualquer conselho (CRP, CRM, CRFa, CREFITO etc).
|
|
*/
|
|
function formatRegistroProfissional({ tipo, numero, uf }) {
|
|
if (!tipo || !numero) return '';
|
|
const parts = [tipo, numero];
|
|
if (uf) parts[1] = `${numero}/${uf}`;
|
|
return parts.join(' ');
|
|
}
|
|
|
|
// ── 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).
|
|
*
|
|
* Inclui registro profissional (CRP/CRM/CRFa etc) adicionado pela migration
|
|
* 20260521000003. Retorna:
|
|
* - terapeuta_registro: "CRP 12345/SP" auto-formatado (genérico p/ qualquer conselho)
|
|
* - terapeuta_crp: compat — só preenchido se o tipo for CRP (templates legacy)
|
|
* - terapeuta_registro_tipo / _numero / _uf: campos individuais p/ uso fino
|
|
*/
|
|
export async function loadTherapistData() {
|
|
const ownerId = await getOwnerId();
|
|
|
|
const { data: profile } = await supabase
|
|
.from('profiles')
|
|
.select('full_name, phone, professional_registration_type, professional_registration_number, professional_registration_uf')
|
|
.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 || '';
|
|
|
|
const tipo = profile?.professional_registration_type || '';
|
|
const numero = profile?.professional_registration_number || '';
|
|
const uf = profile?.professional_registration_uf || '';
|
|
const registro = formatRegistroProfissional({ tipo, numero, uf });
|
|
|
|
return {
|
|
terapeuta_nome: profile?.full_name || '',
|
|
terapeuta_email: email,
|
|
terapeuta_telefone: profile?.phone || '',
|
|
terapeuta_registro: registro,
|
|
terapeuta_registro_tipo: tipo,
|
|
terapeuta_registro_numero: numero,
|
|
terapeuta_registro_uf: uf,
|
|
// Compat: templates antigos referenciam {{terapeuta_crp}} — preenche só
|
|
// o número/UF (sem prefixo) pra não duplicar com o "CRP" já no HTML.
|
|
// Quando o registro não é CRP, retorna vazio (template visualmente errado
|
|
// pede pra usar {{terapeuta_registro}}).
|
|
terapeuta_crp: tipo === 'CRP' ? (uf ? `${numero}/${uf}` : numero) : ''
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Busca dados da clinica (tenant).
|
|
*
|
|
* `tenants.cpf_cnpj` aceita 11 (CPF) ou 14 (CNPJ) dígitos. Pra `clinica_cnpj`
|
|
* só preenche se tiver 14 dígitos (formatado XX.XXX.XXX/XXXX-XX).
|
|
*/
|
|
export async function loadClinicData(tenantId) {
|
|
const { data: tenant } = await supabase
|
|
.from('tenants')
|
|
.select('*')
|
|
.eq('id', tenantId)
|
|
.maybeSingle();
|
|
|
|
if (!tenant) {
|
|
return { clinica_nome: '', clinica_endereco: '', clinica_telefone: '', clinica_cnpj: '' };
|
|
}
|
|
|
|
const endereco = tenant.logradouro
|
|
? [tenant.logradouro, tenant.numero, tenant.bairro, tenant.cidade, tenant.estado]
|
|
.filter(Boolean).join(', ')
|
|
: tenant.address || '';
|
|
|
|
const digits = String(tenant.cpf_cnpj || '').replace(/\D/g, '');
|
|
let cnpj = '';
|
|
if (digits.length === 14) {
|
|
cnpj = digits.replace(/^(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})$/, '$1.$2.$3/$4-$5');
|
|
} else if (digits.length === 11) {
|
|
cnpj = digits.replace(/^(\d{3})(\d{3})(\d{3})(\d{2})$/, '$1.$2.$3-$4');
|
|
}
|
|
|
|
return {
|
|
clinica_nome: tenant.name || '',
|
|
clinica_endereco: endereco,
|
|
clinica_telefone: tenant.phone || '',
|
|
clinica_cnpj: 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.
|
|
*
|
|
* @param {string} patientId
|
|
* @param {string|null} agendaEventoId - sessão p/ data_sessao + valor
|
|
* @param {object} extras - overrides ad-hoc (ex: { valor: 150, forma_pagamento: 'PIX' })
|
|
*/
|
|
export async function loadAllVariables(patientId, agendaEventoId = null, extras = {}) {
|
|
const ownerId = await getOwnerId();
|
|
const tenantId = await getActiveTenantId(ownerId);
|
|
|
|
const [patient, session, therapist, clinic] = await Promise.all([
|
|
loadPatientData(patientId),
|
|
loadSessionData(agendaEventoId),
|
|
loadTherapistData(),
|
|
loadClinicData(tenantId)
|
|
]);
|
|
|
|
// Resolve valor numérico (extras tem prioridade sobre session)
|
|
const valorNum = extras.valor != null
|
|
? Number(extras.valor)
|
|
: (session.valor ? Number(String(session.valor).replace(/[R$\s.]/g, '').replace(',', '.')) : null);
|
|
|
|
const valorFormatted = isFinite(valorNum)
|
|
? `R$ ${valorNum.toFixed(2).replace('.', ',')}`
|
|
: (session.valor || '');
|
|
|
|
const valorExtensoStr = isFinite(valorNum) ? valorExtenso(valorNum) : '';
|
|
|
|
const merged = {
|
|
...patient,
|
|
...session,
|
|
...therapist,
|
|
...clinic,
|
|
...getDateVariables(),
|
|
// Overrides explícitos (forma_pagamento, valor, qualquer outra chave)
|
|
...extras,
|
|
// Computados — sempre rebaixam extras nos campos que controlamos
|
|
valor: valorFormatted,
|
|
valor_extenso: valorExtensoStr,
|
|
forma_pagamento: extras.forma_pagamento || session.forma_pagamento || '',
|
|
cidade_estado: clinic.clinica_endereco
|
|
? `${clinic.clinica_endereco.split(', ').slice(-2).join('/')}`
|
|
: ''
|
|
};
|
|
|
|
return merged;
|
|
}
|
|
|
|
// ── 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 `
|
|
<!DOCTYPE html>
|
|
<html lang="pt-BR" style="color-scheme:light;">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
*, *::before, *::after { color-scheme: light; }
|
|
@page { size: A4; margin: 20mm 15mm 25mm 15mm; }
|
|
html, body {
|
|
all: initial;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
font-size: 12pt;
|
|
line-height: 1.6;
|
|
color: #1a1a1a;
|
|
background: #ffffff;
|
|
-webkit-print-color-adjust: exact;
|
|
}
|
|
h1, h2, h3, h4, p, ul, ol, li, table, tr, td, th, div, span, strong, em, hr, a {
|
|
all: revert;
|
|
color: inherit;
|
|
font-family: inherit;
|
|
}
|
|
h2 { font-size: 16pt; margin-bottom: 16px; }
|
|
h3 { font-size: 13pt; margin-top: 20px; margin-bottom: 8px; }
|
|
p { margin: 8px 0; }
|
|
table { border-collapse: collapse; }
|
|
td { padding: 4px 8px; }
|
|
hr { border: none; border-top: 1px solid #333333; }
|
|
a { color: #2563eb; }
|
|
.doc-header { text-align: center; margin-bottom: 24px; padding-bottom: 12px; border-bottom: 1px solid #cccccc; }
|
|
.doc-header img { max-height: 60px; margin-bottom: 8px; }
|
|
.doc-content { min-height: 600px; }
|
|
.doc-footer { margin-top: 40px; padding-top: 12px; border-top: 1px solid #cccccc; font-size: 10pt; color: #666666; text-align: center; }
|
|
.signature-line { margin-top: 60px; text-align: center; }
|
|
.signature-line hr { width: 250px; margin: 0 auto 4px; border: none; border-top: 1px solid #333333; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="doc-header">${cabecalho}</div>
|
|
<div class="doc-content">${corpo}</div>
|
|
<div class="doc-footer">${rodape}</div>
|
|
</body>
|
|
</html>`.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, templateTipo }) {
|
|
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: mapTipoDocumento(templateTipo),
|
|
descricao: `Gerado a partir do template: ${templateNome || 'documento'}`,
|
|
tags: ['gerado'],
|
|
visibilidade: 'privado',
|
|
status_revisao: 'aprovado',
|
|
uploaded_by: ownerId
|
|
});
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
// ── Quick path: emitir recibo de sessão ─────────────────────
|
|
//
|
|
// Wrapper de alto nível que tudo numa chamada só:
|
|
// 1. busca template global 'recibo_pagamento'
|
|
// 2. carrega variáveis (paciente + sessão + terapeuta + clínica + valor + extenso)
|
|
// 3. gera PDF
|
|
// 4. salva no Storage + tabelas document_generated + documents
|
|
// 5. dispara download
|
|
//
|
|
// Usado pelo popover da agenda e painel financeiro do paciente.
|
|
// Pra fluxo customizado (escolher template, editar variáveis, etc),
|
|
// continue usando DocumentGenerateDialog → useDocumentGenerate.
|
|
//
|
|
export async function emitirReciboParaSessao(agendaEventoId, { patientId, valor, formaPagamento } = {}) {
|
|
if (!agendaEventoId && !patientId) {
|
|
throw new Error('Informe agendaEventoId ou patientId.');
|
|
}
|
|
|
|
// Resolve patient_id pela sessão se não veio
|
|
let resolvedPatientId = patientId;
|
|
if (!resolvedPatientId && agendaEventoId) {
|
|
const { data, error } = await supabase
|
|
.from('agenda_eventos')
|
|
.select('paciente_id')
|
|
.eq('id', agendaEventoId)
|
|
.single();
|
|
if (error) throw error;
|
|
resolvedPatientId = data?.paciente_id;
|
|
if (!resolvedPatientId) throw new Error('Sessão sem paciente vinculado.');
|
|
}
|
|
|
|
// Busca template global recibo_pagamento
|
|
const { data: tpl, error: tplErr } = await supabase
|
|
.from('document_templates')
|
|
.select('*')
|
|
.eq('tipo', 'recibo_pagamento')
|
|
.eq('is_global', true)
|
|
.eq('ativo', true)
|
|
.limit(1)
|
|
.single();
|
|
if (tplErr) throw tplErr;
|
|
if (!tpl) throw new Error('Template de recibo não encontrado.');
|
|
|
|
// Variáveis (extras sobrescrevem session.valor / forma_pagamento)
|
|
const extras = {};
|
|
if (valor != null) extras.valor = valor;
|
|
if (formaPagamento) extras.forma_pagamento = formaPagamento;
|
|
|
|
const variables = await loadAllVariables(resolvedPatientId, agendaEventoId, extras);
|
|
|
|
// Gera PDF
|
|
const blob = await generatePdfBlob(tpl, variables);
|
|
|
|
// Salva
|
|
const saved = await saveGeneratedDocument({
|
|
templateId: tpl.id,
|
|
patientId: resolvedPatientId,
|
|
dadosPreenchidos: variables,
|
|
pdfBlob: blob,
|
|
templateNome: tpl.nome_template,
|
|
templateTipo: tpl.tipo
|
|
});
|
|
|
|
// Download client-side
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
const dataStr = (variables.data_sessao || variables.data_atual || '').replace(/\//g, '-');
|
|
a.download = `recibo_${(variables.paciente_nome || 'paciente').replace(/\s+/g, '_')}_${dataStr || 'sessao'}.pdf`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
|
|
|
return { generated: saved, variables, blob };
|
|
}
|
|
|
|
/**
|
|
* 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 || [];
|
|
}
|