A tenant-facing, SSRS-style report designer — header, grouped body sections, footer — built on the document-merge engine CM already ships, not a second rendering pipeline. Written 2026-08-31. Phases 0-2 shipped the same day; Phase 4 (totals on flat tables, Include Header/Footer, a standalone Run feature) shipped 2026-09-02.
Verified by direct code reading on 2026-08-31 — not assumed.
DocumentGenerationService.RenderHtmlAsync already handles single-value merge fields, flat repeating tables (5 entities wired: EstimateItem, InvoiceItem, QuoteItem, PurchaseOrderItem, ProjectSchedule), and grouped/subtotaled tables (currently only EstimateItem grouped by CategoryID, proven correct across all 21 shipped reports). PDF (Blink) and Word (DocIO) export both consume the same resolved HTML — anything plugged into this pipeline gets both formats for free.DocumentTemplateEditor.razor already builds flat custom reports today. Its Syncfusion rich-text editor has working "Insert Field" and "Insert Repeating Table" toolbar buttons, sourced from the real merge-field catalog (Ref_Tables / Ref_TableFields). The only gap for the flat case is discoverability — no TemplateType routes here outside the five built-in document kinds.FieldMetadata powers Choose Columns / Advanced Filter on grids. Not the same system as the merge-field catalog and not what this plan builds on; conflating the two would be a real mistake.EstimateItem × CategoryID — nothing else has ever been grouped.Not a generic "any entity, any field" engine. Building that now, for entities nobody has asked to group, means inventing a "what's summable" abstraction with zero real use case to validate against. V1 lets a tenant pick detail columns, toggle numbered headings, and toggle subtotals — the exact toggles already proven across the 21 hardcoded reports — but the data source and group-by field stay fixed to the one real pattern. Revisit only if a second real grouping need shows up (Phase 3).
A side table keyed by TemplateID would silently orphan on template copy — CopyToSiteAsync (Browse Template Library) only ever copies BodyHtml. Embedding config as data-* attributes on the placeholder div travels correctly through copy/export with zero new table, matching the existing data-merge-repeat precedent:
<div class="report-group-section" data-source="EstimateItem" data-groupby="CategoryID"
data-fields="ItemDesc,ItemQuantity,UOM,ExtPriceTotal" data-numbered="true" data-subtotal="true">
[Grouped Section: Estimate Items by Category]
</div>
Add a CustomReport TemplateType + GetCustomReportTemplatesAsync + a generate/preview entry point. Zero changes to DocumentGenerationService.cs — flat-repeat-table and single-field merge already fully cover Header + flat Body list + Footer. Fully independent, genuinely shippable on its own, validates all the plumbing before any new rendering code.
GroupSectionRegex, RenderConfigurableGroupedTable, IsSummableEstimateItemField, wired into RenderHtmlAsync; the "Insert Grouped Section" dialog and toolbar button in the template editor. Subtotal math cross-checked against an independent direct-SQL computation — exact match.
Custom reports merged into the Estimates grid's Print Reports dialog with a labeled "Your Custom Reports" section alongside the system reports; saving a CustomReport with no real content (no field, table, or grouped section) now shows a dismissible soft-warning instead of silently saving a degenerate report.
Extend data-source beyond EstimateItem to the other four repeat-table entities, and/or add a real DataType/summable flag to the merge field catalog so the summable check generalizes instead of a new hardcoded switch per entity.
Totals row on flat repeating tables (previously only the grouped-section path had this): "Insert Repeating Table" gains a "Show totals row" checkbox, shown only when the picked entity has a real summable field - newly exposed QuoteItem.ExtQuotedPrice and PurchaseOrderItem.TotalPrice (both already existed on the model, just weren't resolved by the merge engine) alongside the existing EstimateItem price fields.
Include Header / Include Footer, per template, all types (not just CustomReport) - two new DocumentMergeTemplate columns, a read-only preview of the site's actual header/footer shown only while its checkbox is checked, and a real fix for a genuine Chromium print-to-PDF bug (a bare empty header/footer string doesn't render blank, it triggers Chromium's own fallback date stamp - suppressed with the same invisible-but-real template already used for the system Estimate reports).
Report Designer got its own standalone "Run" feature - a Project/Estimate/Invoice/Quote/PO picker + Generate button, live-verified end to end, deliberately not wired into the Estimates grid's Print Reports dialog or any other existing reporting surface, per direct instruction ("we are not linking this Designer to any specific reporting area... that's the whole point").
Flagged deliberately, not silently decided — revisit when this plan is picked up.
| Question | Leaning |
|---|---|
| Group-by source dropdown now vs. later | V1 hardcodes EstimateItem/CategoryID with no dropdown at all. Cheap to add now for future-proofing; simpler to leave out today — no second real use case exists yet. |
| Where custom reports live/generate from | This plan reuses the Estimates grid's Print Reports dialog (fine while the only source is EstimateItem). A ProjectSchedule-grouped report would need a Project-level entry point instead — decide now or relocate later. |
| Merged vs. separate report list | Recommended: merge into the same Print Reports dialog with a subheader distinguishing system vs. custom. Alternative: a fully separate "Custom Reports" entry point. |
CustomReport template through the editor as a tenant would, generate via the Estimates grid, confirm PDF/Word both render correctly with real project/estimate data — same PyMuPDF render-and-inspect pattern used for the 21 system reports.Reference patterns to mirror closely: RenderItemsByCostCategoryHtml / RenderCategoryCostSummaryHtml (the grouping/subtotal logic V1 generalizes) and InsertRepeatingTableAsync (the dialog pattern to copy for "Insert Grouped Section"), both already in the codebase.