feat: Die Buchungsliste zeigt die Notiz und laesst waehlen, welche Arten erscheinen

Die Notiz stand nur im Bearbeiten-Dialog, obwohl man sie schreibt, um sie
spaeter wiederzufinden. Jetzt hat sie eine eigene Spalte mit eigenem Filter;
der ganze Text steht im Tooltip. Das Feld im Dialog ist wieder einzeilig und
laesst sich bei Bedarf aufziehen.

Statt einer Einfachauswahl gibt es drei Haken: Ausgaben, Einnahmen,
Uebertraege. Man will oft zwei Arten gleichzeitig sehen und eine dritte
weglassen.

Und "nur Einnahmen" zeigt keine Uebertraege mehr. Eine verknuepfte Buchung ist
weder Ausgabe noch Einnahme, sie verschiebt nur Geld - nach ihrem rohen Typ
gefiltert stand sie trotzdem unter den Einnahmen, und weil der Server nur eine
Seite des Paares lieferte, sogar einzeln statt als eine Zeile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-19 11:17:37 +02:00
co-authored by Claude Opus 5
parent 50fc0e3353
commit 498774b0da
5 changed files with 56 additions and 15 deletions
+41 -12
View File
@@ -59,7 +59,11 @@ export default function Transactions() {
const [accounts, setAccounts] = useState([]);
const [groups, setGroups] = useState([]);
const [tags, setTags] = useState([]);
const [filter, setFilter] = useState({ account_id: "", type: "" });
// Drei Haken statt eines Typs: Man will oft zwei Arten gleichzeitig sehen
// und eine dritte weglassen, und genau das ging mit einer Einfachauswahl nicht.
const [filter, setFilter] = useState({
account_id: "", arten: { expense: true, income: true, uebertrag: true },
});
const [form, setForm] = useState(null);
const [editing, setEditing] = useState(null);
const [busy, setBusy] = useState(false);
@@ -92,7 +96,7 @@ export default function Transactions() {
// Verknüpfungen getrennt: Sie sind eine Schicht über den Buchungen,
// kein Feld an ihnen – deshalb auch ein eigener Abruf.
const [liste, verkn, vorschlaege, aboListe] = await Promise.all([
api.listTransactions({ account_id: filter.account_id, type: filter.type }),
api.listTransactions({ account_id: filter.account_id }),
api.transactionLinks().catch(() => ({})),
// Nur fragen, nichts tun: Beim Anschauen der Liste darf nichts
// verknüpft werden.
@@ -207,7 +211,8 @@ export default function Transactions() {
}
}
useEffect(() => { ladenStamm(); }, []);
useEffect(() => { ladenBuchungen(); }, [filter.account_id, filter.type]);
// Nur das Konto geht an den Server; nach Art wird im Browser gefiltert.
useEffect(() => { ladenBuchungen(); }, [filter.account_id]);
const fromAcc = form && accountById[Number(form.account_id)];
const toAcc = form && accountById[Number(form.to_account_id)];
@@ -356,8 +361,12 @@ export default function Transactions() {
if (gegenseiteDa) paare[tx.id] = txns.find((x) => x.id === link.other_id);
sichtbar.push(tx);
}
return { zeilen: sichtbar, paarZu: paare };
}, [txns, links]);
// Eine verknuepfte Buchung ist weder Ausgabe noch Einnahme – sie verschiebt
// nur Geld (siehe transfers.linkedHint). Nach ihrem rohen `type` zu filtern
// zeigte sie unter „Einnahmen", wo sie niemand sucht.
const art = (t) => (t.type === "transfer" || links[String(t.id)] ? "uebertrag" : t.type);
return { zeilen: sichtbar.filter((t) => filter.arten[art(t)]), paarZu: paare };
}, [txns, links, filter.arten]);
function rowLabel(tx) {
const gegen = paarZu[tx.id];
@@ -495,6 +504,16 @@ export default function Transactions() {
);
},
},
{
// Die Notiz stand bisher nur im Bearbeiten-Dialog. Wer sie schreibt,
// will sie aber spaeter sehen, ohne jede Zeile aufzuklappen.
key: "notes", header: t("transactions.notes"), width: 180,
filterText: (tx) => tx.notes || "",
render: (tx) => (tx.notes
// Ganzer Text im Tooltip: Die Spalte ist schmal, die Notiz darf lang sein.
? <span className="ellipsis" title={tx.notes}>{tx.notes}</span>
: <span className="muted">—</span>),
},
{
// Eigene Spalte, damit die Auswahl in der Zeile steht und nicht unter dem
// Text – gestapelt wurden die Zeilen hoch und die Spalte ausgefranst.
@@ -654,12 +673,22 @@ export default function Transactions() {
placeholder={t("transactions.allAccounts")} showCurrency={false}
minWidth={190}
onChange={(id) => setFilter({ ...filter, account_id: id != null ? String(id) : "" })} />
<select value={filter.type} onChange={(e) => setFilter({ ...filter, type: e.target.value })}>
<option value="">{t("transactions.allTypes")}</option>
<option value="expense">{t("transactions.expense")}</option>
<option value="income">{t("transactions.income")}</option>
<option value="transfer">{t("transactions.transfer")}</option>
</select>
<div className="art-filter">
{[["expense", "filterExpenses"], ["income", "filterIncome"],
["uebertrag", "filterTransfers"]].map(([art, schluessel]) => (
<label className="checkline" key={art}>
<input
type="checkbox"
checked={filter.arten[art]}
onChange={(e) => setFilter({
...filter,
arten: { ...filter.arten, [art]: e.target.checked },
})}
/>
<span>{t(`transactions.${schluessel}`)}</span>
</label>
))}
</div>
</div>
<div className="card">
@@ -802,7 +831,7 @@ export default function Transactions() {
Freier Text, weil sich das nicht in Schubladen fassen laesst. */}
<label>
<span>{t("transactions.notes")}</span>
<textarea rows={2} value={form.notes}
<textarea rows={1} value={form.notes}
placeholder={t("transactions.notesPlaceholder")}
onChange={(e) => setForm({ ...form, notes: e.target.value })} />
</label>