From 76060b0632a6cca112a5464a7cd2bedf25c00904 Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 20 Apr 2026 18:06:40 +0200 Subject: [PATCH] =?UTF-8?q?feat(invoices):=20M3=20logo=20upload=20?= =?UTF-8?q?=E2=80=94=20embed=20in=20PDF=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the Settings polish item left open after M2. pdf/logo.ts - loadLogo(mediaId): fetches the large variant from mana-media, sniffs content-type to pick 'png' vs 'jpg', returns null on any failure so the PDF still renders without a logo - uploadLogo(file): multipart POST to /api/v1/media/upload with app=invoices, returns the new mediaId (or throws a user-facing msg) - logoPreviewUrl(mediaId): thin helper so the settings form doesn't have to know the media-URL lookup pattern Renderer wiring - loadLogo runs in the same Promise.all as font embedding so it doesn't add a serial wait - embedPng / embedJpg based on the sniffed kind; errors degrade silently - renderHeader takes a PDFImage|null and, when present, draws it top- left above the sender name, max 25mm × 45% content-width, aspect preserved, 3mm breathing room below Settings UI (SenderProfileForm) - Logo slot at the top of the Absender section: preview when set, "Ersetzen" / "Entfernen" actions; "+ Logo hochladen" drop-style button when empty - Upload persists immediately (no separate "Speichern" click for logo changes) — keeps the interaction one-handed - Accepts PNG / JPEG; invalid types rejected client-side before the network round-trip Closes one of the open items from docs/plans/invoices-module.md §M3. Next open: M8 AI-tools (create_invoice / mark_paid / list / stats). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/SenderProfileForm.svelte | 160 ++++++++++++++++++ .../web/src/lib/modules/invoices/pdf/logo.ts | 88 ++++++++++ .../src/lib/modules/invoices/pdf/renderer.ts | 58 ++++++- 3 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 apps/mana/apps/web/src/lib/modules/invoices/pdf/logo.ts diff --git a/apps/mana/apps/web/src/lib/modules/invoices/components/SenderProfileForm.svelte b/apps/mana/apps/web/src/lib/modules/invoices/components/SenderProfileForm.svelte index f4de1016b..d56dcd9ed 100644 --- a/apps/mana/apps/web/src/lib/modules/invoices/components/SenderProfileForm.svelte +++ b/apps/mana/apps/web/src/lib/modules/invoices/components/SenderProfileForm.svelte @@ -6,10 +6,49 @@ import { invoiceSettingsStore } from '../stores/settings.svelte'; import type { InvoiceSettings, Currency } from '../types'; import { VAT_RATES_CH, VAT_RATES_DE, CURRENCIES } from '../constants'; + import { uploadLogo, logoPreviewUrl } from '../pdf/logo'; let settings = $state(null); let saving = $state(false); let savedAt = $state(null); + let uploadingLogo = $state(false); + let logoError = $state(null); + let logoInput: HTMLInputElement | undefined = $state(); + + async function onLogoSelect(e: Event) { + const input = e.target as HTMLInputElement; + const file = input.files?.[0]; + if (!file || !settings) return; + uploadingLogo = true; + logoError = null; + try { + const mediaId = await uploadLogo(file); + // Persist immediately — user doesn't need to hit "Speichern" separately + // for logo changes. The in-memory settings state stays in sync so + // further edits don't lose the mediaId. + await invoiceSettingsStore.update({ logoMediaId: mediaId }); + settings.logoMediaId = mediaId; + } catch (e) { + logoError = e instanceof Error ? e.message : 'Upload fehlgeschlagen'; + } finally { + uploadingLogo = false; + input.value = ''; + } + } + + async function removeLogo() { + if (!settings) return; + uploadingLogo = true; + logoError = null; + try { + await invoiceSettingsStore.update({ logoMediaId: null }); + settings.logoMediaId = null; + } catch (e) { + logoError = e instanceof Error ? e.message : 'Entfernen fehlgeschlagen'; + } finally { + uploadingLogo = false; + } + } $effect(() => { invoiceSettingsStore.get().then((s) => { @@ -66,6 +105,56 @@

Absender

Erscheint im Kopf jeder Rechnung.

+
+ Logo +
+ {#if settings.logoMediaId} + Logo-Vorschau +
+ + +
+ {:else} + + {/if} + +
+ {#if logoError} + {logoError} + {/if} +
+