From b1e8e010c0c2610a20e838b61455d6e7a0974066 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Thu, 21 May 2026 05:21:36 -0300 Subject: [PATCH] roadmap #13: relatorios export PDF + Excel + CSV MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROADMAP item #1.3 #13. exceljs e jspdf ja estavam no package.json mas as paginas de relatorio so renderizavam UI — zero export. src/services/reportExport.service.js (novo) com 3 funcoes: - exportSessionsToPDF: layout HTML→PDF via pdf.service.js (header com branding tenant, KPI grid, tabela A4 com striping) - exportSessionsToXLSX: ExcelJS workbook formatado (titulo + subtitle + KPIs inline + tabela com header escuro + alternating row + frozen header). Import dinamico — exceljs e pesado, so carrega no click. - exportSessionsToCSV: vanilla (sem deps) com BOM UTF-8 + separador ';' (Excel-friendly em pt-BR) 3 botoes em ambas paginas: - RelatoriosPage.vue (/therapist/relatorios): icones pi-file-pdf + pi-file-excel + pi-table no header (rounded), tooltip, disabled quando total=0 ou loading, toast de sucesso/erro - MelissaRelatorios.vue (Melissa secao): mesma logica, botoes nativos .mr-head-btn no padrao Melissa Filtro de status da tabela e respeitado no export (exporta o que o usuario esta vendo). KPIs incluidos no PDF e XLSX. §1.3 UX = 3/4 fechado: #10 (busca global) + #11 (recently viewed) + #13 (relatorios export). #12 (papel timbrado) bloqueado em codigo externo do UniaoApp. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/layout/melissa/MelissaRelatorios.vue | 87 ++++++ src/services/reportExport.service.js | 281 +++++++++++++++++++ src/views/pages/therapist/RelatoriosPage.vue | 97 ++++++- 3 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 src/services/reportExport.service.js diff --git a/src/layout/melissa/MelissaRelatorios.vue b/src/layout/melissa/MelissaRelatorios.vue index 13ec3c6..b137e3f 100644 --- a/src/layout/melissa/MelissaRelatorios.vue +++ b/src/layout/melissa/MelissaRelatorios.vue @@ -12,12 +12,15 @@ * isoWeek/isoMonth + Chart.js). */ import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'; +import { useToast } from 'primevue/usetoast'; import { supabase } from '@/lib/supabase/client'; import { useTenantStore } from '@/stores/tenantStore'; +import { exportSessionsToPDF, exportSessionsToXLSX, exportSessionsToCSV } from '@/services/reportExport.service'; // Chart/DataTable/Column/Tag/Skeleton: auto via PrimeVueResolver const emit = defineEmits(['close']); const tenantStore = useTenantStore(); +const toast = useToast(); // ── Breakpoints + drawer mobile ──────────────────────── const drawerOpen = ref(false); @@ -251,6 +254,66 @@ function patientName(s) { return s.patients?.nome_completo || '—'; } +// ── Export PDF / Excel / CSV ────────────────────────── +const exportingPdf = ref(false); +const exportingXlsx = ref(false); + +function buildExportParams() { + const period = PERIOD_OPTIONS.find(p => p.key === selectedPeriod.value)?.label || ''; + const normalized = sessionsFiltradas.value.map(s => ({ + ...s, + paciente_nome: s.patients?.nome_completo || '—' + })); + return { + title: 'Relatório de Sessões', + subtitle: period, + sessions: normalized, + kpis: [ + { label: 'Total', value: total.value }, + { label: 'Realizadas', value: realizadas.value }, + { label: 'Faltas', value: faltas.value }, + { label: 'Canceladas', value: canceladas.value } + ], + tenantName: tenantStore.activeTenantName || tenantStore.tenant?.name || '', + terapeutaNome: tenantStore.user?.full_name || tenantStore.user?.email || '' + }; +} + +async function exportPdf() { + if (exportingPdf.value) return; + exportingPdf.value = true; + try { + const file = await exportSessionsToPDF(buildExportParams()); + toast.add({ severity: 'success', summary: 'PDF gerado', detail: file, life: 2500 }); + } catch (e) { + toast.add({ severity: 'error', summary: 'Erro ao gerar PDF', detail: e?.message || '', life: 4500 }); + } finally { + exportingPdf.value = false; + } +} + +async function exportXlsx() { + if (exportingXlsx.value) return; + exportingXlsx.value = true; + try { + const file = await exportSessionsToXLSX(buildExportParams()); + toast.add({ severity: 'success', summary: 'Excel gerado', detail: file, life: 2500 }); + } catch (e) { + toast.add({ severity: 'error', summary: 'Erro ao gerar Excel', detail: e?.message || '', life: 4500 }); + } finally { + exportingXlsx.value = false; + } +} + +function exportCsv() { + try { + const file = exportSessionsToCSV(buildExportParams()); + toast.add({ severity: 'success', summary: 'CSV gerado', detail: file, life: 2500 }); + } catch (e) { + toast.add({ severity: 'error', summary: 'Erro ao gerar CSV', detail: e?.message || '', life: 4500 }); + } +} + watch(selectedPeriod, () => { statusFilter.value = null; loadSessions(); @@ -308,6 +371,30 @@ onBeforeUnmount(() => { {{ periodLabel }}
+ + +