roadmap #14: recibo profissional PDF — gerador + quick path da agenda
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>
This commit is contained in:
@@ -36,6 +36,7 @@ import { useConfirm } from 'primevue/useconfirm';
|
||||
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useAgendaFinanceiro } from '@/composables/useAgendaFinanceiro';
|
||||
import { emitirReciboParaSessao } from '@/services/DocumentGenerate.service';
|
||||
|
||||
// ── props / emits ─────────────────────────────────────────────────────────────
|
||||
const props = defineProps({
|
||||
@@ -56,6 +57,7 @@ const { gerarCobrancaManual, loading: finLoading, error: finError } = useAgendaF
|
||||
const record = ref(null); // financial_record vinculado
|
||||
const fetching = ref(false);
|
||||
const generating = ref(false);
|
||||
const emittingRecibo = ref(false);
|
||||
|
||||
// ── opções de método de pagamento ─────────────────────────────────────────────
|
||||
const PAYMENT_METHODS = [
|
||||
@@ -224,6 +226,27 @@ function requestCancel() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Emitir recibo PDF da sessão ─────────────────────────────────────────────
|
||||
// Gera, salva (Storage + documents/document_generated) e baixa um recibo
|
||||
// pré-preenchido com paciente/sessão/valor/forma de pagamento + registro
|
||||
// profissional do terapeuta (CRP/CRM/CRFa etc — auto-formatado).
|
||||
async function onEmitirRecibo() {
|
||||
if (emittingRecibo.value) return;
|
||||
emittingRecibo.value = true;
|
||||
try {
|
||||
await emitirReciboParaSessao(props.evento.id, {
|
||||
patientId: props.evento.patient_id || props.evento.paciente_id,
|
||||
valor: record.value?.final_amount ?? record.value?.amount ?? props.evento.price,
|
||||
formaPagamento: paymentLabel(record.value?.payment_method)
|
||||
});
|
||||
toast.add({ severity: 'success', summary: 'Recibo emitido', detail: 'PDF baixado e salvo nos documentos do paciente.', life: 3000 });
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro ao emitir recibo', detail: e?.message || 'Tente novamente.', life: 4500 });
|
||||
} finally {
|
||||
emittingRecibo.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -292,6 +315,20 @@ function requestCancel() {
|
||||
<Button label="Receber" icon="pi pi-check" size="small" class="rounded-full flex-1" @click="openPayDialog" />
|
||||
<Button icon="pi pi-times" size="small" severity="danger" outlined class="rounded-full h-7 w-7" v-tooltip.top="'Cancelar cobrança'" @click="requestCancel" />
|
||||
</div>
|
||||
|
||||
<!-- Ação: pago — emitir recibo PDF -->
|
||||
<div v-else-if="record.status === 'paid'" class="flex gap-1.5 mt-3">
|
||||
<Button
|
||||
label="Emitir recibo"
|
||||
icon="pi pi-file-pdf"
|
||||
size="small"
|
||||
outlined
|
||||
class="rounded-full flex-1"
|
||||
:loading="emittingRecibo"
|
||||
v-tooltip.top="'Gera PDF e salva nos documentos do paciente'"
|
||||
@click="onEmitirRecibo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user