roadmap #13: relatorios export PDF + Excel + CSV
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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(() => {
|
||||
<span class="mr-page__count">{{ periodLabel }}</span>
|
||||
</div>
|
||||
<div class="mr-page__actions">
|
||||
<button
|
||||
class="mr-head-btn"
|
||||
v-tooltip.bottom="'Exportar PDF'"
|
||||
:disabled="exportingPdf || loading || total === 0"
|
||||
@click="exportPdf"
|
||||
>
|
||||
<i :class="exportingPdf ? 'pi pi-spin pi-spinner' : 'pi pi-file-pdf'" />
|
||||
</button>
|
||||
<button
|
||||
class="mr-head-btn"
|
||||
v-tooltip.bottom="'Exportar Excel (.xlsx)'"
|
||||
:disabled="exportingXlsx || loading || total === 0"
|
||||
@click="exportXlsx"
|
||||
>
|
||||
<i :class="exportingXlsx ? 'pi pi-spin pi-spinner' : 'pi pi-file-excel'" />
|
||||
</button>
|
||||
<button
|
||||
class="mr-head-btn"
|
||||
v-tooltip.bottom="'Exportar CSV'"
|
||||
:disabled="loading || total === 0"
|
||||
@click="exportCsv"
|
||||
>
|
||||
<i class="pi pi-table" />
|
||||
</button>
|
||||
<button
|
||||
class="mr-head-btn"
|
||||
v-tooltip.bottom="'Recarregar'"
|
||||
|
||||
Reference in New Issue
Block a user