# Exploration: PDF Reports Module ## Executive Summary Design client-side PDF generation for 18 report types across inventory, products, movements, purchases, customers, sales, profits, and analytics. All PDFs generated in browser using pdf-lib (~150KB), dynamically imported on demand. Reusable template pattern for headers, footers, and tables ensures consistency and reduces code duplication. ## Current State ### Existing Architecture - **Framework**: Next.js 16.2.11 App Router (TypeScript strict) - **Database**: SQLite via Drizzle ORM (better-sqlite3) - **Services**: inventory, sales, purchases, customers, suppliers modules with service functions - **UI**: shadcn/ui components, Tailwind CSS, ARS currency (es-AR locale) - **Admin Shell**: Sidebar navigation, header, mobile drawer - **Reports Page**: Placeholder at `/admin/reports` (coming soon) ### Existing Data Sources ```typescript // Inventory - computeTrafficLight() — stock status (green/yellow/red) - getMovements() — inventory movement history - getRestockSuggestions() — low stock variants // Sales - getSales(filters) — list sales with pagination - getSaleById(id) — single sale with items // Purchases - getPurchases(filters) — list purchases - getPurchaseById(id) — single purchase with items - allocateAdditionalCosts() — cost allocation logic // Customers - Customer service with CRUD operations // Suppliers - Supplier service with CRUD operations ``` ### Missing Components - No PDF libraries installed (pdf-lib, jsPDF, html2canvas) - No report generation code - No export functionality - No print-optimized CSS --- ## Required Reports (18 total) ### Inventory Reports (4) 1. **Inventory General** — All variants with current stock, min stock, location 2. **Inventory Critical** — Variants below minimum stock (red traffic light) 3. **Products to Restock** — Variants needing reorder (yellow + red traffic light) 4. **Inventory Valuation** — Stock value by variant (quantity × cost) ### Product Reports (2) 5. **Products by Category** — Grouped by category with counts and totals 6. **Products by Supplier** — Grouped by preferred supplier ### Movement Reports (1) 7. **Movement History** — All inventory movements with filters (date range, type, variant) ### Purchase Reports (2) 8. **Purchases** — List of purchases with totals, status, supplier 9. **Purchase Detail** — Single purchase with items, costs, allocation ### Customer Reports (2) 10. **Customers** — List of customers with contact info, type, total purchases 11. **Customer Detail** — Single customer with purchase history ### Sales Reports (2) 12. **Sales** — List of sales with totals, status, payment method 13. **Sale Detail** — Single sale with items, discounts, taxes ### Profit Reports (2) 14. **Profits** — Gross profit by period (sale price - cost) 15. **Cost Evolution** — Average cost trends over time ### Analytics Reports (3) 16. **Seasonality Analysis** — Sales by month/quarter, identify patterns 17. **Best Sellers** — Top variants by quantity sold 18. **Low Rotation** — Variants with low sales velocity --- ## Technical Requirements ### Client-Side Generation - **No server Puppeteer** — all PDFs generated in browser - **Libraries**: pdf-lib (~150KB) — already chosen in foundation - **Dynamic Import**: Load pdf-lib only when user clicks export - **Bundle Impact**: ~150KB added to client bundle (lazy-loaded chunk) ### PDF Structure (per report) ``` ┌─────────────────────────────────────┐ │ [Logo] Report Title │ ← Header (page 1) │ Generated: 2026-07-23 14:30 │ │ Period: 2026-01-01 to 2026-06-30 │ │ Filters: Category=Shirts, Status=… │ ├─────────────────────────────────────┤ │ Data Table │ │ ┌──────┬────────┬───────┬────────┐ │ │ │ SKU │ Name │ Stock │ Value │ │ │ ├──────┼────────┼───────┼────────┤ │ │ │ ... │ ... │ ... │ ... │ │ │ └──────┴────────┴───────┴────────┘ │ │ │ │ [Continues on next page if needed] │ ├─────────────────────────────────────┤ │ Totals: $1,234,567 │ ← Footer │ Page 1 of 5 │ │ byrachel — www.byrachel.com │ └─────────────────────────────────────┘ ``` ### Format & Layout - **Paper**: A4 (210mm × 297mm) - **Orientation**: Portrait (default) or Landscape (wide tables) - **Margins**: 15mm all sides - **Fonts**: Helvetica (built-in pdf-lib font) - **Colors**: Black text, gray borders, accent color for headers ### Pagination - Calculate rows per page based on available height - Split table rows across pages - Repeat header row on each page - Page numbers in footer (Page X of Y) ### Progress Indicator - Show loading state during data fetching (server-side) - Show progress during PDF generation (client-side) - Use React state + spinner component ### Alternative: @media print CSS - Provide print-optimized CSS for browser's native print - Fallback for users who prefer Ctrl+P - Hide navigation, show only report content --- ## Approaches ### Approach 1: pdf-lib (Programmatic PDF Generation) ✅ RECOMMENDED **Description**: Build PDFs from scratch using pdf-lib's low-level API. Manually position text, draw tables, embed images. Create reusable template functions for headers, footers, and tables. **Implementation**: ```typescript // src/lib/reports/templates/header.ts import { PDFDocument, PDFFont, rgb } from 'pdf-lib'; export function drawHeader( page: PDFPage, font: PDFFont, options: { logo?: Uint8Array; title: string; subtitle?: string; generatedAt: Date; } ) { const { width, height } = page.getSize(); // Draw logo (if provided) if (options.logo) { const logoImage = await pdfDoc.embedPng(options.logo); page.drawImage(logoImage, { x: 15, y: height - 40, width: 30, height: 30 }); } // Draw title page.drawText(options.title, { x: options.logo ? 50 : 15, y: height - 30, size: 18, font, color: rgb(0, 0, 0), }); // Draw generation date page.drawText(`Generated: ${formatDate(options.generatedAt)}`, { x: 15, y: height - 50, size: 10, font, color: rgb(0.4, 0.4, 0.4), }); } ``` **Pros**: - ✅ Smallest bundle (~150KB vs 500KB+ for alternatives) - ✅ Modern, TypeScript-first API - ✅ Full control over layout, pagination, positioning - ✅ Fast (no DOM rendering overhead) - ✅ Already chosen in foundation architecture - ✅ Sufficient for all 18 report types **Cons**: - ❌ Manual layout (no CSS/HTML) - ❌ More code to write (table drawing, pagination logic) - ❌ Steeper learning curve for team unfamiliar with pdf-lib **Effort**: Medium-High (40-60 hours) --- ### Approach 2: jsPDF + html2canvas (HTML-to-PDF) **Description**: Render React components to HTML, capture with html2canvas, convert to PDF with jsPDF. Easier styling with CSS but larger bundle and slower performance. **Implementation**: ```typescript // Client component import jsPDF from 'jspdf'; import html2canvas from 'html2canvas'; async function exportToPDF(elementId: string) { const element = document.getElementById(elementId); const canvas = await html2canvas(element); const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF('p', 'mm', 'a4'); const imgWidth = 210; // A4 width in mm const imgHeight = (canvas.height * imgWidth) / canvas.width; pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight); pdf.save('report.pdf'); } ``` **Pros**: - ✅ Easier styling (use existing CSS/Tailwind) - ✅ Less code (no manual table drawing) - ✅ Familiar workflow (HTML → PDF) **Cons**: - ❌ Large bundle (~500KB+ combined) - ❌ Slow (DOM rendering + canvas conversion) - ❌ Inconsistent rendering (browser differences) - ❌ Pagination issues (hard to split tables across pages) - ❌ Not suitable for large reports (memory issues) **Effort**: Low-Medium (20-30 hours) --- ### Approach 3: @react-pdf/renderer (React PDF Components) **Description**: Use React components to define PDF layout (similar to React Native). Declarative API with ``, ``, ``, `` components. **Implementation**: ```typescript import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer'; const styles = StyleSheet.create({ page: { padding: 15 }, header: { fontSize: 18, marginBottom: 10 }, table: { display: 'flex', flexDirection: 'column' }, row: { flexDirection: 'row', borderBottom: 1 }, cell: { flex: 1, padding: 5 }, }); function InventoryReport({ data }) { return ( Inventory Report {data.map((item) => ( {item.sku} {item.name} {item.stock} ))} ); } ``` **Pros**: - ✅ React components (familiar API) - ✅ Declarative layout - ✅ Built-in pagination - ✅ Good for complex layouts **Cons**: - ❌ Adds ~200KB to bundle - ❌ Different rendering engine (not browser DOM) - ❌ Learning curve (different from web React) - ❌ Limited CSS support (subset of Flexbox) **Effort**: Medium (30-40 hours) --- ### Approach 4: Hybrid (pdf-lib + @media print CSS) **Description**: Use pdf-lib for complex reports (analytics, multi-page tables), provide @media print CSS for simpler reports (single-page lists). Fallback option for users who prefer browser print. **Pros**: - ✅ Flexibility (choose best tool per report) - ✅ Fallback option (Ctrl+P always works) - ✅ Progressive enhancement **Cons**: - ❌ Two code paths to maintain - ❌ Inconsistent user experience - ❌ More complex implementation **Effort**: High (50-70 hours) --- ## Recommendation: **Approach 1 — pdf-lib** ### Rationale 1. **Already Decided**: Foundation architecture chose pdf-lib for small bundle size and modern API. 2. **Bundle Size**: ~150KB vs 500KB+ for jsPDF+html2canvas. Critical for Oracle VPS with limited RAM. 3. **Performance**: No DOM rendering overhead. Faster than html2canvas for large reports. 4. **Control**: Full control over pagination, headers, footers, table layout. Essential for professional reports. 5. **Sufficiency**: All 18 report types can be built with pdf-lib. No need for HTML-to-PDF complexity. 6. **TypeScript**: Modern, type-safe API. Better DX than jsPDF. ### Tradeoffs Accepted - **Manual Layout**: More code to write, but reusable templates reduce duplication. - **Learning Curve**: Team needs to learn pdf-lib API, but documentation is good. - **No CSS**: Can't reuse existing Tailwind styles, but consistent branding via templates. --- ## Architecture ### File Structure ``` src/lib/reports/ ├── templates/ │ ├── header.ts # Reusable header (logo, title, date, filters) │ ├── footer.ts # Reusable footer (page numbers, generation info, branding) │ ├── table.ts # Reusable table component (columns, rows, pagination) │ ├── page.ts # Page management (new page, margins, orientation) │ └── types.ts # Shared types (ReportOptions, TableColumn, etc.) │ ├── generators/ │ ├── inventory.ts # Inventory reports (general, critical, restock, valuation) │ ├── products.ts # Product reports (by category, by supplier) │ ├── movements.ts # Movement history report │ ├── purchases.ts # Purchase reports (list, detail) │ ├── customers.ts # Customer reports (list, detail) │ ├── sales.ts # Sales reports (list, detail) │ ├── profits.ts # Profit reports (profits, cost evolution) │ └── analytics.ts # Analytics reports (seasonality, best sellers, low rotation) │ ├── utils.ts # PDF utilities (formatting, calculations, helpers) └── index.ts # Public API (export all generators) src/components/reports/ ├── export-button.tsx # Reusable export button with progress indicator ├── report-filters.tsx # Filter form component (date range, categories, etc.) ├── report-header.tsx # Client-side report header (for @media print alternative) └── print-styles.css # @media print styles (hide nav, show report) src/app/(admin)/reports/ ├── page.tsx # Reports dashboard (list all 18 reports) ├── inventory/ │ ├── general/page.tsx │ ├── critical/page.tsx │ ├── restock/page.tsx │ └── valuation/page.tsx ├── products/ │ ├── by-category/page.tsx │ └── by-supplier/page.tsx ├── movements/page.tsx ├── purchases/ │ ├── page.tsx │ └── [id]/page.tsx ├── customers/ │ ├── page.tsx │ └── [id]/page.tsx ├── sales/ │ ├── page.tsx │ └── [id]/page.tsx ├── profits/page.tsx ├── cost-evolution/page.tsx └── analytics/ ├── seasonality/page.tsx ├── best-sellers/page.tsx └── low-rotation/page.tsx ``` ### Template Pattern **Header Template** (`src/lib/reports/templates/header.ts`): ```typescript export interface HeaderOptions { title: string; subtitle?: string; generatedAt: Date; period?: { from: Date; to: Date }; filters?: Array<{ label: string; value: string }>; logo?: Uint8Array; // PNG bytes } export function drawHeader( pdfDoc: PDFDocument, page: PDFPage, font: PDFFont, options: HeaderOptions ): number { // Returns Y position after header (for content placement) } ``` **Footer Template** (`src/lib/reports/templates/footer.ts`): ```typescript export interface FooterOptions { pageNumber: number; totalPages: number; totals?: Array<{ label: string; value: string }>; branding?: string; // "byrachel — www.byrachel.com" } export function drawFooter( page: PDFPage, font: PDFFont, options: FooterOptions ) { // Draw page numbers, totals, branding } ``` **Table Template** (`src/lib/reports/templates/table.ts`): ```typescript export interface TableColumn { header: string; key: string; width: number; // percentage (0-1) align?: 'left' | 'center' | 'right'; format?: (value: any) => string; } export interface TableOptions { columns: TableColumn[]; rows: any[]; startY: number; fontSize?: number; rowHeight?: number; headerColor?: RGB; alternateRowColor?: RGB; } export function drawTable( pdfDoc: PDFDocument, page: PDFPage, font: PDFFont, options: TableOptions ): { endY: number; pagesUsed: number } { // Handles pagination, row breaks, header repetition } ``` ### Data Fetching Strategy **Server-Side (API Routes / Server Actions)**: ```typescript // src/app/api/reports/inventory/general/route.ts import { db } from '@/lib/db'; import { variants, products, inventory, categories } from '@/lib/db/schema'; import { eq, and, sql } from 'drizzle-orm'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const categoryId = searchParams.get('categoryId'); const location = searchParams.get('location'); // Fetch data with filters const data = await db .select({ sku: variants.sku, productName: products.name, size: variants.size, color: variants.color, stock: inventory.quantity, minStock: inventory.minStock, location: inventory.location, cost: variants.averageCost, value: sql`${inventory.quantity} * ${variants.averageCost}`, }) .from(variants) .innerJoin(products, eq(variants.productId, products.id)) .innerJoin(inventory, eq(variants.id, inventory.variantId)) .leftJoin(categories, eq(products.categoryId, categories.id)) .where( and( categoryId ? eq(products.categoryId, Number(categoryId)) : undefined, location ? eq(inventory.location, location) : undefined, eq(products.isActive, true), eq(variants.isActive, true) ) ); return Response.json(data); } ``` **Client-Side (Export Button)**: ```typescript // src/components/reports/export-button.tsx 'use client'; import { useState } from 'react'; export function ExportButton({ reportType, filters }: { reportType: string; filters: any }) { const [loading, setLoading] = useState(false); const [progress, setProgress] = useState(0); async function handleExport() { setLoading(true); setProgress(10); try { // Fetch data from API const response = await fetch(`/api/reports/${reportType}?${new URLSearchParams(filters)}`); const data = await response.json(); setProgress(50); // Dynamically import pdf-lib const { PDFDocument } = await import('pdf-lib'); setProgress(60); // Generate PDF const pdfDoc = await PDFDocument.create(); const font = await pdfDoc.embedFont('Helvetica'); // Call report generator const generator = await import(`@/lib/reports/generators/${reportType}`); await generator.default(pdfDoc, font, data); setProgress(90); // Download PDF const pdfBytes = await pdfDoc.save(); const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${reportType}-${Date.now()}.pdf`; a.click(); setProgress(100); } finally { setLoading(false); setTimeout(() => setProgress(0), 1000); } } return ( ); } ``` ### PDF Generation Flow ``` User clicks "Export PDF" ↓ Client calls API route with filters ↓ Server fetches data from DB (with joins, aggregations) ↓ Server returns JSON data ↓ Client dynamically imports pdf-lib (~150KB) ↓ Client creates PDFDocument ↓ Client calls report generator (e.g., generateInventoryReport) ↓ Generator uses templates (header, footer, table) ↓ Generator handles pagination (split rows across pages) ↓ Client downloads PDF blob ``` ### Pagination Logic ```typescript // src/lib/reports/templates/table.ts const A4_HEIGHT = 842; // points (297mm) const MARGIN = 42.5; // 15mm const FOOTER_HEIGHT = 50; function calculateRowsPerPage(pageHeight: number, rowHeight: number, headerHeight: number): number { const availableHeight = pageHeight - MARGIN * 2 - FOOTER_HEIGHT - headerHeight; return Math.floor(availableHeight / rowHeight); } function paginateRows(rows: any[], rowsPerPage: number): any[][] { const pages: any[][] = []; for (let i = 0; i < rows.length; i += rowsPerPage) { pages.push(rows.slice(i, i + rowsPerPage)); } return pages; } ``` ### Logo Handling ```typescript // src/lib/reports/utils.ts export async function loadLogo(pdfDoc: PDFDocument): Promise { try { const response = await fetch('/images/logo.png'); if (!response.ok) return undefined; const arrayBuffer = await response.arrayBuffer(); return new Uint8Array(arrayBuffer); } catch { return undefined; // Fallback to text-only header } } ``` --- ## Affected Areas ### New Files - `src/lib/reports/` — 15+ files (templates, generators, utils) - `src/components/reports/` — 4 files (export button, filters, print styles) - `src/app/(admin)/reports/` — 20+ files (report pages) - `src/app/api/reports/` — 18+ API routes (one per report type) ### Modified Files - `package.json` — Add `pdf-lib` dependency - `src/app/(admin)/reports/page.tsx` — Replace placeholder with dashboard - `src/app/globals.css` — Add @media print styles (optional) ### Dependencies - **pdf-lib** (~150KB) — PDF generation library - **No other dependencies** — use existing Drizzle, React, Tailwind ### Performance Impact - **Bundle Size**: +150KB (lazy-loaded chunk, only loaded when user clicks export) - **Memory**: Minimal (PDF generation is short-lived, garbage collected after download) - **CPU**: Low (pdf-lib is fast, no DOM rendering) --- ## Risks ### 1. Manual Layout Complexity **Risk**: Writing table drawing, pagination, and positioning code is time-consuming and error-prone. **Mitigation**: Create reusable templates (header, footer, table) to reduce duplication. Start with simple reports, iterate to complex ones. **Severity**: Medium ### 2. Large Report Performance **Risk**: Reports with thousands of rows (e.g., movement history) may be slow to generate or cause memory issues. **Mitigation**: - Limit default date range (e.g., last 90 days) - Paginate data (fetch in chunks) - Show progress indicator - Test with large datasets early **Severity**: Medium ### 3. Font Limitations **Risk**: pdf-lib's built-in fonts (Helvetica, Times, Courier) may not support special characters (ñ, á, é, í, ó, ú). **Mitigation**: - Use UTF-8 encoding (pdf-lib supports it) - Test with Spanish text early - If issues, embed custom font (adds ~50KB) **Severity**: Low ### 4. Logo Embedding **Risk**: Logo may not be available (user hasn't uploaded it) or may be wrong format (JPEG instead of PNG). **Mitigation**: - Fallback to text-only header if logo not found - Support both PNG and JPEG (pdf-lib has `embedPng` and `embedJpg`) - Provide default logo in `public/images/logo.png` **Severity**: Low ### 5. Browser Compatibility **Risk**: Dynamic import of pdf-lib may fail in older browsers. **Mitigation**: - pdf-lib supports all modern browsers (Chrome, Firefox, Safari, Edge) - Provide fallback message for unsupported browsers - Test on target browsers **Severity**: Low ### 6. Data Aggregation Complexity **Risk**: Some reports (profits, analytics) require complex aggregations (JOINs, GROUP BY, calculations). **Mitigation**: - Create dedicated service functions for complex queries - Test queries with realistic data volumes - Use SQL aggregations (faster than in-memory) **Severity**: Medium ### 7. Print CSS Maintenance **Risk**: Maintaining two code paths (pdf-lib + @media print) increases complexity. **Mitigation**: - Make @media print optional (not required for MVP) - Focus on pdf-lib first, add print CSS later if needed **Severity**: Low --- ## Ready for Proposal **Status**: ✅ Ready **Next Steps**: 1. Create proposal document with scope, approach, and acceptance criteria 2. Define report specifications (columns, filters, sorting for each of 18 reports) 3. Implement template system (header, footer, table) 4. Implement 2-3 sample reports (inventory general, sales list, sale detail) 5. Test with realistic data volumes 6. Iterate on remaining reports **Recommendation**: Proceed to proposal phase with pdf-lib, reusable templates, and server-side data fetching. Start with inventory and sales reports as proof of concept.