Commit Graph

2 Commits

Author SHA1 Message Date
Darko Gjorgjijoski
05e8acc3b1 feat(pdf): let payment receipts and reports be overridden too (#731)
Only invoices and estimates could be customised. Payment receipts and all five
reports were hardcoded to app.pdf.*, so changing them meant editing files inside
the image -- and losing the edit on the next upgrade.

Those documents have no template picker and no design to choose between, so
overriding one is not a selection: it is a same-named file in
storage/app/templates/pdf/{type}/ winning over the built-in. PdfTemplateUtils::
resolveView() is that rule, and it needs no setting, no column and no UI.

resolveView asks View::exists rather than checking the storage disk. The disk and
the view namespace are registered separately and could disagree about where
custom templates live; asking the thing that will actually render removes that
possibility.

make:template covers the new types. Their names are not free, since an override
replaces one specific document, so it validates against the real list -- 'payment'
for payments, and the five report names -- and reports what is available when the
name is wrong. Neither type gets a preview image written, having no picker to
show one in.

The payment preview route also went through the built-in view directly rather
than the service, so ?preview ignored an override and rendered with none of the
shared data. It goes through the service now, like invoices and estimates.

Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
2026-08-01 13:01:15 +02:00
Darko Gjorgjijoski
8ab860a1ae fix(pdf): make custom templates behave the way the docs describe (#730)
Custom templates are a real feature with no test coverage at all, and several
rough edges that only show up once someone actually uses one.

make:template validated nothing. --type was checked only by the interactive
prompt, so `--type=payment` skipped the prompt and died on an uncaught
FileNotFoundException looking for payment1.blade.php: a stack trace instead of a
message. The name was not checked either, so `../escaped` wrote outside the
templates directory. Both are refused now.

Every custom template of a type shared one partials/table.blade.php. It was
written on first use and reused thereafter, so editing the items table for one
custom template silently changed it for all of them -- a file that looks
per-template and behaves globally. Each template now gets its own copy under
partials/{name}/, and its include is rewritten to match. Existing templates keep
including the old shared path, which still resolves.

A custom template sharing a built-in's name appeared twice in the picker with
the same label, and findFormattedTemplate() array_reverses and takes the first
match, so the custom one silently won. The listing is keyed by name now, so it
appears once, as the entry that will actually be used.

A custom template with no same-named .png rendered <img src=""> in the picker: a
blank tile, no error, no hint anything was missing. It falls back to the preview
of the template it was cloned from.

template_name was validated as `required` and nothing else, so any string was
accepted and stored. findFormattedTemplate() returns null for an unknown name,
the null reads as "not custom", and rendering falls through to
app.pdf.{type}.{name} -- a raw "view not found" 500 at PDF time, long after the
save that caused it. New PdfTemplateExists rule, scoped per document type.

make:template also copies a _header/_footer companion when the source template
has one, so a scaffolded template keeps the repeating page furniture.

Both getEstimateTemplateName/getInvoiceTemplateName asked for the template list
with the default image format, base64-encoding a preview of every template just
to read the names back.

Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
2026-08-01 12:56:39 +02:00