From 01ae12ffea7f9568e8135c42bb0237e8df9e8e08 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 7 Sep 2026 21:35:02 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20"davon=20...=20fest=20gebunden"=20war=20?= =?UTF-8?q?irrefuehrend=20=E2=80=93=20und=20nicht=20nachpruefbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Hinweis stand unter der Zeile und begann mit "davon". Die naechste Zahl darueber ist der Ist-Betrag, also las es sich als "von den 17.66 sind 113.72 gebunden" – und wo die Vertraege mehr kosten als geplant, ergab es vollends keinen Sinn. Die Rechnung stimmte; der Satz war falsch. Jetzt steht schlicht da, was es ist: "4 Vertraege kosten CHF 113.72 im Monat". Kein "davon", kein Bezug auf eine Zahl, zu der es keinen gibt. Kostet mehr als geplant, sagt die Zeile das ausdruecklich, statt es der Rechnung zu ueberlassen. Vor allem aber laesst sich die Summe jetzt AUFKLAPPEN: darunter steht jeder einzelne Vertrag mit seinem Monatswert, und bei laengerem Intervall auch der urspruengliche Betrag ("CHF 190.00 jaehrlich · 16.84 EUR"). Eine Summe, die man nicht nachrechnen kann, muss man glauben – und wenn sie einem krumm vorkommt, hilft nur Nachsehen. Die Liste kommt aus derselben Quelle wie die Summe (monatlich_base_cents vom Server). Stimmten die beiden nicht ueberein, waere das ein echter Fehler – und man saehe ihn sofort. Co-Authored-By: Claude Opus 5 --- web/src/finance.css | 32 ++++++++++++++++ web/src/locales/de.json | 10 ++--- web/src/locales/en.json | 10 ++--- web/src/pages/Budget.jsx | 82 +++++++++++++++++++++++++++++++--------- 4 files changed, 106 insertions(+), 28 deletions(-) diff --git a/web/src/finance.css b/web/src/finance.css index 1913b8b..eea367c 100644 --- a/web/src/finance.css +++ b/web/src/finance.css @@ -1892,3 +1892,35 @@ tr.row-dup { color: var(--accent, var(--fg)); background: var(--accent-soft, transparent); } + +/* Die Aufschlüsselung unter einer Budget-Zeile. Aufklappbar, weil eine Summe, + die man nicht nachrechnen kann, geglaubt werden muss. */ +.budget-rechnung-kopf { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + padding: 0; + border: 0; + background: none; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.budget-rechnung-kopf .icon { color: var(--muted); flex: none; } +.budget-rechnung-kopf:hover .muted { color: var(--fg); } +.budget-rechnung-kopf .warn { margin-left: auto; white-space: nowrap; } + +.budget-vertragsliste { + list-style: none; + margin: 2px 0 6px; + padding: 0 0 0 17px; +} +.budget-vertragsliste li { + display: flex; + gap: var(--sp-2); + padding: 1px 0; +} +.budget-vertragsliste li > :first-child { flex: 1; min-width: 0; } +.budget-vertrag-betrag { white-space: nowrap; font-variant-numeric: tabular-nums; } diff --git a/web/src/locales/de.json b/web/src/locales/de.json index afbd298..dc9544c 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -326,12 +326,12 @@ "modusFest": "Festen Betrag setzen", "surchargePlaceholder": "Zuschlag", "contractsWithoutRate": "Für {{waehrungen}} liegt kein Kurs vor. Verträge in dieser Währung fehlen in der Rechnung.", - "fromContracts_one": "{{betrag}} aus einem Vertrag", - "fromContracts_other": "{{betrag}} aus {{count}} Verträgen", - "boundByContracts_one": "davon {{betrag}} fest gebunden (ein Vertrag)", - "boundByContracts_other": "davon {{betrag}} fest gebunden ({{count}} Verträge)", "contractsWithoutCategory_one": "Ein Vertrag über {{betrag}} im Monat hat keine Kategorie und fehlt deshalb im Budget.", - "contractsWithoutCategory_other": "{{count}} Verträge über {{betrag}} im Monat haben keine Kategorie und fehlen deshalb im Budget." + "contractsWithoutCategory_other": "{{count}} Verträge über {{betrag}} im Monat haben keine Kategorie und fehlen deshalb im Budget.", + "contractsCost_one": "Ein Vertrag kostet {{betrag}} im Monat", + "contractsCost_other": "{{count}} Verträge kosten {{betrag}} im Monat", + "moreThanPlanned": "mehr als geplant", + "noRateShort": "kein Kurs" }, "abos": { "title": "Abos & Verträge", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 44f7ca0..d16372b 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -326,12 +326,12 @@ "modusFest": "Set a fixed amount", "surchargePlaceholder": "Extra", "contractsWithoutRate": "No exchange rate for {{waehrungen}}. Contracts in that currency are missing from the calculation.", - "fromContracts_one": "{{betrag}} from one contract", - "fromContracts_other": "{{betrag}} from {{count}} contracts", - "boundByContracts_one": "{{betrag}} of it is committed (one contract)", - "boundByContracts_other": "{{betrag}} of it is committed ({{count}} contracts)", "contractsWithoutCategory_one": "One contract worth {{betrag}} per month has no category and is missing from the budget.", - "contractsWithoutCategory_other": "{{count}} contracts worth {{betrag}} per month have no category and are missing from the budget." + "contractsWithoutCategory_other": "{{count}} contracts worth {{betrag}} per month have no category and are missing from the budget.", + "contractsCost_one": "One contract costs {{betrag}} per month", + "contractsCost_other": "{{count}} contracts cost {{betrag}} per month", + "moreThanPlanned": "more than planned", + "noRateShort": "no rate" }, "abos": { "title": "Subscriptions & contracts", diff --git a/web/src/pages/Budget.jsx b/web/src/pages/Budget.jsx index 5cd0d06..2cf570c 100644 --- a/web/src/pages/Budget.jsx +++ b/web/src/pages/Budget.jsx @@ -44,6 +44,10 @@ export default function Budget() { const [accounts, setAccounts] = useState([]); const [template, setTemplate] = useState({ lines: [], income: [] }); const [incomeModal, setIncomeModal] = useState(null); + // Die Vertraege – fuer die Aufschluesselung unter einer Kategoriezeile. + const [abos, setAbos] = useState([]); + // Welche Kategorie gerade aufgeklappt ist. + const [offen, setOffen] = useState(null); const base = views?.[0]?.base_currency || "EUR"; const fmt = (c) => formatMoney(c, base); @@ -62,8 +66,10 @@ export default function Budget() { } async function loadStamm() { try { - const [g, a] = await Promise.all([api.listCategories(), api.listAccounts()]); - setGroups(g); setAccounts(a); + const [g, a, v] = await Promise.all([ + api.listCategories(), api.listAccounts(), api.listSubscriptions(), + ]); + setGroups(g); setAccounts(a); setAbos(v.subscriptions); } catch (err) { toast(err.message, "error"); } } @@ -118,6 +124,13 @@ export default function Budget() { } catch (err) { toast(err.message, "error"); } } + function vertraegeVon(catId) { + return abos.filter((s) => s.is_active && s.category_id === catId); + } + function zeigeVertraege(catId) { + setOffen(catId); + } + async function saveTemplate(catId, cents) { try { await api.setTemplateCategory(catId, cents); loadTemplate(); } catch (err) { toast(err.message, "error"); } } @@ -224,23 +237,56 @@ export default function Budget() { ? saveZuschlag(v.label, c, cents) : saveMonth(v.label, c.category_id, cents))} /> - {/* Die Rechnung offenlegen. Ohne sie stuende im Feld eine Zahl, - die nicht der Soll-Betrag ist – und niemand wuesste, warum. */} - {berechnet ? ( -
- {t("budget.fromContracts", { - betrag: fmt(c.aus_vertraegen_cents), count: c.vertraege_anzahl, - })} - {" + "}{fmt(c.zuschlag_cents)}{" = "} - {fmt(c.planned_cents)} + {/* Die Rechnung offenlegen – und zwar aufklappbar bis auf den + einzelnen Vertrag. Eine Summe, die man nicht nachrechnen + kann, muss man glauben; und wenn sie einem krumm vorkommt, + hilft nur Nachsehen. */} + {(berechnet || c.vertraege_anzahl > 0) && ( +
+ + {offen === c.category_id && ( +
    + {vertraegeVon(c.category_id).map((s) => ( +
  • + {s.name} + + {s.interval_months > 1 && ( + + {formatMoney(s.amount_cents, s.currency)} + {" "}{t("abos.every." + s.interval_months)}{" · "} + + )} + {s.monatlich_base_cents == null + ? t("budget.noRateShort") + : fmt(s.monatlich_base_cents)} + +
  • + ))} +
+ )}
- ) : c.vertraege_anzahl > 0 ? ( -
- {t("budget.boundByContracts", { - betrag: fmt(c.aus_vertraegen_cents), count: c.vertraege_anzahl, - })} -
- ) : null} + )}
); })}