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
@@ -59,6 +59,7 @@ exports.up = async function (knex) {
table.string('parse_status', 16).notNullable().defaultTo('pending'); // pending|parsed|failed|manual
table.text('parse_error');
table.string('parse_method', 24); // qr|pdf_text|ocr|none
table.integer('page_count'); // PDF page count (for "jump to last page / QR")
// Best-effort parsed fields (assist only — always editable/confirmable):
table.string('supplier_name', 255);
table.string('invoice_number', 128);