| name | office-documents |
| description | ForemanOS Office document patterns. Use when generating DOCX with docxtemplater, XLSX with xlsx-populate, or processing document templates. |
Office Document Generation for ForemanOS
When to Use This Skill
- Creating DOCX documents from templates (daily reports, room sheets)
- Processing XLSX spreadsheets with template variables
- Building DOCX files from scratch with PizZip
- Working with the template processor pipeline
Core Patterns
Pattern 1: DOCX Template Processing with Docxtemplater
Fill DOCX templates with data using {{variable}} placeholders:
import Docxtemplater from 'docxtemplater';
import PizZip from 'pizzip';
export async function processDocxTemplate(
templateBuffer: Buffer,
data: TemplateData
): Promise<Buffer> {
const zip = new PizZip(templateBuffer);
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
nullGetter: () => '',
});
doc.setData(data);
doc.render();
const output = doc.getZip().generate({
type: 'nodebuffer',
compression: 'DEFLATE',
});
return output;
}
Pattern 2: XLSX Template Processing with xlsx-populate
Fill XLSX spreadsheets by replacing {{variable}} patterns in cells:
export async function processXlsxTemplate(
templateBuffer: Buffer,
data: TemplateData
): Promise<Buffer> {
const XlsxPopulate = require('xlsx-populate');
const workbook = await XlsxPopulate.fromDataAsync(templateBuffer);
workbook.sheets().forEach((sheet: any) => {
sheet.usedRange().forEach((cell: any) => {
const value = cell.value();
if (typeof value === 'string') {
let newValue = value;
Object.keys(data).forEach(key => {
const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
newValue = newValue.replace(regex, (data[key] || ));
});
(newValue !== value) {
cell.(newValue);
}
}
});
});
output = workbook.();
output;
}
Pattern 3: Building DOCX from Scratch with PizZip
For programmatic DOCX creation without a template (room sheets, exports):
import PizZip from 'pizzip';
export async function generateDocx(data: RoomSheetData): Promise<Blob> {
const zip = new PizZip();
zip.file('[Content_Types].xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml"
ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
<Override PartName="/word/styles.xml"
ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
</Types>`);
zip.file('_rels/.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
Target="word/document.xml"/>
</Relationships>`);
zip.file('word/_rels/document.xml.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
Target="styles.xml"/>
</Relationships>`);
zip.file('word/styles.xml', stylesXml);
zip.(, (data));
zip.({
: ,
: ,
});
}
Pattern 4: XML Table Generation for DOCX
Helper for creating Word tables in raw XML:
function escapeXml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function createTable(headers: string[], rows: string[][]): string {
const colCount = headers.length;
const colWidth = Math.floor(9000 / colCount);
let xml = '<w:tbl>';
xml += '<w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="5000" w:type="pct"/></w:tblPr>';
xml += '<w:tblGrid>';
for (let i = 0; i < colCount; i++) {
xml += `<w:gridCol w:w="${colWidth}"/>`;
}
xml += '</w:tblGrid>';
xml += '<w:tr>';
headers.forEach( => {
xml += ;
xml += ;
});
xml += ;
rows.( {
xml += ;
row.( {
xml += ;
});
xml += ;
});
xml += ;
xml;
}
Pattern 5: Template Processing Pipeline
The unified template processor handles DOCX, XLSX, and PDF:
export async function processTemplateById(
templateId: string,
data: TemplateData
): Promise<{ buffer: Buffer; filename: string; contentType: string }> {
const template = await prisma.documentTemplate.findUnique({
where: { id: templateId },
});
const templateUrl = await getFileUrl(template.cloud_storage_path, template.isPublic);
const response = await fetch(templateUrl);
const templateBuffer = Buffer.from(await response.arrayBuffer());
switch (template.fileFormat.toLowerCase()) {
case 'docx':
return {
buffer: await processDocxTemplate(templateBuffer, data),
filename: ,
: ,
};
:
{
: (templateBuffer, data),
: ,
: ,
};
:
{
: (templateBuffer, data),
: ,
: ,
};
}
}
Pattern 6: Template Data Extraction
Extract data from daily report conversations for template filling:
export async function extractDailyReportData(
conversationId: string
): Promise<TemplateData> {
const conversation = await prisma.conversation.findUnique({
where: { id: conversationId },
});
const project = conversation.projectId
? await prisma.project.findUnique({
where: { id: conversation.projectId },
include: { User_Project_ownerIdToUser: { select: { username: true } } },
})
: null;
const reportData = (conversation.reportData as ReportData) || {};
const weatherSnapshots = (conversation.weatherSnapshots as WeatherSnapshot[]) || [];
const photos = (conversation.photos as PhotoEntry[]) || [];
return {
project_name: project?.name || 'Unknown Project',
report_date: new Date(conversation.).(),
: reportData. || ,
: weatherSnapshots[weatherSnapshots. - ]?. || ,
: photos.,
};
}
TemplateData Interface
export interface TemplateData {
project_name?: string;
project_address?: string;
report_date?: string;
report_title?: string;
weather_condition?: string;
weather_temperature?: string;
crew_size?: number;
hours_worked?: number;
tasks_completed?: string;
work_description?: string;
percent_complete?: number;
materials_delivered?: string;
equipment_used?: string;
safety_incidents?: number;
photo_count?: number;
additional_notes?: string;
[key: string]: any;
}
Key Files
| File | Purpose |
|---|
lib/template-processor.ts | Unified template processor (DOCX, XLSX, PDF form filling) |
lib/room-docx-generator.ts | Room sheet DOCX with PizZip (tables, styles, XML) |
lib/room-bulk-export.tsx | Multi-room PDF/DOCX bulk export |
lib/project-summary-report.tsx | Project summary report (PDF/DOCX) |
lib/report-finalization/orchestrator.ts | Report finalization pipeline |
lib/types/report-data.ts | Type definitions for report data fields |
Anti-Patterns
- Never use
new Document() from docx library — ForemanOS uses docxtemplater + PizZip, not the docx npm package.
- Don't forget
nullGetter: () => '' in Docxtemplater — Without this, undefined template vars throw errors.
- Don't use
compression: 'STORE' — Always use compression: 'DEFLATE' for smaller output files.
- Don't forget to escape XML — Raw data in DOCX XML must be escaped with
escapeXml().
- Don't use
xlsx (SheetJS) library — ForemanOS uses xlsx-populate for XLSX processing.
Quick Reference
const output = await processDocxTemplate(templateBuffer, data);
const output = await processXlsxTemplate(templateBuffer, data);
const blob = await generateRoomSheetDOCX(roomData);
const { buffer, filename, contentType } = await processTemplateById(templateId, data);
const data = await extractDailyReportData(conversationId);