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:
Scarriffle
2026-10-01 20:49:01 +02:00
co-authored by Claude Opus 5
parent 76dbd3b481
commit eb72f0cb27
8 changed files with 342 additions and 14 deletions
+46 -2
View File
@@ -18,6 +18,11 @@ export default function Uebertragsvorschlaege({ baseCurrency, onChange }) {
const [stand, setStand] = useState(null); // { verknuepft, vorschlaege }
const [busy, setBusy] = useState(false);
// Abgelehnte Vorschlaege. Ohne diese Liste war „Ignorieren" endgueltig:
// Das Paar blieb bei jedem weiteren Lauf 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.
const [abgelehnt, setAbgelehnt] = useState([]);
async function suchen(melden = false) {
setBusy(true);
@@ -37,7 +42,21 @@ export default function Uebertragsvorschlaege({ baseCurrency, onChange }) {
}
}
useEffect(() => { suchen(false); }, []);
async function abgelehnteLaden() {
try { setAbgelehnt(await api.ignoredTransfers()); } catch { /* still */ }
}
useEffect(() => { suchen(false); abgelehnteLaden(); }, []);
async function zurueckholen(paar) {
try {
await api.unignoreTransfer(paar.a.id, paar.b.id);
await abgelehnteLaden();
suchen(false);
} catch (e) {
toast(e.message, "error");
}
}
async function verbinden(paar) {
try {
@@ -53,6 +72,7 @@ export default function Uebertragsvorschlaege({ baseCurrency, onChange }) {
try {
await api.ignoreTransfer(paar.a.id, paar.b.id);
suchen(false);
abgelehnteLaden();
} catch (e) {
toast(e.message, "error");
}
@@ -60,7 +80,7 @@ export default function Uebertragsvorschlaege({ baseCurrency, onChange }) {
// Vor dem ersten Ergebnis keine leere Karte zeigen.
if (stand === null) return null;
if (stand.vorschlaege.length === 0) return null;
if (stand.vorschlaege.length === 0 && abgelehnt.length === 0) return null;
return (
<div className="card">
@@ -102,6 +122,30 @@ export default function Uebertragsvorschlaege({ baseCurrency, onChange }) {
);
})}
</div>
{/* Der Rueckweg. Eingeklappt, weil man ihn selten braucht – aber da,
weil ein Klick sonst fuer immer gilt. */}
{abgelehnt.length > 0 && (
<details className="abgelehnte">
<summary className="muted small">
{t("transfers.ignoredCount", { count: abgelehnt.length })}
</summary>
<p className="muted small">{t("transfers.ignoredHint")}</p>
{abgelehnt.map((paar) => (
<div className="uebertrag-vorschlag" key={`x${paar.a.id}-${paar.b.id}`}>
<div className="uebertrag-seiten">
<Seite seite={paar.a} baseCurrency={baseCurrency} />
<Icon name="arrow-left-right" size={16} className="uebertrag-pfeil" />
<Seite seite={paar.b} baseCurrency={baseCurrency} />
</div>
<button className="btn sm" style={{ marginTop: "var(--sp-2)" }}
onClick={() => zurueckholen(paar)}>
{t("transfers.unignore")}
</button>
</div>
))}
</details>
)}
</div>
);
}