From 6ae651a8aec21b165614349efbbab8286c7822c3 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Thu, 21 May 2026 05:05:17 -0300 Subject: [PATCH] =?UTF-8?q?roadmap=20#14:=20recibo=20profissional=20PDF=20?= =?UTF-8?q?=E2=80=94=20gerador=20+=20quick=20path=20da=20agenda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ...1000008_recibo_uses_terapeuta_registro.sql | 35 ++++ .../agenda/AgendaEventoFinanceiroPanel.vue | 37 ++++ .../composables/useDocumentGenerate.js | 3 +- src/services/DocumentGenerate.service.js | 178 ++++++++++++++++-- src/services/DocumentTemplates.service.js | 6 +- src/utils/valorExtenso.js | 98 ++++++++++ 6 files changed, 344 insertions(+), 13 deletions(-) create mode 100644 database-novo/migrations/20260521000008_recibo_uses_terapeuta_registro.sql create mode 100644 src/utils/valorExtenso.js diff --git a/database-novo/migrations/20260521000008_recibo_uses_terapeuta_registro.sql b/database-novo/migrations/20260521000008_recibo_uses_terapeuta_registro.sql new file mode 100644 index 0000000..7387e63 --- /dev/null +++ b/database-novo/migrations/20260521000008_recibo_uses_terapeuta_registro.sql @@ -0,0 +1,35 @@ +-- ============================================================================ +-- ROADMAP #1.4 #14 — Recibo profissional usa terapeuta_registro genérico +-- ---------------------------------------------------------------------------- +-- O template recibo_pagamento (seed_015) usa "Psicólogo(a) — CRP {{terapeuta_crp}}". +-- Como agora suportamos múltiplos conselhos (CRP/CRM/CRFa/CREFITO/CRESS/CRN/RMS) +-- via #5 (migration 20260521000003), o recibo precisa ser CFP-agnóstico. +-- +-- Esta migration substitui no recibo_pagamento: +-- "Psicólogo(a) — CRP {{terapeuta_crp}}" → "{{terapeuta_registro}}" +-- e atualiza variaveis[] removendo terapeuta_crp + adicionando terapeuta_registro. +-- +-- {{terapeuta_registro}} é auto-formatado server-side como "CRP 12345/SP", +-- "CRM 12345/SP" etc, então não precisa de "Psicólogo(a) —" hardcoded. +-- ============================================================================ + +BEGIN; + +UPDATE public.document_templates +SET corpo_html = REPLACE( + corpo_html, + 'Psicólogo(a) — CRP {{terapeuta_crp}}', + '{{terapeuta_registro}}' +), +variaveis = ARRAY( + SELECT DISTINCT v FROM ( + SELECT unnest(variaveis) v + UNION ALL + SELECT 'terapeuta_registro' + ) sub + WHERE v <> 'terapeuta_crp' +), +updated_at = now() +WHERE tipo = 'recibo_pagamento' AND is_global = true; + +COMMIT; diff --git a/src/components/agenda/AgendaEventoFinanceiroPanel.vue b/src/components/agenda/AgendaEventoFinanceiroPanel.vue index 189e557..9066465 100644 --- a/src/components/agenda/AgendaEventoFinanceiroPanel.vue +++ b/src/components/agenda/AgendaEventoFinanceiroPanel.vue @@ -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; + } +}