This commit is contained in:
seb
2026-10-06 13:36:03 +02:00
parent cab06d96e8
commit 07f0d5ba80
9 changed files with 1710 additions and 21 deletions

View File

@@ -0,0 +1,206 @@
ExcelJS Spreadsheet Guide: Reading, Writing, Formatting, and VFS Integration
# Goal
Provide actionable instructions, code patterns, and best practices for creating, reading, modifying, and styling Excel (`.xlsx`) and CSV spreadsheets in the `javascript_execute` sandbox using `ExcelJS`.
# Environment & Globals
- Both `ExcelJS` and `exceljs` are pre-injected in global scope. Do NOT use `require()` or `import`.
- `Workbook`, `stream.xlsx.WorkbookWriter`, and `stream.xlsx.WorkbookReader` are pre-bridged to the Virtual Filesystem (VFS).
- Wrap async scripts in an IIFE: `module.exports = (async () => { ... })();`.
---
# 1. Creating and Populating a Workbook
```javascript
module.exports = (async () => {
const wb = new ExcelJS.Workbook();
wb.creator = 'QuixPOS Assistant';
wb.created = new Date();
const ws = wb.addWorksheet('Articles', {
properties: { tabColor: { argb: 'FF1F4E79' } },
views: [{ state: 'frozen', xSplit: 0, ySplit: 1 }] // freeze header row
});
// Define column schemas
ws.columns = [
{ header: 'SKU', key: 'sku', width: 14 },
{ header: 'Title', key: 'title', width: 36 },
{ header: 'Price (Gross)', key: 'price', width: 14, style: { numFmt: '#,##0.00 €' } },
{ header: 'Stock', key: 'stock', width: 10, style: { numFmt: '#,##0' } },
{ header: 'Supplier SKU', key: 'supplierSku', width: 16 }
];
// Add rows via object matching column keys
ws.addRow({ sku: 'ART-1001', title: 'Espresso Arabica Premium', price: 14.99, stock: 45, supplierSku: 'SUP-01' });
ws.addRow({ sku: 'ART-1002', title: 'Earl Grey Organic Tea', price: 8.50, stock: 20, supplierSku: 'SUP-02' });
// Or add multiple rows at once
ws.addRows([
{ sku: 'ART-1003', title: 'Single Origin Colombia', price: 16.20, stock: 12, supplierSku: 'SUP-03' },
{ sku: 'ART-1004', title: 'Matcha Ceremonial Grade', price: 24.00, stock: 8, supplierSku: 'SUP-04' }
]);
// Save directly to VFS
await wb.xlsx.writeFile('articles-export.xlsx');
return { success: true, rows: ws.rowCount, file: 'articles-export.xlsx' };
})();
```
---
# 2. Header & Cell Styling
```javascript
// Header styling: bold white text with dark blue background
const headerRow = ws.getRow(1);
headerRow.height = 24;
headerRow.font = { name: 'Arial', size: 11, bold: true, color: { argb: 'FFFFFFFF' } };
headerRow.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF1F4E79' }
};
headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
// Zebra striping data rows
for (let i = 2; i <= ws.rowCount; i++) {
const row = ws.getRow(i);
row.height = 20;
row.alignment = { vertical: 'middle' };
if (i % 2 === 0) {
row.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFF9FAFB' }
};
}
// Thin borders
row.eachCell({ includeEmpty: true }, (cell) => {
cell.border = {
bottom: { style: 'thin', color: { argb: 'FFE5E7EB' } }
};
});
}
```
---
# 3. Formulas & Summary Rows
```javascript
// Add a total row at the end
const totalRow = ws.addRow({
title: 'Total Stock & Value',
price: { formula: 'SUMPRODUCT(C2:C5, D2:D5)', result: 0 },
stock: { formula: 'SUM(D2:D5)', result: 85 }
});
totalRow.font = { bold: true };
totalRow.getCell('title').alignment = { horizontal: 'right' };
totalRow.border = {
top: { style: 'thin', color: { argb: 'FF000000' } },
bottom: { style: 'double', color: { argb: 'FF000000' } }
};
```
---
# 4. Reading & Modifying Existing Spreadsheets from VFS
```javascript
module.exports = (async () => {
const wb = new ExcelJS.Workbook();
// Path is relative to VFS (e.g. user attachment or previous artifact)
await wb.xlsx.readFile('attachments/inventory.xlsx');
const ws = wb.getWorksheet(1); // 1-based index or name 'Sheet1'
const records = [];
ws.eachRow({ includeEmpty: false }, (row, rowNumber) => {
if (rowNumber === 1) return; // skip header
records.push({
sku: row.getCell(1).text,
name: row.getCell(2).text,
price: Number(row.getCell(3).value) || 0,
stock: Number(row.getCell(4).value) || 0
});
});
// You can modify cells and write back
ws.getCell('C2').value = 19.99;
await wb.xlsx.writeFile('attachments/inventory-updated.xlsx');
return { totalRead: records.length, records };
})();
```
---
# 5. Working with CSV Files
```javascript
// Read CSV from VFS
const csvWb = new ExcelJS.Workbook();
await csvWb.csv.readFile('data.csv');
const sheet = csvWb.getWorksheet(1);
// Write CSV to VFS
await csvWb.csv.writeFile('export.csv');
```
---
# 6. Streaming Massive Datasets
For large tables (>5,000 rows) use `WorkbookWriter` to prevent high memory consumption:
```javascript
module.exports = (async () => {
const writer = new ExcelJS.stream.xlsx.WorkbookWriter({ filename: 'large-catalog.xlsx' });
const ws = writer.addWorksheet('Catalog');
ws.columns = [
{ header: 'ID', key: 'id', width: 10 },
{ header: 'Name', key: 'name', width: 30 },
{ header: 'Stock', key: 'stock', width: 12 }
];
for (let i = 1; i <= 10000; i++) {
ws.addRow({ id: i, name: `Product ${i}`, stock: Math.floor(Math.random() * 100) }).commit();
}
ws.commit();
await writer.commit();
return { exportedRows: 10000, file: 'large-catalog.xlsx' };
})();
```
---
# 7. Embedding Product / Company Images
```javascript
// Image filename is automatically resolved from VFS
const logoId = wb.addImage({
filename: 'images/logo.png',
extension: 'png'
});
// Position image by cell coordinates
ws.addImage(logoId, {
tl: { col: 5, row: 1 },
ext: { width: 120, height: 40 }
});
```
---
# 8. Presenting Output to User
When you generate or modify an Excel file in `javascript_execute`, **always include a clickable Markdown link in your response summary**:
- `[Download Articles Spreadsheet](articles-export.xlsx)`
- `[Download Updated Inventory](attachments/inventory-updated.xlsx)`

View File

@@ -0,0 +1,274 @@
PDFKit PDF Generation Guide: Documents, Styling, Layout, and VFS Integration
# Goal
Provide actionable instructions, code patterns, and best practices for creating beautiful, production-ready PDF documents (invoices, reports, labels, catalogs) in the `javascript_execute` sandbox using `PDFKit`.
# Environment & Globals
- `PDFDocument`, `PDFKit`, and `pdfkit` are pre-injected in global scope. Do NOT use `require()` or `import`.
- Instances of `PDFDocument` provide built-in VFS file writing: `await doc.toFile('path/to/file.pdf')` or `const buf = await doc.toBuffer()`.
- Standard stream piping also works: `doc.pipe(fs.createWriteStream('path.pdf'))`.
- Wrap async scripts in an IIFE: `module.exports = (async () => { ... })();`.
---
# 1. Quick Start: Simple Document
```javascript
module.exports = (async () => {
const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.fontSize(22)
.fillColor('#1F4E79')
.text('QuixPOS Monthly Summary', { align: 'center' });
doc.moveDown();
doc.fontSize(12)
.fillColor('#333333')
.text('This report summarizes inventory levels and recent sales transactions.');
// Save directly to VFS
await doc.toFile('monthly-summary.pdf');
return { file: 'monthly-summary.pdf' };
})();
```
---
# 2. Typography, Colors & Standard Fonts
PDFKit comes with 14 standard built-in fonts that require no external font files:
- **Helvetica**: `'Helvetica'`, `'Helvetica-Bold'`, `'Helvetica-Oblique'`, `'Helvetica-BoldOblique'`
- **Times**: `'Times-Roman'`, `'Times-Bold'`, `'Times-Italic'`, `'Times-BoldItalic'`
- **Courier**: `'Courier'`, `'Courier-Bold'`, `'Courier-Oblique'`, `'Courier-BoldOblique'`
```javascript
// Header
doc.font('Helvetica-Bold')
.fontSize(20)
.fillColor('#1F4E79')
.text('INVOICE', 50, 50);
// Subheaders & meta
doc.font('Helvetica')
.fontSize(10)
.fillColor('#555555')
.text('Invoice #: INV-2026-001', 50, 80)
.text('Date: 2026-10-06', 50, 95)
.text('Due Date: 2026-11-05', 50, 110);
```
---
# 3. Vector Shapes, Lines, and Boxes
Coordinates in PDFKit use points (72 points = 1 inch). A4 dimensions: 595.28 x 841.89 points.
```javascript
// Horizontal rule / separator
doc.moveTo(50, 135)
.lineTo(545, 135)
.strokeColor('#E5E7EB')
.lineWidth(1)
.stroke();
// Filled info box with rounded corners
doc.roundedRect(50, 150, 495, 60, 4)
.fillColor('#F9FAFB')
.fill();
doc.font('Helvetica-Bold').fontSize(10).fillColor('#1F4E79')
.text('Billed To:', 65, 160);
doc.font('Helvetica').fontSize(9).fillColor('#333333')
.text('Acme Retail GmbH\nIndustriestrasse 12, 10115 Berlin\nVAT ID: DE123456789', 65, 175);
```
---
# 4. Building Clean Data Tables
```javascript
const tableTop = 230;
const colX = {
pos: 50,
sku: 80,
title: 170,
qty: 370,
price: 430,
total: 495
};
// Table Header Background
doc.rect(50, tableTop, 495, 20)
.fillColor('#1F4E79')
.fill();
// Table Header Text
doc.font('Helvetica-Bold').fontSize(9).fillColor('#FFFFFF');
doc.text('#', colX.pos + 5, tableTop + 5);
doc.text('SKU', colX.sku, tableTop + 5);
doc.text('Description', colX.title, tableTop + 5);
doc.text('Qty', colX.qty, tableTop + 5, { width: 50, align: 'right' });
doc.text('Price', colX.price, tableTop + 5, { width: 55, align: 'right' });
doc.text('Total', colX.total, tableTop + 5, { width: 50, align: 'right' });
// Table Rows
const items = [
{ sku: 'ART-101', title: 'Premium Espresso Roast 1kg', qty: 5, price: 18.50 },
{ sku: 'ART-102', title: 'Single Origin Filter Roast 250g', qty: 10, price: 7.20 },
{ sku: 'ART-103', title: 'Ceremonial Matcha Tin 100g', qty: 2, price: 24.00 }
];
let y = tableTop + 24;
items.forEach((item, index) => {
const lineTotal = item.qty * item.price;
// Zebra striping
if (index % 2 === 1) {
doc.rect(50, y - 4, 495, 20).fillColor('#F9FAFB').fill();
}
doc.font('Helvetica').fontSize(9).fillColor('#333333');
doc.text(String(index + 1), colX.pos + 5, y);
doc.text(item.sku, colX.sku, y);
doc.text(item.title, colX.title, y, { width: 190, ellipsis: true });
doc.text(String(item.qty), colX.qty, y, { width: 50, align: 'right' });
doc.text(`${item.price.toFixed(2)} €`, colX.price, y, { width: 55, align: 'right' });
doc.text(`${lineTotal.toFixed(2)} €`, colX.total, y, { width: 50, align: 'right' });
// Underline
doc.moveTo(50, y + 16).lineTo(545, y + 16).strokeColor('#F3F4F6').lineWidth(0.5).stroke();
y += 20;
});
// Summary block
y += 10;
doc.font('Helvetica-Bold').fontSize(10).fillColor('#1F4E79')
.text('Total Net:', 380, y, { width: 90, align: 'right' })
.text('212.50 €', 480, y, { width: 65, align: 'right' });
doc.font('Helvetica').fontSize(9).fillColor('#555555')
.text('VAT (19%):', 380, y + 15, { width: 90, align: 'right' })
.text('40.38 €', 480, y + 15, { width: 65, align: 'right' });
doc.font('Helvetica-Bold').fontSize(11).fillColor('#1F4E79')
.text('Grand Total:', 380, y + 32, { width: 90, align: 'right' })
.text('252.88 €', 480, y + 32, { width: 65, align: 'right' });
```
---
# 5. Embedding Images & Logos
`doc.image()` accepts VFS relative paths (e.g. `'images/logo.png'`), virtual database images, or raw Buffers:
```javascript
// Place company logo in top-right corner
try {
doc.image('images/logo.png', 425, 45, { width: 120 });
} catch {
// Graceful fallback if image does not exist
doc.font('Helvetica-Bold').fontSize(14).fillColor('#1F4E79').text('QuixPOS', 450, 50);
}
```
---
# 6. Multi-page Documents & Page Numbers
To add page numbers across all pages, use `{ bufferPages: true }`:
```javascript
const doc = new PDFDocument({ size: 'A4', margin: 50, bufferPages: true });
// Page 1 content...
doc.text('First Page Content');
// Add Page 2
doc.addPage();
doc.text('Second Page Content');
// Add footer with page numbering to every page before closing
const range = doc.bufferedPageRange();
for (let i = 0; i < range.count; i++) {
doc.switchToPage(i);
doc.font('Helvetica').fontSize(8).fillColor('#9CA3AF')
.text(
`Page ${i + 1} of ${range.count} • Generated by QuixPOS Assistant`,
50,
800,
{ align: 'center', width: 495 }
);
}
await doc.toFile('multipage-report.pdf');
```
---
# 7. Complete End-to-End Invoice Example
```javascript
module.exports = (async () => {
const doc = new PDFDocument({ size: 'A4', margin: 50, bufferPages: true });
// Header
doc.font('Helvetica-Bold').fontSize(22).fillColor('#1F4E79').text('INVOICE', 50, 50);
doc.font('Helvetica').fontSize(9).fillColor('#6B7280')
.text('Invoice #: INV-2026-089', 50, 80)
.text(`Date: ${new Date().toISOString().slice(0, 10)}`, 50, 95);
// Client box
doc.roundedRect(50, 125, 495, 55, 4).fillColor('#F9FAFB').fill();
doc.font('Helvetica-Bold').fontSize(9).fillColor('#1F4E79').text('Customer:', 65, 135);
doc.font('Helvetica').fontSize(9).fillColor('#374151')
.text('Sample Store GmbH • Friedrichstrasse 45, 10117 Berlin', 65, 150);
// Items table
let y = 205;
doc.rect(50, y, 495, 20).fillColor('#1F4E79').fill();
doc.font('Helvetica-Bold').fontSize(9).fillColor('#FFFFFF')
.text('Description', 60, y + 5)
.text('Qty', 350, y + 5, { width: 50, align: 'right' })
.text('Unit Price', 410, y + 5, { width: 60, align: 'right' })
.text('Total', 480, y + 5, { width: 55, align: 'right' });
y += 24;
const items = [
{ desc: 'Robusta Espresso Blend 1kg', qty: 4, price: 15.00 },
{ desc: 'Single Origin Ethiopia Yirgacheffe 500g', qty: 2, price: 12.50 }
];
let total = 0;
for (const item of items) {
const lineTotal = item.qty * item.price;
total += lineTotal;
doc.font('Helvetica').fontSize(9).fillColor('#1F2937')
.text(item.desc, 60, y)
.text(String(item.qty), 350, y, { width: 50, align: 'right' })
.text(`${item.price.toFixed(2)} €`, 410, y, { width: 60, align: 'right' })
.text(`${lineTotal.toFixed(2)} €`, 480, y, { width: 55, align: 'right' });
y += 20;
}
doc.moveTo(50, y + 5).lineTo(545, y + 5).strokeColor('#E5E7EB').stroke();
y += 15;
doc.font('Helvetica-Bold').fontSize(11).fillColor('#1F4E79')
.text('Total (Gross):', 380, y, { width: 90, align: 'right' })
.text(`${total.toFixed(2)} €`, 480, y, { width: 55, align: 'right' });
// Output to VFS
const filename = 'invoices/invoice-2026-089.pdf';
await doc.toFile(filename);
return { success: true, file: filename, total };
})();
```
---
# 8. Presenting Output to User
When you generate a PDF in `javascript_execute`, **always include a clickable Markdown link in your response summary**:
- `[Download Invoice PDF](invoices/invoice-2026-089.pdf)`
- `[Download Monthly Report](monthly-summary.pdf)`