feat(accounting): PDF/image preview in triage, opened at the QR-bill (no OCR)

Instead of OCR, let the admin read the payment slip directly: the triage modal
now embeds the captured document and, for PDFs, opens at the LAST page scrolled
to the Swiss QR-bill area so IBAN/amount/reference are visible while typing.

- backend: capture PDF page count at upload via pdf-lib (new
  inbound_documents.page_count, added to in-flight migration 124); new
  GET /api/admin/expenses/inbound/:id/file streams the stored file inline
  (safePath-guarded, nosniff). Raw-serve is acceptable here (admin views own
  uploads); the hardened rasterise-in-isolated-worker path stays a follow-up.
- frontend: getInboundFileBlob fetches the file with Bearer auth as a blob;
  the triage modal renders it (iframe for PDF with #page=<last>&view=FitH,300,
  <img> for camera photos) in a two-column layout next to the form.
- i18n: accounting.inbox.previewLoading / qrHint (EN + DE).

Verified: node -c, require-graph, migration-124 harness (page_count), npm run
build green.
This commit is contained in:
Luca
2026-06-11 00:38:37 +02:00
parent 2b5efebaff
commit 502fbad5a8
7 changed files with 101 additions and 7 deletions
+2
View File
@@ -3428,6 +3428,8 @@
"untitled": "Unbenanntes Dokument",
"noAmount": "Betrag nicht erfasst",
"rebillHint": "Erstellt eine bearbeitbare geplante Rechnung beim Kunden. MwSt-/Steuerbehandlung ist v1 — mit Treuhänder prüfen.",
"previewLoading": "Vorschau wird geladen…",
"qrHint": "An der letzten Seite geöffnet — der Schweizer QR-Einzahlschein sitzt meist unten.",
"status": {
"unsorted": "Neu",
"categorized": "Kategorisiert",
+2
View File
@@ -3428,6 +3428,8 @@
"untitled": "Untitled document",
"noAmount": "amount not entered",
"rebillHint": "Creates an editable scheduled invoice on the client. VAT/tax handling is v1 — verify with your Treuhänder.",
"previewLoading": "Loading preview…",
"qrHint": "Opened at the last page — the Swiss QR-bill usually sits at the bottom.",
"status": {
"unsorted": "New",
"categorized": "Categorized",