feat: Einer Buchung sieht man an, ob sie ein Uebertrag ist – und was sie war
Drei Dinge, die zusammengehoeren. 1. Uebertraege sind als solche lesbar. Stand nur eine Seite in der Liste – weil nach einem Konto gefiltert wird oder die andere nicht mitgeladen war – trug die Zeile den Rohtext der Bank als Ueberschrift. "To CHF MB:8f33c0aa-9c92-456b-a6fe-b64a858a019c" sah aus wie eine Zahlung an jemanden; die Verknuepfung stand klein darunter. Jetzt steht oben, zwischen WELCHEN Konten das Geld lief, in beiden Faellen gleich, und daneben eine Marke "Transfer". Ein Uebertrag ist weder Ausgabe noch Einnahme – das gehoert in die erste Zeile, nicht in eine Fussnote. 2. Notiz je Buchung. Das Feld gab es im Modell seit jeher, die Oberflaeche bot es nie an. Die Kategorie sagt, in welche Schublade etwas gehoert – nicht, WAS es war. "Amazon" kann Einrichtung sein, Werkzeug oder ein Geschenk, und in drei Monaten weiss das niemand mehr. Die Notiz steht in der Liste und ist mitdurchsuchbar: Wer sie schreibt, will spaeter danach suchen. 3. "Ignorieren" ist keine Einbahnstrasse mehr. Ein abgelehntes Paar blieb bei jedem weiteren Suchlauf stumm, und nichts verriet, dass es je einen Vorschlag gab. Wer einmal falsch klickte, suchte danach einen Fehler in der Erkennung, den es nicht gab. Die abgelehnten Paare lassen sich jetzt ansehen und zurueckholen. Und wenn die Vorschlaege nicht geladen werden koennen, wird das gesagt statt verschluckt – sonst sieht die Liste aus, als gaebe es nichts zu verbinden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
76dbd3b481
commit
eb72f0cb27
@@ -31,6 +31,7 @@ function emptyForm() {
|
||||
to_amount: "",
|
||||
payee: "",
|
||||
description: "",
|
||||
notes: "",
|
||||
budget_as_expense: true,
|
||||
budget_category_id: "",
|
||||
// Leer heisst: aus dem Datum. Der Normalfall, und er bleibt es.
|
||||
@@ -83,7 +84,12 @@ export default function Transactions() {
|
||||
api.transactionLinks().catch(() => ({})),
|
||||
// Nur fragen, nichts tun: Beim Anschauen der Liste darf nichts
|
||||
// verknüpft werden.
|
||||
api.transferSuggestions().catch(() => ({ vorschlaege: [] })),
|
||||
api.transferSuggestions().catch((e) => {
|
||||
// Stillschweigen waere hier das Schlimmste: Die Liste saehe aus, als
|
||||
// gaebe es nichts zu verbinden.
|
||||
toast(t("transfers.suggestionsFailed", { fehler: e.message }), "error");
|
||||
return { vorschlaege: [] };
|
||||
}),
|
||||
]);
|
||||
setTxns(liste);
|
||||
setLinks(verkn);
|
||||
@@ -211,6 +217,7 @@ export default function Transactions() {
|
||||
to_amount: tx.to_amount_cents ? centsToInput(tx.to_amount_cents) : "",
|
||||
payee: tx.payee || "",
|
||||
description: tx.description || "",
|
||||
notes: tx.notes || "",
|
||||
budget_as_expense: tx.budget_treatment !== "neutral",
|
||||
budget_category_id: tx.budget_category_id ? String(tx.budget_category_id) : "",
|
||||
// Nur der von Hand gesetzte Monat gehoert ins Formular. Leer heisst:
|
||||
@@ -238,6 +245,7 @@ export default function Transactions() {
|
||||
amount_cents: cents,
|
||||
payee: form.payee.trim() || null,
|
||||
description: form.description.trim() || null,
|
||||
notes: form.notes.trim() || null,
|
||||
budget_period: form.budget_period || null,
|
||||
};
|
||||
if (form.type === "transfer") {
|
||||
@@ -295,11 +303,31 @@ export default function Transactions() {
|
||||
const von = accountById[tx.account_id]?.name || "?";
|
||||
const nach = accountById[gegen.account_id]?.name || "?";
|
||||
return {
|
||||
icon: "arrow-left-right", color: "#6b7480",
|
||||
icon: "arrow-left-right", color: "#6b7480", uebertrag: true,
|
||||
title: `${von} → ${nach}`,
|
||||
sub: tx.payee || tx.description || t("transactions.transfer"),
|
||||
};
|
||||
}
|
||||
// Verknuepft, aber die Gegenseite ist nicht in der Liste (gefiltert oder
|
||||
// auf einer anderen Seite). Dieselbe Darstellung wie beim Paar: Oben steht,
|
||||
// zwischen WELCHEN Konten das Geld lief, und nicht ein Empfaengertext, den
|
||||
// die Bank vergeben hat.
|
||||
//
|
||||
// Vorher stand hier der Rohtext als Titel und die Verknuepfung nur klein
|
||||
// darunter – „To CHF MB:8f33c0aa-9c92-456b-a6fe-b64a858a019c" sah aus wie
|
||||
// eine Zahlung an jemanden.
|
||||
const link = links[String(tx.id)];
|
||||
if (link) {
|
||||
const eigen = accountById[tx.account_id]?.name || "?";
|
||||
const anderes = accountById[link.other_account_id]?.name
|
||||
|| t("transfers.linked");
|
||||
return {
|
||||
icon: "arrow-left-right", color: "#6b7480", uebertrag: true,
|
||||
title: link.richtung === "raus"
|
||||
? `${eigen} → ${anderes}` : `${anderes} → ${eigen}`,
|
||||
sub: tx.payee || tx.description || "",
|
||||
};
|
||||
}
|
||||
if (tx.type === "transfer") {
|
||||
const from = accountById[tx.account_id]?.name || "?";
|
||||
const to = accountById[tx.to_account_id]?.name || "?";
|
||||
@@ -330,7 +358,9 @@ export default function Transactions() {
|
||||
{
|
||||
// Wofür die Zahlung war – danach sucht man in der Liste.
|
||||
key: "text", header: t("transactions.subject"), grow: true, min: 190,
|
||||
filterText: (tx) => rowLabel(tx).title,
|
||||
// Auch die Notiz durchsuchbar: Wer sie schreibt, will spaeter danach
|
||||
// suchen koennen („Geschenk", „beruflich").
|
||||
filterText: (tx) => [rowLabel(tx).title, tx.notes].filter(Boolean).join(" "),
|
||||
sortValue: (tx) => rowLabel(tx).title,
|
||||
render: (tx) => {
|
||||
const l = rowLabel(tx);
|
||||
@@ -343,6 +373,15 @@ export default function Transactions() {
|
||||
stapeln macht die Zeilen hoch und den Blick eng. */}
|
||||
<div className="titelzeile">
|
||||
<span className="ellipsis">{l.title}</span>
|
||||
{/* Ein Uebertrag ist keine Ausgabe und keine Einnahme. Das
|
||||
gehoert in die erste Zeile, nicht in eine Fussnote. */}
|
||||
{l.uebertrag && (
|
||||
<span className="badge accent herkunft"
|
||||
title={t("transfers.linkedHint")}>
|
||||
<Icon name="arrow-left-right" size={11} />
|
||||
{t("transactions.transfer")}
|
||||
</span>
|
||||
)}
|
||||
<span className="badge herkunft">{t(`todo.from.${tx.herkunft}`, {
|
||||
defaultValue: tx.herkunft,
|
||||
})}</span>
|
||||
@@ -358,17 +397,16 @@ export default function Transactions() {
|
||||
)}
|
||||
</div>
|
||||
{l.sub && <div className="named-sub ellipsis">{l.sub}</div>}
|
||||
{/* Verknüpft heisst: zählt nicht als Ausgabe oder Einnahme.
|
||||
Ohne das Gegenkonto sagt „Übertrag" nichts. */}
|
||||
{/* Bei einer Paar-Zeile steht das Gegenkonto schon im Titel –
|
||||
die Marke waere dort nur eine Wiederholung. */}
|
||||
{links[String(tx.id)] && !paarZu[tx.id] && (
|
||||
<div className="named-sub ellipsis uebertrag-marke">
|
||||
<Icon name="arrow-left-right" size={12} />{" "}
|
||||
{accountById[links[String(tx.id)].other_account_id]?.name
|
||||
|| t("transfers.linked")}
|
||||
{/* Von Hand geschrieben und deshalb aussagekraeftiger als jeder
|
||||
Banktext: „Amazon" kann Einrichtung sein, Werkzeug oder ein
|
||||
Geschenk. */}
|
||||
{tx.notes && (
|
||||
<div className="named-sub ellipsis buchungsnotiz" title={tx.notes}>
|
||||
<Icon name="edit" size={11} /> {tx.notes}
|
||||
</div>
|
||||
)}
|
||||
{/* Die fruehere Marke mit dem Gegenkonto ist entfallen: Das
|
||||
Gegenkonto steht jetzt im Titel, wo man es sucht. */}
|
||||
{/* Nachträglich erkannt: Auf einem anderen Konto steht der
|
||||
gleiche Betrag am selben Tag in die andere Richtung. Fragen
|
||||
statt tun – und die Antwort merken. */}
|
||||
@@ -657,6 +695,16 @@ export default function Transactions() {
|
||||
<span>{t("transactions.description")}</span>
|
||||
<input value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
||||
</label>
|
||||
{/* Die Kategorie sagt, in welche Schublade es gehoert. Die Notiz
|
||||
sagt, WAS es war – „Amazon" kann Einrichtung sein, Werkzeug
|
||||
oder ein Geschenk, und in drei Monaten weiss das niemand mehr.
|
||||
Freier Text, weil sich das nicht in Schubladen fassen laesst. */}
|
||||
<label>
|
||||
<span>{t("transactions.notes")}</span>
|
||||
<textarea rows={2} value={form.notes}
|
||||
placeholder={t("transactions.notesPlaceholder")}
|
||||
onChange={(e) => setForm({ ...form, notes: e.target.value })} />
|
||||
</label>
|
||||
</form>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user