diff --git a/apps/mana/apps/web/src/lib/data/crypto/registry.ts b/apps/mana/apps/web/src/lib/data/crypto/registry.ts index b6166e2b4..4b6da63ba 100644 --- a/apps/mana/apps/web/src/lib/data/crypto/registry.ts +++ b/apps/mana/apps/web/src/lib/data/crypto/registry.ts @@ -623,9 +623,15 @@ export const ENCRYPTION_REGISTRY: Record = { // and are the most sensitive fields in the module (appear on every PDF // the user issues). logoMediaId / accentColor / number sequence state // are plaintext — structural, no privacy value. + // Structured address fields (senderStreet/Zip/City/Country) get the + // same treatment as the legacy senderAddress blob. invoiceSettings: entry([ 'senderName', 'senderAddress', + 'senderStreet', + 'senderZip', + 'senderCity', + 'senderCountry', 'senderEmail', 'senderVatNumber', 'senderIban', diff --git a/apps/mana/apps/web/src/lib/modules/finance/stores/finance.svelte.ts b/apps/mana/apps/web/src/lib/modules/finance/stores/finance.svelte.ts index 6412a454d..151fb0907 100644 --- a/apps/mana/apps/web/src/lib/modules/finance/stores/finance.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/finance/stores/finance.svelte.ts @@ -66,6 +66,57 @@ export const financeStore = { emitDomainEvent('TransactionDeleted', 'finance', 'transactions', id, { transactionId: id }); }, + /** + * Upsert an income transaction linked to a paid invoice. + * + * Deterministic id (`invoice-tx-{invoiceId}`) makes the write idempotent: + * marking the same invoice paid twice updates the row instead of + * inserting a duplicate. Users can still delete the finance row + * manually if they don't want it — this isn't a sealed join. + * + * Multi-currency caveat: finance stores bare `number` amounts without + * a currency column. Callers pass the invoice's gross in its own + * currency; if the user mixes CHF invoices and EUR finance, the + * finance totals will be wrong — that's a known limitation of the + * finance module, not of this bridge. + */ + async upsertTransactionFromInvoice(data: { + invoiceId: string; + amount: number; + description: string; + date: string; + note?: string; + }) { + const id = `invoice-tx-${data.invoiceId}`; + const newLocal: LocalTransaction = { + id, + type: 'income', + amount: Math.abs(data.amount), + categoryId: null, + description: data.description, + date: data.date, + note: data.note ?? null, + }; + await encryptRecord('transactions', newLocal); + await transactionTable.put(newLocal); + emitDomainEvent('TransactionCreated', 'finance', 'transactions', id, { + transactionId: id, + amount: data.amount, + type: 'income', + description: data.description, + source: 'invoice', + invoiceId: data.invoiceId, + }); + }, + + /** Remove the auto-generated income when an invoice is reset / voided. */ + async deleteTransactionFromInvoice(invoiceId: string) { + const id = `invoice-tx-${invoiceId}`; + const existing = await transactionTable.get(id); + if (!existing || existing.deletedAt) return; + await this.deleteTransaction(id); + }, + async addCategory(data: { name: string; emoji: string; color: string; type: TransactionType }) { const existing = await categoryTable.toArray(); const count = existing.filter((c) => !c.deletedAt && c.type === data.type).length; diff --git a/apps/mana/apps/web/src/lib/modules/invoices/components/ClientPicker.svelte b/apps/mana/apps/web/src/lib/modules/invoices/components/ClientPicker.svelte index bc3de271e..1b88b12f7 100644 --- a/apps/mana/apps/web/src/lib/modules/invoices/components/ClientPicker.svelte +++ b/apps/mana/apps/web/src/lib/modules/invoices/components/ClientPicker.svelte @@ -45,9 +45,14 @@ source: 'contact' as ClientSource, name: c.displayName ?? 'Unbenannter Kontakt', email: c.email, - address: [c.street, c.postalCode && c.city ? `${c.postalCode} ${c.city}` : null] - .filter(Boolean) - .join('\n'), + // Contacts already have structured fields — map them over directly + // so picking a contact populates Strasse/PLZ/Ort without lossy + // string-joining. + street: c.street ?? undefined, + zip: c.postalCode ?? undefined, + city: c.city ?? undefined, + country: c.country ?? undefined, + address: undefined as string | undefined, })); const fromClients = invoiceClients .filter((c) => c.name.toLowerCase().includes(q)) @@ -56,7 +61,11 @@ source: 'invoice-client' as ClientSource, name: c.name, email: c.email, - address: c.address, + street: undefined as string | undefined, + zip: undefined as string | undefined, + city: undefined as string | undefined, + country: undefined as string | undefined, + address: c.address ?? undefined, })); return [...fromContacts, ...fromClients].slice(0, 8); }); @@ -66,7 +75,11 @@ clientSource = s.source; snapshot = { name: s.name, - address: s.address ?? undefined, + street: s.street, + zip: s.zip, + city: s.city, + country: s.country, + address: s.address, email: s.email ?? undefined, }; query = s.name; @@ -111,15 +124,46 @@ {/if} - +
+ + + + +
- +
+ + +
+ +
+ + +
+ +
+ Abweichende Adresse im PDF anzeigen (Freitext-Fallback) +

+ Wird nur verwendet, wenn die strukturierten Felder oben leer sind. Nützlich für Postfächer + / c/o-Adressen, die nicht ins Strasse+PLZ+Ort-Schema passen. +

+ +