first commit
This commit is contained in:
+726
@@ -0,0 +1,726 @@
|
||||
# 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 `<Document>`, `<Page>`, `<View>`, `<Text>` 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 (
|
||||
<Document>
|
||||
<Page size="A4" style={styles.page}>
|
||||
<Text style={styles.header}>Inventory Report</Text>
|
||||
<View style={styles.table}>
|
||||
{data.map((item) => (
|
||||
<View key={item.id} style={styles.row}>
|
||||
<Text style={styles.cell}>{item.sku}</Text>
|
||||
<Text style={styles.cell}>{item.name}</Text>
|
||||
<Text style={styles.cell}>{item.stock}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**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 (
|
||||
<button onClick={handleExport} disabled={loading}>
|
||||
{loading ? `Exporting... ${progress}%` : 'Export PDF'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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<Uint8Array | undefined> {
|
||||
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.
|
||||
Reference in New Issue
Block a user