feat: Eine Ausgabe, die kein Verlust ist
Sparen zaehlt im Budget wie eine Ausgabe – zu Recht, das Geld ist diesen Monat gebunden. Es stand danach aber in Rot da, und wer tausend zuruecklegte statt achthundert, bekam eine Warnung fuer etwas Gutes. Ein Haken an der Kategorie stellt das ab. Zwei Folgen: Der Betrag steht neutral. NICHT gruen: Gruen heisst in Certain ueberall "Geld kommt rein", und CHF 1'010 in Gruen liest sich als Einnahme. Grau sagt genau das Richtige – kein Verlust, kein Gewinn. Und mehr als geplant ist keine Ueberschreitung mehr. Das gilt auch eine Ebene hoeher: Eine Gruppe misst sich an dem, was WIRKLICH zu viel war. Sonst faerbt sich "Finanzen" rot, weil jemand gespart hat, und die echte Ueberschreitung daneben faellt nicht mehr auf. Angezeigt werden weiter die vollen Summen; nur das Urteil laesst das Sparen aus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
15c2a164ec
commit
ff621984dc
@@ -10,12 +10,15 @@ import Modal from "../components/Modal";
|
||||
import EditableAmount from "../components/EditableAmount";
|
||||
import { formatMoney, formatMonthLabel } from "../format";
|
||||
|
||||
function ProgressBar({ actual, planned }) {
|
||||
function ProgressBar({ actual, planned, over }) {
|
||||
const pct = planned > 0 ? Math.min(actual / planned, 1) * 100 : actual > 0 ? 100 : 0;
|
||||
const over = actual > planned;
|
||||
// Ob ueberschritten wurde, sagt der Server – er laesst dabei das Sparen aus,
|
||||
// und das ist eine Rechnung, die hier niemand nachbauen sollte. Ohne Angabe
|
||||
// (andere Aufrufer) bleibt es beim schlichten Vergleich.
|
||||
const rot = over ?? actual > planned;
|
||||
return (
|
||||
<div className="budget-bar">
|
||||
<div className={`budget-bar-fill${over ? " over" : ""}`} style={{ width: `${pct}%` }} />
|
||||
<div className={`budget-bar-fill${rot ? " over" : ""}`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -158,15 +161,15 @@ export default function Budget() {
|
||||
<div className="budget-group-head">
|
||||
<IconChip name={g.icon} color={g.color} size={22} />
|
||||
<strong className="grow ellipsis">{g.name}</strong>
|
||||
<span className={`amount small ${g.over ? "neg" : "muted"}`}>{fmt(g.actual_cents)}</span>
|
||||
<span className={`amount small ${g.over && !g.good ? "neg" : "muted"}`}>{fmt(g.actual_cents)}</span>
|
||||
<span className="muted small"> / {fmt(g.planned_cents)}</span>
|
||||
</div>
|
||||
<ProgressBar actual={g.actual_cents} planned={g.planned_cents} />
|
||||
<ProgressBar actual={g.actual_cents} planned={g.planned_cents} over={g.over} />
|
||||
{g.categories.map((c) => (
|
||||
<div className="budget-crow" key={c.category_id}>
|
||||
<IconChip name={c.icon} color={c.color} size={20} />
|
||||
<span className="grow ellipsis">{c.name}</span>
|
||||
<span className={`amount small ${c.over ? "neg" : "muted"}`}>{fmt(c.actual_cents)}</span>
|
||||
<span className={`amount small ${c.over && !c.good ? "neg" : "muted"}`}>{fmt(c.actual_cents)}</span>
|
||||
<EditableAmount cents={c.planned_cents} onSave={(cents) => saveMonth(v.label, c.category_id, cents)} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -197,8 +197,10 @@ export default function Categories() {
|
||||
groupId,
|
||||
form: editing
|
||||
? { name: editing.name, icon: editing.icon, color: editing.color,
|
||||
excluded_from_stats: !!editing.excluded_from_stats }
|
||||
: { name: "", icon: "more-horizontal", color: "#6b7480", excluded_from_stats: false },
|
||||
excluded_from_stats: !!editing.excluded_from_stats,
|
||||
spending_is_good: !!editing.spending_is_good }
|
||||
: { name: "", icon: "more-horizontal", color: "#6b7480",
|
||||
excluded_from_stats: false, spending_is_good: false },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,7 +213,8 @@ export default function Categories() {
|
||||
else await api.createGroup({ name: form.name.trim(), kind: form.kind, icon: form.icon, color: form.color });
|
||||
} else {
|
||||
const rumpf = { name: form.name.trim(), icon: form.icon, color: form.color,
|
||||
excluded_from_stats: !!form.excluded_from_stats };
|
||||
excluded_from_stats: !!form.excluded_from_stats,
|
||||
spending_is_good: !!form.spending_is_good };
|
||||
if (editing) await api.updateCategory(editing.id, rumpf);
|
||||
else await api.createCategory({ group_id: groupId, ...rumpf });
|
||||
}
|
||||
@@ -339,6 +342,12 @@ export default function Categories() {
|
||||
<span>{t("categories.excludedFromStats")}</span>
|
||||
</label>
|
||||
<p className="muted small" style={{ marginTop: -6 }}>{t("categories.excludedHint")}</p>
|
||||
<label className="checkline">
|
||||
<input type="checkbox" checked={!!modal.form.spending_is_good}
|
||||
onChange={(e) => setModal({ ...modal, form: { ...modal.form, spending_is_good: e.target.checked } })} />
|
||||
<span>{t("categories.spendingIsGood")}</span>
|
||||
</label>
|
||||
<p className="muted small" style={{ marginTop: -6 }}>{t("categories.spendingIsGoodHint")}</p>
|
||||
</>
|
||||
)}
|
||||
<label>
|
||||
|
||||
Reference in New Issue
Block a user