ClientManager · Engineering Reference

Report Builder

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.

Phases 0, 1, 2, 4 shipped Phase 3 not scheduled 5 phases 3 open questions
WHY THIS EXISTS — The 21 shipped Estimate Reports are deliberately fixed, non-editable system reports (subtotal/variance math is baked into C#, not just merge fields). This plan is the answer to the separate, real ask: what does a tenant get when they want to define their own report? Reuses the existing merge engine end to end rather than introducing a second one.

Grounded facts

Verified by direct code reading on 2026-08-31 — not assumed.

Design decisions

V1 grouping scope: EstimateItem × CategoryID only

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).

Grouping config lives inline in HTML attributes, not a side SQL table

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>

Phasing

Phase 0 — Flat custom reportsShipped

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.

Phase 1 — The grouped-section engineShipped

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.

Phase 2 — PolishShipped

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.

Phase 3 — Generalize, only if neededNot scheduled

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.

Phase 4 — Totals, Header/Footer control, and a standalone Run featureShipped

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").

Open questions

Flagged deliberately, not silently decided — revisit when this plan is picked up.

QuestionLeaning
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.

Verification approach

Critical files

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.