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:
co-authored by
Claude Opus 5
parent
ab5d18e0d0
commit
01ae12ffea
@@ -1892,3 +1892,35 @@ tr.row-dup {
|
|||||||
color: var(--accent, var(--fg));
|
color: var(--accent, var(--fg));
|
||||||
background: var(--accent-soft, transparent);
|
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; }
|
||||||
|
|||||||
@@ -326,12 +326,12 @@
|
|||||||
"modusFest": "Festen Betrag setzen",
|
"modusFest": "Festen Betrag setzen",
|
||||||
"surchargePlaceholder": "Zuschlag",
|
"surchargePlaceholder": "Zuschlag",
|
||||||
"contractsWithoutRate": "Für {{waehrungen}} liegt kein Kurs vor. Verträge in dieser Währung fehlen in der Rechnung.",
|
"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_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": {
|
"abos": {
|
||||||
"title": "Abos & Verträge",
|
"title": "Abos & Verträge",
|
||||||
|
|||||||
@@ -326,12 +326,12 @@
|
|||||||
"modusFest": "Set a fixed amount",
|
"modusFest": "Set a fixed amount",
|
||||||
"surchargePlaceholder": "Extra",
|
"surchargePlaceholder": "Extra",
|
||||||
"contractsWithoutRate": "No exchange rate for {{waehrungen}}. Contracts in that currency are missing from the calculation.",
|
"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_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": {
|
"abos": {
|
||||||
"title": "Subscriptions & contracts",
|
"title": "Subscriptions & contracts",
|
||||||
|
|||||||
+60
-14
@@ -44,6 +44,10 @@ export default function Budget() {
|
|||||||
const [accounts, setAccounts] = useState([]);
|
const [accounts, setAccounts] = useState([]);
|
||||||
const [template, setTemplate] = useState({ lines: [], income: [] });
|
const [template, setTemplate] = useState({ lines: [], income: [] });
|
||||||
const [incomeModal, setIncomeModal] = useState(null);
|
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 base = views?.[0]?.base_currency || "EUR";
|
||||||
const fmt = (c) => formatMoney(c, base);
|
const fmt = (c) => formatMoney(c, base);
|
||||||
@@ -62,8 +66,10 @@ export default function Budget() {
|
|||||||
}
|
}
|
||||||
async function loadStamm() {
|
async function loadStamm() {
|
||||||
try {
|
try {
|
||||||
const [g, a] = await Promise.all([api.listCategories(), api.listAccounts()]);
|
const [g, a, v] = await Promise.all([
|
||||||
setGroups(g); setAccounts(a);
|
api.listCategories(), api.listAccounts(), api.listSubscriptions(),
|
||||||
|
]);
|
||||||
|
setGroups(g); setAccounts(a); setAbos(v.subscriptions);
|
||||||
} catch (err) { toast(err.message, "error"); }
|
} catch (err) { toast(err.message, "error"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +124,13 @@ export default function Budget() {
|
|||||||
} catch (err) { toast(err.message, "error"); }
|
} 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) {
|
async function saveTemplate(catId, cents) {
|
||||||
try { await api.setTemplateCategory(catId, cents); loadTemplate(); } catch (err) { toast(err.message, "error"); }
|
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)
|
? saveZuschlag(v.label, c, cents)
|
||||||
: saveMonth(v.label, c.category_id, cents))} />
|
: saveMonth(v.label, c.category_id, cents))} />
|
||||||
</div>
|
</div>
|
||||||
{/* Die Rechnung offenlegen. Ohne sie stuende im Feld eine Zahl,
|
{/* Die Rechnung offenlegen – und zwar aufklappbar bis auf den
|
||||||
die nicht der Soll-Betrag ist – und niemand wuesste, warum. */}
|
einzelnen Vertrag. Eine Summe, die man nicht nachrechnen
|
||||||
{berechnet ? (
|
kann, muss man glauben; und wenn sie einem krumm vorkommt,
|
||||||
<div className="budget-rechnung muted small">
|
hilft nur Nachsehen. */}
|
||||||
{t("budget.fromContracts", {
|
{(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,
|
betrag: fmt(c.aus_vertraegen_cents), count: c.vertraege_anzahl,
|
||||||
})}
|
})}
|
||||||
|
{berechnet && (
|
||||||
|
<>
|
||||||
{" + "}{fmt(c.zuschlag_cents)}{" = "}
|
{" + "}{fmt(c.zuschlag_cents)}{" = "}
|
||||||
<strong>{fmt(c.planned_cents)}</strong>
|
<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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user