feat: Geld, an das man nicht herankommt, zaehlt nicht mehr als verfuegbar
Eine Saeule 3a ist gesperrt – bis zur Rente, zur Selbstaendigkeit oder zur Auswanderung. Bisher zaehlte ihr Stand in dieselbe Summe wie das Girokonto, und die Antwort auf "wie viel habe ich" war um genau diesen Betrag zu hoch. Das Geld steht da und ist doch nicht da. Konten lassen sich jetzt als gebunden kennzeichnen. "Gesamt" zeigt dann nur noch, was wirklich ausgegeben werden kann; darunter steht "zusaetzlich CHF 2'004.61 gebunden" – aufklappbar, mit jedem Konto einzeln. Eine Summe, die man nicht nachrechnen kann, muss man glauben. Verschwiegen wird nichts: Der Saldo bleibt am Konto stehen, und Vermoegensverlauf wie Nettovermoegen rechnen weiter damit. Es nicht ausgeben zu koennen heisst nicht, es nicht zu besitzen – das ist der ganze Unterschied, und ein Test haelt ihn fest. Ein eigenes Kennzeichen, keine Ableitung aus dem Kontotyp: Ein frei verkaeufliches Depot und eine gesperrte Saeule 3a kommen beide als "depot" herein. Am Typ laesst sich das nicht ablesen – am ausdruecklichen Eintrag schon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
35c5d2c84a
commit
76dbd3b481
@@ -53,6 +53,14 @@ function AccountBalances() {
|
||||
formatMoney allein faerbt nicht – das Minus stuende still da. */}
|
||||
<strong><Money cents={data.total} currency={data.currency} /></strong>
|
||||
</div>
|
||||
{/* Gesperrtes Geld steht daneben, nicht darin – sonst beantwortet die
|
||||
Summe eine andere Frage, als der Nutzer stellt. */}
|
||||
{data.gebunden > 0 && (
|
||||
<div className="dash-line muted small">
|
||||
<span className="grow">{t("cards.locked")}</span>
|
||||
{formatMoney(data.gebunden, data.currency)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1189,6 +1189,25 @@ tr.row-dup {
|
||||
}
|
||||
.rahmen-balken.ueber .rahmen-fuellung { background: var(--warn); }
|
||||
|
||||
/* ---------- Gebundenes Geld ----------
|
||||
Steht unter „Gesamt", nicht darin. Aufklappbar, weil eine Summe, die man
|
||||
nicht nachrechnen kann, man glauben muss. */
|
||||
.gebunden-zeile {
|
||||
padding: 0 var(--sp-4) var(--sp-2);
|
||||
margin-top: -6px;
|
||||
}
|
||||
.gebunden-zeile summary {
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.gebunden-posten {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
padding: 3px 0 3px var(--sp-3);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* ---------- Übertrags-Vorschläge ---------- */
|
||||
.uebertrag-vorschlag {
|
||||
padding: var(--sp-3) 0;
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
"incomeExpense": "Einnahmen & Ausgaben",
|
||||
"recent": "Letzte Buchungen",
|
||||
"total": "Gesamt",
|
||||
"locked": "zusätzlich gebunden",
|
||||
"planned": "geplant",
|
||||
"spent": "ausgegeben",
|
||||
"assets": "Vermögen",
|
||||
@@ -185,6 +186,11 @@
|
||||
"creditLimitHint": "Was die Karte ausgeben darf – kein Guthaben. Der Kontostand gehört nicht hierher: Eine abbezahlte Karte steht auf 0, jede Ausgabe drückt sie ins Minus, die Rechnung holt sie zurück.",
|
||||
"creditLimitLeft": "noch {{left}} von {{limit}} frei",
|
||||
"creditLimitExceeded": "{{over}} über dem Rahmen von {{limit}}",
|
||||
"locked": "Geld ist gebunden – nicht verfügbar",
|
||||
"lockedHint": "Für Konten, an die man nicht herankommt (z. B. Säule 3a: gesperrt bis zur Rente, Selbstständigkeit oder Auswanderung). Der Betrag zählt weiter zum Vermögen, aber nicht zu „Gesamt“ – denn ausgeben lässt er sich nicht.",
|
||||
"lockedBadge": "gebunden",
|
||||
"lockedTotal_one": "zusätzlich {{value}} gebunden ({{count}} Konto)",
|
||||
"lockedTotal_other": "zusätzlich {{value}} gebunden ({{count}} Konten)",
|
||||
"savingsCategory": "Sparen gegen Kategorie",
|
||||
"savingsCategoryNone": "Kein Sparziel",
|
||||
"savingsCategoryHint": "Überweisungen auf dieses Konto zählen im Budget als Sparen gegen diese Kategorie – auch dann, wenn sie über die Bank als zwei verknüpfte Buchungen hereinkommen. Einzelne Buchungen lassen sich davon ausnehmen.",
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
"incomeExpense": "Income & expenses",
|
||||
"recent": "Recent transactions",
|
||||
"total": "Total",
|
||||
"locked": "plus locked",
|
||||
"planned": "planned",
|
||||
"spent": "spent",
|
||||
"assets": "Net worth",
|
||||
@@ -185,6 +186,11 @@
|
||||
"creditLimitHint": "What the card may spend — not money you have. The balance does not belong here: a paid-off card sits at 0, every purchase pushes it negative, and the bill brings it back.",
|
||||
"creditLimitLeft": "{{left}} of {{limit}} left",
|
||||
"creditLimitExceeded": "{{over}} over the {{limit}} limit",
|
||||
"locked": "Money is locked — not available",
|
||||
"lockedHint": "For accounts you cannot access (e.g. a Swiss pillar 3a: locked until retirement, self-employment or emigration). The amount still counts towards your wealth, but not towards “Total” — because you cannot spend it.",
|
||||
"lockedBadge": "locked",
|
||||
"lockedTotal_one": "plus {{value}} locked ({{count}} account)",
|
||||
"lockedTotal_other": "plus {{value}} locked ({{count}} accounts)",
|
||||
"savingsCategory": "Count savings towards",
|
||||
"savingsCategoryNone": "Not a savings account",
|
||||
"savingsCategoryHint": "Transfers into this account count as savings against this category in the budget – including those that arrive from the bank as two linked transactions. Individual transactions can be excluded.",
|
||||
|
||||
@@ -34,6 +34,7 @@ function emptyForm() {
|
||||
opening_balance: "",
|
||||
opening_date: "",
|
||||
credit_limit: "",
|
||||
is_locked: false,
|
||||
savings_category_id: "",
|
||||
identifiers: [],
|
||||
};
|
||||
@@ -260,6 +261,7 @@ export default function Accounts() {
|
||||
opening_date: acc.opening_date || "",
|
||||
credit_limit: acc.credit_limit_cents != null
|
||||
? centsToInput(acc.credit_limit_cents) : "",
|
||||
is_locked: !!acc.is_locked,
|
||||
savings_category_id: acc.savings_category_id ? String(acc.savings_category_id) : "",
|
||||
identifiers: acc.identifiers.map((i) => ({ kind: i.kind, value: i.value })),
|
||||
});
|
||||
@@ -289,6 +291,8 @@ export default function Accounts() {
|
||||
// Girokonto macht, soll den Rahmen nicht unsichtbar mitschleppen.
|
||||
credit_limit_cents: form.type === "kreditkarte"
|
||||
? (parseAmountToCents(form.credit_limit) ?? null) : null,
|
||||
// Eine Kreditkarte hat kein Guthaben, das gebunden sein koennte.
|
||||
is_locked: form.type === "kreditkarte" ? false : !!form.is_locked,
|
||||
savings_category_id: form.savings_category_id ? Number(form.savings_category_id) : null,
|
||||
identifiers: form.identifiers
|
||||
.filter((i) => i.value.trim())
|
||||
@@ -336,9 +340,17 @@ export default function Accounts() {
|
||||
// Archivierte bleiben draussen – das Backend rechnet ebenso
|
||||
// (balances.net_worth_cents, dashboard.py). Zwei Zahlen fuer dieselbe Sache
|
||||
// sind eine Zahl zu viel.
|
||||
const total = accounts
|
||||
.filter((a) => !a.is_archived)
|
||||
//
|
||||
// Gesperrtes Geld ebenfalls: „Gesamt" beantwortet die Frage „wie viel habe
|
||||
// ich zur Verfuegung", und eine Saeule 3a gehoert darauf nicht. Es wird
|
||||
// daneben ausgewiesen, nicht verschwiegen – es gehoert dem Nutzer, nur eben
|
||||
// noch nicht.
|
||||
const aktiv = accounts.filter((a) => !a.is_archived);
|
||||
const total = aktiv
|
||||
.filter((a) => !a.is_locked)
|
||||
.reduce((s, a) => s + a.balance_base_cents, 0);
|
||||
const gebundene = aktiv.filter((a) => a.is_locked);
|
||||
const gebunden = gebundene.reduce((s, a) => s + a.balance_base_cents, 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -357,6 +369,25 @@ export default function Accounts() {
|
||||
<span className="muted">{t("accounts.total")}</span>
|
||||
<strong><Money cents={total} currency={baseCurrency} /></strong>
|
||||
</div>
|
||||
{/* Aufklappbar, weil eine Summe, die man nicht nachrechnen kann, man
|
||||
glauben muss. Darunter steht jedes gebundene Konto einzeln. */}
|
||||
{gebundene.length > 0 && (
|
||||
<details className="gebunden-zeile">
|
||||
<summary className="muted small">
|
||||
{t("accounts.lockedTotal", {
|
||||
value: formatMoney(gebunden, baseCurrency),
|
||||
count: gebundene.length,
|
||||
})}
|
||||
</summary>
|
||||
{gebundene.map((a) => (
|
||||
<div className="gebunden-posten" key={a.id}>
|
||||
<span className="grow ellipsis">{a.name}</span>
|
||||
<Money cents={a.balance_cents} currency={a.currency}
|
||||
baseCents={a.balance_base_cents} baseCurrency={baseCurrency} />
|
||||
</div>
|
||||
))}
|
||||
</details>
|
||||
)}
|
||||
<div className="account-list">
|
||||
{accounts.map((a, i) => (
|
||||
<div className={`account-row${aufschluss[a.id] ? " mit-aufschluss" : ""}`}
|
||||
@@ -370,6 +401,12 @@ export default function Accounts() {
|
||||
Certain eine Auszugszeile diesem Konto zuordnet. */}
|
||||
{a.identifiers.length > 0
|
||||
&& ` · ${t("accounts.identShort", { count: a.identifiers.length })}`}
|
||||
{a.is_locked && (
|
||||
<span className="badge" style={{ marginLeft: 6 }}
|
||||
title={t("accounts.lockedHint")}>
|
||||
{t("accounts.lockedBadge")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Kreditrahmen account={a} t={t} />
|
||||
{/* Bei angebundenen Konten der Saldo, den die Bank fuehrt.
|
||||
@@ -515,6 +552,20 @@ export default function Accounts() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Nicht aus dem Kontotyp abgeleitet: Ein frei verkaeufliches Depot
|
||||
und eine gesperrte Saeule 3a kommen beide als „depot" herein.
|
||||
Bei einer Kreditkarte gibt es nichts zu binden. */}
|
||||
{form.type !== "kreditkarte" && (
|
||||
<>
|
||||
<label className="checkline">
|
||||
<input type="checkbox" checked={form.is_locked}
|
||||
onChange={(e) => setForm({ ...form, is_locked: e.target.checked })} />
|
||||
<span>{t("accounts.locked")}</span>
|
||||
</label>
|
||||
<p className="muted small">{t("accounts.lockedHint")}</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<label>
|
||||
<span>{t("accounts.savingsCategory")}</span>
|
||||
<CategorySelect
|
||||
|
||||
Reference in New Issue
Block a user