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:
co-authored by
Claude Opus 5
parent
50fc0e3353
commit
498774b0da
@@ -286,6 +286,14 @@
|
||||
}
|
||||
|
||||
/* Filterleiste. */
|
||||
.art-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.art-filter .checkline { margin-bottom: 0; }
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: var(--sp-3);
|
||||
|
||||
@@ -318,7 +318,9 @@
|
||||
"empty": "Noch keine Buchungen.",
|
||||
"needAccount": "Lege zuerst ein Konto an, um Buchungen zu erfassen.",
|
||||
"allAccounts": "Alle Konten",
|
||||
"allTypes": "Alle Typen",
|
||||
"filterExpenses": "Ausgaben",
|
||||
"filterIncome": "Einnahmen",
|
||||
"filterTransfers": "Überträge",
|
||||
"deleteConfirm": "Diese Buchung löschen?",
|
||||
"amountRequired": "Bitte einen Betrag eingeben.",
|
||||
"accountRequired": "Bitte ein Konto wählen.",
|
||||
|
||||
@@ -318,7 +318,9 @@
|
||||
"empty": "No transactions yet.",
|
||||
"needAccount": "Create an account first to record transactions.",
|
||||
"allAccounts": "All accounts",
|
||||
"allTypes": "All types",
|
||||
"filterExpenses": "Expenses",
|
||||
"filterIncome": "Income",
|
||||
"filterTransfers": "Transfers",
|
||||
"deleteConfirm": "Delete this transaction?",
|
||||
"amountRequired": "Please enter an amount.",
|
||||
"accountRequired": "Please choose an account.",
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-1
@@ -240,7 +240,7 @@ input:focus, select:focus, textarea:focus {
|
||||
}
|
||||
input::placeholder, textarea::placeholder { color: var(--muted); opacity: 0.7; }
|
||||
/* Nur in der Hoehe ziehbar: seitwaerts sprengte das Feld den Dialog. */
|
||||
textarea { resize: vertical; min-height: 4.5em; line-height: 1.45; }
|
||||
textarea { resize: vertical; }
|
||||
/* Ankreuzfelder sind keine Textfelder: die Regel oben (width:100%, margin-top)
|
||||
zoege sie sonst ueber die volle Breite, das Haekchen landete in der Mitte und
|
||||
der Text daneben ganz rechts. Hier zentral ausgenommen, damit das nicht an
|
||||
|
||||
Reference in New Issue
Block a user