
Export di report in PDF e Excel con Next.js
Se chiedete a qualsiasi team che ha fatto ricerca utente con acquirenti aziendali quale sia la feature più richiesta nelle dashboard B2B, la risposta è quasi sempre la stessa: "esportare in Excel". Al secondo posto: "generare un PDF da presentare nella riunione".
Questi due formati dominano il flusso di lavoro aziendale per ragioni semplici: i fogli di calcolo sono il denominatore comune dell'analisi nelle aziende, e i PDF sono il formato standard per report formali, presentazioni e archiviazione. Una dashboard che non esporta costringe gli utenti a fare screenshot o copiare numeri manualmente — e lo faranno, ma si lamenteranno.
React-PDF: PDF con JSX (client-side e server-side)
React-PDF (@react-pdf/renderer) è una libreria che permette di descrivere PDF usando componenti JSX con un sottoinsieme di stili simile al CSS. Il risultato è un file PDF generato programmaticamente, con layout preciso e indipendente da qualsiasi browser o rendering HTML.
// components/reports/RevenuePDF.tsx
import {
Document, Page, Text, View, StyleSheet, Image
} from '@react-pdf/renderer';
const styles = StyleSheet.create({
page: {
padding: 40,
fontFamily: 'Helvetica',
backgroundColor: '#ffffff',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 24,
paddingBottom: 16,
borderBottom: '1px solid #e5e7eb',
},
title: { fontSize: 20, fontWeight: 'bold', color: '#111827' },
subtitle: { fontSize: 11, color: '#6b7280', marginTop: 4 },
kpiRow: { flexDirection: 'row', gap: 16, marginBottom: 24 },
kpiBox: {
flex: 1,
padding: 16,
backgroundColor: '#f9fafb',
borderRadius: 8,
},
kpiLabel: { fontSize: 10, color: '#6b7280', marginBottom: 4 },
kpiValue: { fontSize: 24, fontWeight: 'bold', color: '#111827' },
table: { marginTop: 16 },
tableRow: { flexDirection: 'row', borderBottom: '1px solid #f3f4f6', padding: '8px 0' },
tableHeader: { fontSize: 10, color: '#6b7280', fontWeight: 'bold' },
tableCell: { fontSize: 10, color: '#374151', flex: 1 },
});
interface RevenueReportData {
period: string;
totalRevenue: number;
growth: number;
topCustomers: Array<{ name: string; revenue: number; orders: number }>;
}
export function RevenuePDF({ data }: { data: RevenueReportData }) {
return (
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.header}>
<View>
<Text style={styles.title}>Report Ricavi</Text>
<Text style={styles.subtitle}>Periodo: {data.period}</Text>
</View>
<Text style={{ fontSize: 10, color: '#9ca3af' }}>
Generato il {new Date().toLocaleDateString('it-IT')}
</Text>
</View>
<View style={styles.kpiRow}>
<View style={styles.kpiBox}>
<Text style={styles.kpiLabel}>Ricavi Totali</Text>
<Text style={styles.kpiValue}>
{new Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' })
.format(data.totalRevenue)}
</Text>
</View>
<View style={styles.kpiBox}>
<Text style={styles.kpiLabel}>Crescita</Text>
<Text style={{ ...styles.kpiValue, color: data.growth >= 0 ? '#16a34a' : '#dc2626' }}>
{data.growth > 0 ? '+' : ''}{data.growth.toFixed(1)}%
</Text>
</View>
</View>
<Text style={{ fontSize: 13, fontWeight: 'bold', marginBottom: 8 }}>Top Clienti</Text>
<View style={styles.table}>
<View style={styles.tableRow}>
<Text style={{ ...styles.tableHeader, flex: 2 }}>Cliente</Text>
<Text style={styles.tableHeader}>Ricavi</Text>
<Text style={styles.tableHeader}>Ordini</Text>
</View>
{data.topCustomers.map((c, i) => (
<View key={i} style={styles.tableRow}>
<Text style={{ ...styles.tableCell, flex: 2 }}>{c.name}</Text>
<Text style={styles.tableCell}>
{new Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' }).format(c.revenue)}
</Text>
<Text style={styles.tableCell}>{c.orders}</Text>
</View>
))}
</View>
</Page>
</Document>
);
}
// Download sul client:
import { pdf } from '@react-pdf/renderer';
async function downloadPDF(data: RevenueReportData) {
const blob = await pdf(<RevenuePDF data={data} />).toBlob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `report-ricavi-${data.period}.pdf`;
a.click();
URL.revokeObjectURL(url);
}
Vantaggi di React-PDF: layout pixel-perfect, nessuna dipendenza dal browser, funziona nelle Server Actions e API Routes, e il risultato è consistente in qualsiasi ambiente.
Limitazioni: non renderizza grafici automaticamente. Per includere grafici nei PDF con React-PDF, dovete esportare i grafici come immagini SVG o PNG e includerli come <Image>. Questo aggiunge complessità alla pipeline di generazione.
Puppeteer: screenshot della pagina come PDF
Puppeteer controlla un'istanza di Chrome headless e può generare PDF da qualsiasi pagina web. L'approccio è diverso: invece di descrivere il PDF programmaticamente, renderizzate la pagina della dashboard nel browser e la "stampate" come PDF.
// app/api/reports/pdf/route.ts
import puppeteer from 'puppeteer-core';
import chromium from '@sparticuz/chromium'; // versione ottimizzata per serverless
export async function POST(request: Request) {
const { reportUrl, filename } = await request.json();
const browser = await puppeteer.launch({
args: chromium.args,
executablePath: await chromium.executablePath(),
headless: true,
});
const page = await browser.newPage();
// Iniettare token di autenticazione per pagine protette
const sessionToken = request.headers.get('authorization');
await page.setExtraHTTPHeaders({ authorization: sessionToken ?? '' });
await page.goto(`${process.env.NEXT_PUBLIC_URL}${reportUrl}?print=true`, {
waitUntil: 'networkidle0', // attende il completamento di tutte le richieste dati
timeout: 30_000,
});
// Attendere che le animazioni dei grafici terminino
await page.waitForTimeout(2000);
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20px', bottom: '20px', left: '20px', right: '20px' },
});
await browser.close();
return new Response(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${filename}.pdf"`,
}
});
}
Vantaggi di Puppeteer: il PDF include grafici e tutta la visualizzazione della dashboard esattamente come appare sullo schermo, senza necessità di ricreare il layout in codice PDF. È più rapido da implementare quando il design esiste già.
Limitazioni: richiede Chrome headless sul server (~130MB), latenza maggiore di generazione (2-5s), costi infrastrutturali più alti in serverless, e il CSS necessita di adattamenti per la modalità stampa (@media print).
ExcelJS: fogli di calcolo con formattazione completa
Per gli export Excel, ExcelJS è la libreria più robusta disponibile in Node.js — supporta fogli multipli, formattazione celle, formule, grafici, immagini e protezione del foglio.
// lib/exports/excel.ts
import ExcelJS from 'exceljs';
interface ExcelReportOptions {
title: string;
data: Array<Record<string, string | number>>;
columns: Array<{ key: string; header: string; width?: number; format?: string }>;
}
export async function generateExcelReport(options: ExcelReportOptions): Promise<Buffer> {
const workbook = new ExcelJS.Workbook();
workbook.creator = 'Dashboard B2B';
workbook.created = new Date();
const sheet = workbook.addWorksheet(options.title, {
pageSetup: { paperSize: 9, orientation: 'landscape' }
});
// Intestazione visuale
sheet.mergeCells('A1', `${String.fromCharCode(64 + options.columns.length)}1`);
const titleCell = sheet.getCell('A1');
titleCell.value = options.title;
titleCell.font = { bold: true, size: 14, color: { argb: 'FF111827' } };
titleCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF9FAFB' } };
titleCell.alignment = { vertical: 'middle', horizontal: 'center' };
sheet.getRow(1).height = 32;
// Colonne
sheet.columns = options.columns.map(col => ({
key: col.key,
header: col.header,
width: col.width ?? 18,
}));
// Riga di intestazione formattata
const headerRow = sheet.getRow(2);
headerRow.values = ['', ...options.columns.map(c => c.header)];
headerRow.eachCell(cell => {
cell.font = { bold: true, color: { argb: 'FFFFFFFF' } };
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4F46E5' } };
cell.alignment = { vertical: 'middle' };
});
headerRow.height = 24;
// Dati con alternanza di colore
options.data.forEach((row, index) => {
const dataRow = sheet.addRow(row);
if (index % 2 === 0) {
dataRow.eachCell(cell => {
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF5F3FF' } };
});
}
// Formattazione celle numeriche
options.columns.forEach(col => {
if (col.format) {
const cell = dataRow.getCell(col.key);
cell.numFmt = col.format;
}
});
});
// Blocca intestazioni
sheet.views = [{ state: 'frozen', ySplit: 2 }];
// Filtro automatico
sheet.autoFilter = {
from: { row: 2, column: 1 },
to: { row: 2, column: options.columns.length }
};
return workbook.xlsx.writeBuffer() as Promise<Buffer>;
}
Quando generare lato client vs lato server
La decisione su dove generare l'export influenza performance, sicurezza e costi:
| Criterio | Client-side | Server-side |
|---|---|---|
| Dati sensibili nel payload | Rischio (transitano al client) | Sicuro (restano sul server) |
| Dataset grandi (> 50k righe) | Può bloccare il browser | Consigliato |
| Grafici nel PDF | Difficile | Facile con Puppeteer |
| Costo infrastruttura | Nessuno | CPU/memoria del server |
| Latenza percepita | Immediata (blocca la UI) | Asincrona possibile |
La regola pratica: React-PDF lato server per PDF di report formali con dati sensibili; ExcelJS lato server per fogli con più di 1.000 righe o dati finanziari; client-side solo per export semplici di tabelle visibili a schermo.
Conclusione
L'export dei report è la feature più sottovalutata nei roadmap delle dashboard. Sembra semplice — "basta generare un file" — ma l'implementazione di qualità coinvolge decisioni architetturali su dove elaborare, come gestire i grafici, come mantenere la coerenza visiva con la dashboard e come scalare quando molti utenti esportano contemporaneamente.
Implementare l'export come afterthought genera PDF scadenti, fogli senza formattazione e timeout nelle request lunghe. Implementarlo come parte del design del sistema fin dall'inizio produce export che gli utenti portano con orgoglio alle riunioni di direzione.
In SystemForge, l'esportazione è specificata come requisito nel PRD con formato, contenuto e frequenza stimata di utilizzo — non aggiunta come feature dell'ultimo minuto prima del lancio. Contattaci per discutere il tuo progetto.
Hai bisogno di una Dashboard B2B?
Costruiamo dashboard analitiche e pannelli di gestione su misura.
Scopri di più →Hai bisogno di aiuto?