feat: Kontostand aufschluesseln – und der Angleich-Knopf bleibt
Zwei Sachen aus derselben Wurzel: Wenn ein Saldo nicht stimmt, sieht man nicht, warum, und kann auch nichts dagegen tun. Ein Klick auf den Kontostand zeigt jetzt die Rechnung: Anfangsbestand, Einnahmen, Ausgaben, Uebertraege raus und rein – jeweils mit der Anzahl der Buchungen – und was daraus wird. Darunter der Saldo der Bank und, einzeln aufgefuehrt, alle Uebertraege des Kontos. Genau die sind die haeufigste Ursache und in einer langen Buchungsliste am schwersten zu ueberblicken. Rechnung und Kontenliste kommen aus EINER Quelle: ``account_balance_cents`` liest jetzt aus der Aufschluesselung. Sonst koennte die Anzeige etwas anderes behaupten als der Saldo, und man suchte den Fehler im Falschen. Ausserdem war der Knopf "Saldo der Bank uebernehmen" verschwunden, sobald die Bank nur einen verfuegbaren Saldo meldet. Das war zu weit gedacht: Nicht vergleichbar heisst nicht, dass man nicht angleichen duerfte. Der Knopf ist zurueck, und die Nachfrage sagt in dem Fall, worauf man sich einlaesst – der verfuegbare Saldo enthaelt Vorgemerktes, das spaeter noch als Buchung kommt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,8 @@ function TransferKeywords({ accounts }) {
|
||||
const [amount, setAmount] = useState("");
|
||||
const [target, setTarget] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
// Aufgeschlüsselter Saldo je Konto, sobald man danach fragt.
|
||||
const [aufschluss, setAufschluss] = useState({});
|
||||
// Was beim nachträglichen Anwenden übersprungen wurde, samt Grund.
|
||||
const [uebersprungen, setUebersprungen] = useState([]);
|
||||
const confirm = useConfirm();
|
||||
@@ -179,11 +181,24 @@ export default function Accounts() {
|
||||
const [editing, setEditing] = useState(null); // Account-Objekt oder null
|
||||
const [form, setForm] = useState(null); // Formularzustand oder null (geschlossen)
|
||||
const [busy, setBusy] = useState(false);
|
||||
// Aufgeschlüsselter Saldo je Konto, sobald man danach fragt.
|
||||
const [aufschluss, setAufschluss] = useState({});
|
||||
|
||||
async function aufschluesseln(a) {
|
||||
if (aufschluss[a.id]) { setAufschluss((s) => ({ ...s, [a.id]: null })); return; }
|
||||
try {
|
||||
const daten = await api.balanceBreakdown(a.id);
|
||||
setAufschluss((s) => ({ ...s, [a.id]: daten }));
|
||||
} catch (e) {
|
||||
toast(e.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function saldoUebernehmen(a) {
|
||||
if (!(await confirm({
|
||||
title: t("accounts.adoptBankBalance"),
|
||||
message: t("accounts.adoptConfirm", {
|
||||
message: t(a.bank_balance_vergleichbar
|
||||
? "accounts.adoptConfirm" : "accounts.adoptConfirmAvailable", {
|
||||
bank: formatMoney(a.bank_balance_cents, a.currency),
|
||||
diff: formatMoney(a.balance_cents - a.bank_balance_cents, a.currency),
|
||||
}),
|
||||
@@ -309,7 +324,8 @@ export default function Accounts() {
|
||||
</div>
|
||||
<div className="account-list">
|
||||
{accounts.map((a, i) => (
|
||||
<div className="account-row" key={a.id}>
|
||||
<div className={`account-row${aufschluss[a.id] ? " mit-aufschluss" : ""}`}
|
||||
key={a.id}>
|
||||
<IconChip name={a.icon} color={a.color} />
|
||||
<div className="named-text grow">
|
||||
<div><strong>{a.name}</strong></div>
|
||||
@@ -333,7 +349,17 @@ export default function Accounts() {
|
||||
<span className="muted">
|
||||
{t("accounts.bankBalanceAvailable", {
|
||||
bank: formatMoney(a.bank_balance_cents, a.currency),
|
||||
})}
|
||||
})}{" "}
|
||||
{/* Nicht vergleichbar heisst nicht, dass man nicht
|
||||
angleichen dürfte – nur, dass Certain es nicht
|
||||
von sich aus als Abweichung meldet. Die
|
||||
Entscheidung bleibt beim Nutzer. */}
|
||||
{a.bank_balance_cents !== a.balance_cents && (
|
||||
<button type="button" className="btn sm"
|
||||
onClick={() => saldoUebernehmen(a)}>
|
||||
{t("accounts.adoptBankBalance")}
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
) : a.bank_balance_cents === a.balance_cents ? (
|
||||
<span className="saldo-ok">
|
||||
@@ -359,8 +385,14 @@ export default function Accounts() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Money cents={a.balance_cents} currency={a.currency}
|
||||
baseCents={a.balance_base_cents} baseCurrency={baseCurrency} />
|
||||
<button type="button" className="btn sm ghost" title={t("accounts.breakdown")}
|
||||
onClick={() => aufschluesseln(a)}>
|
||||
<Money cents={a.balance_cents} currency={a.currency}
|
||||
baseCents={a.balance_base_cents} baseCurrency={baseCurrency} />
|
||||
</button>
|
||||
{aufschluss[a.id] && (
|
||||
<Aufschluesselung daten={aufschluss[a.id]} t={t} />
|
||||
)}
|
||||
<div className="row-actions">
|
||||
<button className="btn sm ghost" title={t("accounts.moveUp")}
|
||||
disabled={i === 0} onClick={() => verschiebe(i, -1)}>
|
||||
@@ -488,3 +520,61 @@ export default function Accounts() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Woraus sich ein Kontostand zusammensetzt. Stimmt er nicht, liegt es fast
|
||||
// immer an einer dieser Zahlen oder an einem Übertrag, der zu viel oder zu
|
||||
// wenig verschiebt – dann sieht man hier sofort, welcher Teil nicht passt.
|
||||
function Aufschluesselung({ daten, t }) {
|
||||
const { teile, uebertraege, bank, waehrung } = daten;
|
||||
const zeile = (schluessel, vorzeichen) => {
|
||||
const teil = teile[schluessel];
|
||||
return (
|
||||
<div className="aufschluss-zeile" key={schluessel}>
|
||||
<span>
|
||||
{t(`accounts.part.${schluessel}`)}
|
||||
{teil.anzahl > 0 && <span className="muted"> ({teil.anzahl})</span>}
|
||||
</span>
|
||||
<span className="nowrap">
|
||||
{vorzeichen}{formatMoney(teil.cents, waehrung)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className="aufschluss">
|
||||
{zeile("anfangsbestand", "")}
|
||||
{zeile("einnahmen", "+")}
|
||||
{zeile("ausgaben", "−")}
|
||||
{zeile("uebertraege_raus", "−")}
|
||||
{zeile("uebertraege_rein", "+")}
|
||||
<div className="aufschluss-zeile aufschluss-summe">
|
||||
<span>{t("accounts.part.saldo")}</span>
|
||||
<span className="nowrap">{formatMoney(teile.saldo.cents, waehrung)}</span>
|
||||
</div>
|
||||
{bank.cents != null && (
|
||||
<div className="aufschluss-zeile muted">
|
||||
<span>{t(bank.vergleichbar ? "accounts.part.bankGebucht"
|
||||
: "accounts.part.bankVerfuegbar")}</span>
|
||||
<span className="nowrap">{formatMoney(bank.cents, waehrung)}</span>
|
||||
</div>
|
||||
)}
|
||||
{uebertraege.length > 0 && (
|
||||
<div className="aufschluss-uebertraege">
|
||||
<div className="muted small">{t("accounts.part.legs")}</div>
|
||||
{uebertraege.map((u) => (
|
||||
<div className="aufschluss-zeile" key={u.id}>
|
||||
<span className="ellipsis">
|
||||
{formatDate(u.datum)} · {u.von} → {u.nach}
|
||||
{u.text ? ` · ${u.text}` : ""}
|
||||
</span>
|
||||
<span className="nowrap">
|
||||
{u.raus ? "−" : "+"}{formatMoney(u.cents, waehrung)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user