fix: "davon ... fest gebunden" war irrefuehrend – und nicht nachpruefbar

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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-07 21:35:02 +02:00
co-authored by Claude Opus 5
parent ab5d18e0d0
commit 01ae12ffea
4 changed files with 106 additions and 28 deletions
+64 -18
View File
@@ -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))} />
</div>
{/* Die Rechnung offenlegen. Ohne sie stuende im Feld eine Zahl,
die nicht der Soll-Betrag ist – und niemand wuesste, warum. */}
{berechnet ? (
<div className="budget-rechnung muted small">
{t("budget.fromContracts", {
betrag: fmt(c.aus_vertraegen_cents), count: c.vertraege_anzahl,
})}
{" + "}{fmt(c.zuschlag_cents)}{" = "}
<strong>{fmt(c.planned_cents)}</strong>
{/* 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) && (
<div className="budget-rechnung small">
<button type="button" className="budget-rechnung-kopf"
onClick={() => zeigeVertraege(
offen === c.category_id ? null : c.category_id)}>
<Icon name={offen === c.category_id ? "chevron-down" : "chevron-right"}
size={13} />
<span className="muted">
{t("budget.contractsCost", {
betrag: fmt(c.aus_vertraegen_cents), count: c.vertraege_anzahl,
})}
{berechnet && (
<>
{" + "}{fmt(c.zuschlag_cents)}{" = "}
<strong>{fmt(c.planned_cents)}</strong>
</>
)}
</span>
{/* Mehr gebunden als geplant ist keine Rechenpanne,
sondern eine Aussage – aber nur, wenn sie dasteht. */}
{!berechnet && c.aus_vertraegen_cents > c.planned_cents && (
<span className="muted warn">{t("budget.moreThanPlanned")}</span>
)}
</button>
{offen === c.category_id && (
<ul className="budget-vertragsliste muted">
{vertraegeVon(c.category_id).map((s) => (
<li key={s.id}>
<span className="ellipsis">{s.name}</span>
<span className="budget-vertrag-betrag">
{s.interval_months > 1 && (
<span className="muted">
{formatMoney(s.amount_cents, s.currency)}
{" "}{t("abos.every." + s.interval_months)}{" · "}
</span>
)}
{s.monatlich_base_cents == null
? t("budget.noRateShort")
: fmt(s.monatlich_base_cents)}
</span>
</li>
))}
</ul>
)}
</div>
) : c.vertraege_anzahl > 0 ? (
<div className="budget-rechnung muted small">
{t("budget.boundByContracts", {
betrag: fmt(c.aus_vertraegen_cents), count: c.vertraege_anzahl,
})}
</div>
) : null}
)}
</div>
);
})}