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 && ( + + )}
- ) : c.vertraege_anzahl > 0 ? ( -
- {t("budget.boundByContracts", { - betrag: fmt(c.aus_vertraegen_cents), count: c.vertraege_anzahl, - })} -
- ) : null} + )}
); })}