Skip to content

feat: add reusable template builder system#198

Open
roncodes wants to merge 5 commits intomainfrom
feature/template-builder-system
Open

feat: add reusable template builder system#198
roncodes wants to merge 5 commits intomainfrom
feature/template-builder-system

Conversation

@roncodes
Copy link
Member

@roncodes roncodes commented Mar 3, 2026

Template Builder System

Introduces a foundational, reusable template builder system for designing invoice templates, shipping label templates, report templates, receipts, and other documents.

Architecture

Templates are reusable design artefacts — they contain layout, styling, and variable syntax but have no permanent data binding. Data is only injected at render time by the caller passing a subject model.

New Files

Migrations

  • 2026_03_03_000001_create_templates_table.php — Canvas-based template with context_type, dimensions (width/height/unit/orientation), margins, background, and a JSON content array of element objects.
  • 2026_03_03_000002_create_template_queries_table.php — Query data sources attached to a template for iterable collections (e.g. orders in December{orders}).

Models

  • Models/Template.php — Template model with queries HasMany, scopes for context type and company availability.
  • Models/TemplateQuery.php — TemplateQuery model with a self-contained execute() method that applies conditions, sort, limit, and eager-loads.

HTTP Layer

  • Http/Resources/Template.php + TemplateQuery.php
  • Http/Requests/Internal/CreateTemplateRequest.php
  • Http/Controllers/Internal/v1/TemplateController.php — extends FleetbaseController; adds preview (→ HTML), render (→ PDF download), and contextSchemas (→ variable picker schemas) endpoints.
  • Http/Controllers/Internal/v1/TemplateQueryController.php — standard CRUD.

Service

  • Services/TemplateRenderService.php — core rendering engine with:
    • Three-tier context: global/ambient ({company.*}, {user.*}, {now}, {today}) → primary subject → query collections
    • Variable syntax: {namespace.property} with dot-notation
    • Formula syntax: [{ {invoice.subtotal} * 1.1 }] with safe arithmetic evaluation (uses mossadal/math-parser when available, falls back to built-in recursive descent parser)
    • Iteration blocks: {{#each variable}} ... {{/each}} with {this.property}, {loop.index}, {loop.first}, {loop.last}
    • Element types: text, image, table (with dynamic data source), line, shape, qr_code, barcode — all absolutely positioned
    • PDF output via spatie/laravel-pdf with configurable paper size and margins
    • registerContextType() static method for extensions (Ledger, FleetOps, etc.) to register their own context types and variable schemas

Modified Files

  • src/routes.php — registers templates and template-queries routes under the protected internal v1 group
  • src/Providers/CoreServiceProvider.php — registers TemplateRenderService as a singleton
  • composer.json — adds spatie/laravel-pdf ^2.0 and mossadal/math-parser ^2.0

API Endpoints

Method Path Description
GET /int/v1/templates List templates
POST /int/v1/templates Create template
GET /int/v1/templates/:id Get template
PUT /int/v1/templates/:id Update template
DELETE /int/v1/templates/:id Delete template
GET /int/v1/templates/context-schemas Variable picker schemas
POST /int/v1/templates/:id/preview Render to HTML
POST /int/v1/templates/:id/render Render to PDF download
GET/POST/PUT/DELETE /int/v1/template-queries Query data source CRUD

Introduces a foundational, reusable template builder system that supports
designing invoice templates, shipping label templates, report templates,
receipts, and other documents.

New files:
- migrations/2026_03_03_000001_create_templates_table.php
- migrations/2026_03_03_000002_create_template_queries_table.php
- src/Models/Template.php
- src/Models/TemplateQuery.php
- src/Http/Resources/Template.php
- src/Http/Resources/TemplateQuery.php
- src/Http/Requests/Internal/CreateTemplateRequest.php
- src/Http/Controllers/Internal/v1/TemplateController.php
- src/Http/Controllers/Internal/v1/TemplateQueryController.php
- src/Services/TemplateRenderService.php

Modified files:
- src/routes.php — registers templates and template-queries routes
- src/Providers/CoreServiceProvider.php — registers TemplateRenderService singleton
- composer.json — adds spatie/laravel-pdf ^2.0 and mossadal/math-parser ^2.0

Key features:
- Three-tier context: global/ambient (company, user, date) + primary subject + query collections
- Variable syntax: {namespace.property} with dot-notation
- Formula syntax: [{ {invoice.subtotal} * 1.1 }] with safe arithmetic evaluation
- Iteration blocks: {{#each variable}} ... {{/each}} with {this.property}, {loop.index}
- Canvas-based element system: text, image, table, line, shape, qr_code, barcode
- PDF output via spatie/laravel-pdf; HTML preview endpoint
- registerContextType() static method for extensions to register context types
roncodes pushed a commit to fleetbase/ember-ui that referenced this pull request Mar 3, 2026
Implements a full-featured, reusable template builder component for designing
invoice templates, shipping label templates, report templates, receipt templates,
and other document types.

## Components

- `TemplateBuilder` — top-level orchestrator, composes all panels
- `TemplateBuilder::Canvas` — pixel-perfect free-canvas with interact.js drag/resize
- `TemplateBuilder::ElementRenderer` — renders all element types (text, image, table, line, shape, qr_code, barcode)
- `TemplateBuilder::Toolbar` — element type picker, zoom controls, undo/redo, save/preview
- `TemplateBuilder::LayersPanel` — element tree with visibility toggle, reorder, delete
- `TemplateBuilder::PropertiesPanel` — contextual properties editor with Position, Size, Typography, Appearance, Border, Effects sections
- `TemplateBuilder::PropertiesPanel::Section` — collapsible section wrapper
- `TemplateBuilder::PropertiesPanel::Field` — labelled field row
- `TemplateBuilder::VariablePicker` — modal for browsing context schema variables and composing formulas

## Key Features

- Free-canvas drag and resize via interact.js with grid snapping
- 50-step undo/redo history
- Variable syntax: `{invoice.total}`, `{company.name}`
- Formula syntax: `[{ {invoice.subtotal} * 1.1 }]`
- Iteration syntax: `{{#each orders}}...{{/each}}`
- Three-tier context: global/ambient → primary subject → query collections
- Variable picker with search, namespace grouping, type icons, and formula editor
- Full dark mode support via Tailwind + data-theme='dark'
- Paper size and orientation support (A4, Letter, Legal, custom)
- Element types: text, image, table, line, shape, qr_code, barcode

## Dependencies

- interactjs ^1.10.27 (drag/resize)

Refs: fleetbase/core-api#198
roncodes pushed a commit to fleetbase/fleetbase that referenced this pull request Mar 3, 2026
Adds two Ember Data models to the console app to support the new
template builder system introduced in fleetbase/core-api#198 and
fleetbase/ember-ui#121.

## Models

### template
- Full attribute set matching the backend Template model
- Computed: isDraft, isPublished, contextTypeLabel, dimensionLabel
- hasMany: template-query (inverse)
- Supports: name, description, context_type, paper_size, orientation,
  width, height, unit, background_color, content (array), is_default, status

### template-query
- Full attribute set matching the backend TemplateQuery model
- Computed: variableName, variableToken, resourceTypeLabel
- belongsTo: template (inverse)
- Supports: name, label, description, resource_type, filters (object),
  sort_by, sort_direction, limit

Both models follow the existing Fleetbase model conventions:
- @ember-data/model with attr/belongsTo/hasMany decorators
- Standard date computed properties (updatedAgo, updatedAt, createdAt, etc.)
- date-fns for date formatting

Refs: fleetbase/core-api#198, fleetbase/ember-ui#121
Ronald A Richardson and others added 4 commits March 3, 2026 20:39
…pdate

Override createRecord and updateRecord to handle a 'queries' array in the
request payload. After saving the template, _syncQueries() upserts each
query (create if no UUID, update if UUID exists) and soft-deletes any
queries not present in the incoming list (i.e. deleted in the builder).

Temporary client-side UUIDs prefixed with '_new_' are treated as new
records (UUID stripped before insert).

The Template HTTP resource already serializes 'queries' via whenLoaded;
both overridden methods eager-load 'queries' before returning the response
so the frontend receives the full updated list in one round-trip.
…lateController

Both methods were declared as returning JsonResponse but actually return a
TemplateResource (which extends JsonResource, not JsonResponse). This caused
a fatal TypeError at runtime:

  Return value must be of type Illuminate\Http\JsonResponse,
  Fleetbase\Http\Resources\Template returned

Fix: widen the return type union to JsonResource|JsonResponse so both the
TemplateResource path (normal request) and the JsonResponse path (error /
internal request fallback) satisfy the type contract.
… createRecord/updateRecord

Replace the manual overrides of createRecord() and updateRecord() with the
proper HasApiControllerBehavior hook methods onAfterCreate() and onAfterUpdate().

getControllerCallback() in the trait checks whether these methods exist on the
controller and wraps them as closures that are passed to createRecordFromRequest()
and updateRecordFromRequest() in HasApiModelBehavior. Both hooks receive the
same signature: (Request $request, Model $record, array $input).

The hooks call _syncQueries() to upsert/delete TemplateQuery rows and then
load the queries relationship so the response includes them — identical
behaviour to before, but without duplicating the entire create/update flow
or introducing a return-type mismatch.

The _templateFromResponse() helper is also removed as it is no longer needed.
Previously only POST /templates/{id}/preview existed, which required a
persisted record. This meant the builder could not preview a template
before it was saved for the first time.

Changes:

routes.php
  Register POST 'preview' (no {id}) before the existing {id}/preview
  route so Laravel's router matches the literal segment first.

TemplateController::previewUnsaved()
  New method that:
  1. Reads the 'template' key from the request body.
  2. Hydrates a transient (non-persisted) Template model via fill().
  3. Hydrates transient TemplateQuery models from the nested 'queries'
     array and sets them directly on the relation via setRelation() so
     the render pipeline can iterate them without any DB access.
  4. Resolves an optional subject model (subject_type / subject_id).
  5. Delegates to TemplateRenderService::renderToHtml() and returns
     { html } — identical response shape to the persisted preview.

TemplateRenderService::buildContext()
  Guard the loadMissing('queries') call with a relationLoaded() check.
  When the relation has already been set in-memory (transient model case)
  loadMissing would overwrite it with an empty DB result, breaking the
  query context tier for unsaved previews.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant