You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As a company admin, I want a Report Builder where I design document templates by dropping Bricks (Company, Client, Invoice Meta, Items, Totals, …) into five bands — Header, Group Header, Details, Group Footer, Footer — so that invoice and quote PDFs render exactly the layout my company needs, without touching code.
We deliver default bricks and default templates; the user clones and customizes. The PDF generators consume these templates — wanting multiple invoice templates (#411) is the same deliver-and-clone mechanism.
This reframes the original v1 PHP-block spec (kept at the bottom for history): v2 composes code-shipped mason bricks with data-only config files, never reorders raw PHP.
resources/report-templates/ # shipped with the app, version-controlled
invoice/default/manifest.json # { name, slug, type: invoice, version }
invoice/default/bands.json # { header: […], group_header: […], details: […], group_footer: […], footer: […] }
quote/default/{manifest,bands}.json
storage/app/report_templates/ # runtime disk 'report_templates'
system/… # synced from resources/ via `php artisan reports:sync-system`
{company_id}/{slug}/{manifest,bands}.json # user clones — clone = copy folder + rewrite manifest (name, slug, cloned_from)
No report_templates or report_blocks tables. Filament lists templates by scanning the disk; a thin ReportTemplateStorage service handles list/load/save/clone/delete.
bands.json entries are data only: { "brick": "header_company", "width": "half", "config": { "show_vat_id": true } }.
Bricks — code only
Classes in app/Mason/Bricks/ extending Awcodes\Mason\Brick, discovered via ReportBricksCollection (no DB registry). Each declares id/label/icon, allowedBands(), a config slide-over form, and toHtml()/toPreviewHtml() backed by escaped blade views. Details in #521. Seventeen bricks + views are harvested from feature/145-report-builder.
Builder UI
Admin-panel custom Filament pages (no Eloquent resource): a template list (system + current company) and a builder page with five stacked Mason canvases, one per band, each offering only the bricks allowed in that band. Cross-band moves use a "move to band…" action — no cross-canvas dragging (that is what stalled the previous attempt). Actions: Clone (system → company), Rename, Delete (company templates only; system read-only), Preview with dummy data. Details in #523, #519, #525.
Rendering
ReportRenderer: manifest + bands.json + entity → HTML (details band iterates line items; group bands repeat per group) → PDF through the domPDF driver from PR #575. Template resolution chain: document's pdf_template slug → company default setting → system/<type>/default, with the blade template as fallback until parity.
Security
No user-authored PHP/Blade, ever. Users compose shipped bricks; configs are pure data. (This deliberately supersedes the raw-source-editor approach of [Core]: Online PDF invoice template editor #401 — editing Blade in the browser is template-injection/RCE territory.)
bands.json validated on load: unknown brick ids skipped, config filtered against the brick's schema, widths/bands validated against enums.
Slugs sanitized ([a-z0-9-], no ..); storage paths derived from the tenant, never from user input; brick views escape everything with {{ }}; dompdf remote fetch disabled.
Acceptance criteria (Phase 3 exit)
php artisan reports:sync-system publishes shipped templates to storage/app/report_templates/system/ (idempotent).
Builder page shows five band canvases; each offers only bricks whose allowedBands() include that band.
Saving writes bands.json under the company folder; zero DB rows involved.
Clone copies a system template into the company folder; the clone is editable, the system original is not.
ReportRenderer renders invoice/default for a real invoice and produces non-empty PDF bytes via the domPDF driver.
Toggling a brick config off (e.g. show_vat_id: false) removes that field from the rendered output.
PR [IP-542]: invoice edit page #575 (domPDF driver, Modules/Core/Support/PDF/Drivers/domPDF.php) must merge before Phase 3. Phases 1–2 are unblocked.
Branch feature/145-report-builder (PR [IP-130]: report builder #532, closed as superseded) is the harvest source: 17 bricks + blade views, ReportBand/ReportBlockWidth enums, DTOs, storage services, ~20 test files. Fresh work happens on feature/130-report-builder.
Original v1 spec (historical — superseded by the bands + bricks architecture above)
Core Functionality
Drag-and-Drop (DND) Interface: The user must be able to click and drag predefined content blocks (which represent the PHP code chunks) and drop them into designated sorting areas.
Encapsulation of PHP: Each draggable element must represent a complete, functional block of PHP/HTML from the original template (e.g., client_address_1) { ... } ?>). When the block is dragged, the entire underlying PHP string moves with it.
Sorting Zones: The tool must clearly define zones where dragging is allowed, such as:
Client Information Block (#client)
Company Information Block (#company)
Invoice Terms Block (.invoice-terms)
Cross-Zone Movement: Elements should be movable between the Client and Company zones (e.g., moving the client's phone number to the company information block).
Persistence: A Save function must collect the PHP code strings in their new order and write them back into the corresponding sections of the server-side template file (e.g., default.php).
Template Blocks (Examples)
The system should recognize and encapsulate the following as individual draggable elements:
The following scenarios detail how the tool should function to meet the user's need to swap Address 1 and VAT ID in the Client information section.
Scenario 1: Swapping Address and VAT ID
Goal: Ensure the user can rearrange the order of information displayed for the client.
AAA Phase
Description
Arrange
The template editor is loaded. The current order in the Client Info block is: 1. Client Name, 2. VAT ID, 3. Address 1.
Act
The user clicks and drags the Address 1 element and drops it immediately above the VAT ID element.
Assert
Visual: The elements in the editor now display in the order: 1. Client Name, 2. Address 1, 3. VAT ID. Functionality: The user clicks Save. The server-side template file is updated so the PHP code block for client_address_1 precedes the PHP code block for client_vat_id.
---
---
Scenario 2: Moving a Block between Client and Company
Goal: Ensure fields can be moved between the two main address columns.
AAA Phase
Description
Arrange
The template editor is loaded. The Client Phone number is currently listed at the bottom of the Client Info block.
Act
The user clicks and drags the Client Phone element and drops it at the bottom of the Company Info block.
Assert
Visual: The Client Phone element is removed from the Client block and appears in the Company block. Functionality: The user clicks Save. The PHP code for $invoice->client_phone is completely cut from the Client section of the template file and inserted into the Company section of the template file.
---
---
Implementation Note for Developer
The most challenging aspect is the persistence layer (the Save function). The backend must be able to:
Read the current PHP template file (e.g., default.php).
Identify and isolate the specific PHP code blocks within the defined zones (#client, #company).
Receive the newly ordered list of PHP strings from the front end.
Replace the old, ordered set of PHP code blocks in the file with the new, reordered set.
This will likely require careful use of regular expressions or a robust file manipulation utility on the server to avoid corrupting the non-movable surrounding PHP/HTML code
Story
As a company admin, I want a Report Builder where I design document templates by dropping Bricks (Company, Client, Invoice Meta, Items, Totals, …) into five bands — Header, Group Header, Details, Group Footer, Footer — so that invoice and quote PDFs render exactly the layout my company needs, without touching code.
We deliver default bricks and default templates; the user clones and customizes. The PDF generators consume these templates — wanting multiple invoice templates (#411) is the same deliver-and-clone mechanism.
This reframes the original v1 PHP-block spec (kept at the bottom for history): v2 composes code-shipped mason bricks with data-only config files, never reorders raw PHP.
Part of epic #506 (Track A). Sub-tasks: #521, #528 (Phase 1), #523, #519, #525 (Phase 2).
Architecture (decided 2026-07-04)
Storage — pure files, no DB tables
report_templatesorreport_blockstables. Filament lists templates by scanning the disk; a thinReportTemplateStorageservice handles list/load/save/clone/delete.bands.jsonentries are data only:{ "brick": "header_company", "width": "half", "config": { "show_vat_id": true } }.Bricks — code only
Classes in
app/Mason/Bricks/extendingAwcodes\Mason\Brick, discovered viaReportBricksCollection(no DB registry). Each declares id/label/icon,allowedBands(), a config slide-over form, andtoHtml()/toPreviewHtml()backed by escaped blade views. Details in #521. Seventeen bricks + views are harvested fromfeature/145-report-builder.Builder UI
Admin-panel custom Filament pages (no Eloquent resource): a template list (system + current company) and a builder page with five stacked Mason canvases, one per band, each offering only the bricks allowed in that band. Cross-band moves use a "move to band…" action — no cross-canvas dragging (that is what stalled the previous attempt). Actions: Clone (system → company), Rename, Delete (company templates only; system read-only), Preview with dummy data. Details in #523, #519, #525.
Rendering
ReportRenderer: manifest + bands.json + entity → HTML (details band iterates line items; group bands repeat per group) → PDF through the domPDF driver from PR #575. Template resolution chain: document'spdf_templateslug → company default setting →system/<type>/default, with the blade template as fallback until parity.Security
bands.jsonvalidated on load: unknown brick ids skipped, config filtered against the brick's schema, widths/bands validated against enums.[a-z0-9-], no..); storage paths derived from the tenant, never from user input; brick views escape everything with{{ }}; dompdf remote fetch disabled.Acceptance criteria (Phase 3 exit)
php artisan reports:sync-systempublishes shipped templates tostorage/app/report_templates/system/(idempotent).allowedBands()include that band.bands.jsonunder the company folder; zero DB rows involved.ReportRendererrendersinvoice/defaultfor a real invoice and produces non-empty PDF bytes via the domPDF driver.show_vat_id: false) removes that field from the rendered output.page-break-inside: avoid; a PageBreak brick forces a page break ([Core]: PDF templates — page-break support #95).../and invalid slugs are rejected.Test scenarios (PHPUnit, class-based,
#[Test]— no Pest)Dependencies & harvest source
Modules/Core/Support/PDF/Drivers/domPDF.php) must merge before Phase 3. Phases 1–2 are unblocked.feature/145-report-builder(PR [IP-130]: report builder #532, closed as superseded) is the harvest source: 17 bricks + blade views,ReportBand/ReportBlockWidthenums, DTOs, storage services, ~20 test files. Fresh work happens onfeature/130-report-builder.Original v1 spec (historical — superseded by the bands + bricks architecture above)
Core Functionality
Template Blocks (Examples)
The system should recognize and encapsulate the following as individual draggable elements:
<b><?php _htmlsc(format_client($invoice)); ?></b><?php if ($invoice->client_vat_id) { echo '<div>'. trans('vat_id_short') . ': ' . htmlsc($invoice->client_vat_id) . '</div>'; } ?><?php if ($invoice->client_address_1) { echo '<div>' . htmlsc($invoice->client_address_1) . '</div>'; } ?><?php if ($invoice->client_phone) { echo '<div>' . trans('phone_abbr') . ': ' . htmlsc($invoice->client_phone) . '</div>'; } ?>Use Case Scenarios (AAA)
The following scenarios detail how the tool should function to meet the user's need to swap Address 1 and VAT ID in the Client information section.
Scenario 1: Swapping Address and VAT ID
Goal: Ensure the user can rearrange the order of information displayed for the client.
Scenario 2: Moving a Block between Client and Company
Goal: Ensure fields can be moved between the two main address columns.
Implementation Note for Developer
The most challenging aspect is the persistence layer (the Save function). The backend must be able to:
This will likely require careful use of regular expressions or a robust file manipulation utility on the server to avoid corrupting the non-movable surrounding PHP/HTML code