[15]
CRM OFFER GENERATOR IN COMPANY TEMPLATE
▓ TIER B · YEAR 2026 · STATUS: LIVE · LANGUAGES: TYPESCRIPT
Deterministic document generation on raw OOXML, in batches
[FIG. 1] MISSION
I deliberately avoid LLMs where repeatability beats creativity, and I can justify that call: a sales offer has to look identical every single time, so I generate it deterministically. Salespeople at a training company used to retype offers from a spreadsheet into Word by hand and paste in their own footers — tedious, error-prone and hopeless at scale. I built them a backend-less SPA (React + Vite), because a backend would be cost without value here: I parse the sheet with the xlsx library using resilient data mapping — columns by index with a header fallback, since a real-world sheet gets rearranged — and show a searchable product list. The export runs surgically on raw OOXML (PizZip), swapping fragments instead of generating the document from scratch: a salesperson's footer is a full-page design with a page-anchored PNG background and margin-anchored floating tables, so instead of re-rendering I swap its sectPr for the footer's zero-margin sectPr and everything lands pixel-perfect. Instead of a single-item operation I build batch export: selected products are packed into separate .docx files inside one ZIP, and the whole thing launches from a .vbs/.ps1 file — a non-technical user never sees a terminal.
[FIG. 2] ARCHITECTURE
hover a block to see its description
[FIG. 3] CHALLENGES
[+][CH-01]
The salesperson's footer wasn't a plain footer but a full-page design: a page-anchored PNG background and margin-anchored floating tables. Every attempt to re-render the document broke the positioning — floating elements landed elsewhere, because tblpX/tblpY coordinates resolve relative to the section's margins. So I went down to raw XML inside the .docx (PizZip): I swap the main document's trailing sectPr for the footer's zero-margin sectPr, which makes the floating tables' coordinates resolve as correct absolute positions. It's OOXML surgery, not an HTML export — and that's why the result lands pixel-perfect.
[+][CH-02]
The source sheet had a life of its own: columns got rearranged, headers renamed, and some extra data appeared only sometimes. So I map columns by index with a header-keyword fallback (product/name, description, scope), skip the technical A/B columns, and pull the X–AC extras only when non-empty. The tool survives sheet reorganizations without my intervention — which, for a tool used by non-technical people, is a survival requirement.
[+][CH-03]
Naively replacing the {{Title}} placeholder in the template didn't work — and for a while it wasn't clear why. When saving, Word cuts continuous text into separate 'runs' (<w:r>), often due to spell-checking or formatting traces, so the string {{Title}} physically doesn't exist in the XML as one piece. I diagnosed it by peeking straight into the template's document.xml with node and printing the context around the 'Title' index — and the fix is a regex matching the whole sequence from the run containing '{{' through any intermediate runs to the run containing '}}', replacing it with a single run holding the XML-escaped title. A classic WordprocessingML trap you have to live through once to recognize it instantly ever after.
[+][CH-04]
The training data comes from an Excel wired to the CRM via an external query with a login — the app needs a fresh file. The launcher's first version drove Excel through COM automation from PowerShell and lost to the platform: Excel opened programmatically behaves differently than on a double-click, and the CRM auto-refresh simply never started. Instead of piling on workarounds, I rolled the automation back entirely: a VBS script opens the file exactly like a double-click, shows a dialog with instructions (log in, wait for the refresh, save, click OK), and only after confirmation copies the fresh file and starts the hidden server. The launcher also finds a free port in the 4000–4004 pool itself, because salespeople's ports are often taken. A human in the loop proved more reliable than brittle automation — a lesson I've kept for good.
[FIG. 4] AI LAYER
The conversion itself is fully deterministic — with offer documents repeatability beats creativity, so an LLM deliberately never touches the content. AI worked in the process instead: I ran the project spec-first in Claude Code (the batch-export spec existed before the implementation), and the sheet's column mapping is documented in CLAUDE.md as a contract.
[FIG. 2A] XML SURGERY
oferta.docx → word/document.xml · PizZip · PATIENT OPEN
<w:body>
<w:p>…product content from the sheet…</w:p>
− <w:sectPr>
− <w:pgMar w:top="1440" w:bottom="1440"/>
− </w:sectPr>
+ <w:sectPr>
+ <w:pgMar w:top="0" w:bottom="0"/>
+ <w:footerReference r:id="rId7"/>
+ </w:sectPr>
</w:body>
→ hover a diff line to see why this is surgery, not a re-render
[FIG. 5] GALLERY
