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. */
|
/* Filterleiste. */
|
||||||
|
.art-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.art-filter .checkline { margin-bottom: 0; }
|
||||||
|
|
||||||
.filter-bar {
|
.filter-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--sp-3);
|
gap: var(--sp-3);
|
||||||
|
|||||||
@@ -318,7 +318,9 @@
|
|||||||
"empty": "Noch keine Buchungen.",
|
"empty": "Noch keine Buchungen.",
|
||||||
"needAccount": "Lege zuerst ein Konto an, um Buchungen zu erfassen.",
|
"needAccount": "Lege zuerst ein Konto an, um Buchungen zu erfassen.",
|
||||||
"allAccounts": "Alle Konten",
|
"allAccounts": "Alle Konten",
|
||||||
"allTypes": "Alle Typen",
|
"filterExpenses": "Ausgaben",
|
||||||
|
"filterIncome": "Einnahmen",
|
||||||
|
"filterTransfers": "Überträge",
|
||||||
"deleteConfirm": "Diese Buchung löschen?",
|
"deleteConfirm": "Diese Buchung löschen?",
|
||||||
"amountRequired": "Bitte einen Betrag eingeben.",
|
"amountRequired": "Bitte einen Betrag eingeben.",
|
||||||
"accountRequired": "Bitte ein Konto wählen.",
|
"accountRequired": "Bitte ein Konto wählen.",
|
||||||
|
|||||||
@@ -318,7 +318,9 @@
|
|||||||
"empty": "No transactions yet.",
|
"empty": "No transactions yet.",
|
||||||
"needAccount": "Create an account first to record transactions.",
|
"needAccount": "Create an account first to record transactions.",
|
||||||
"allAccounts": "All accounts",
|
"allAccounts": "All accounts",
|
||||||
"allTypes": "All types",
|
"filterExpenses": "Expenses",
|
||||||
|
"filterIncome": "Income",
|
||||||
|
"filterTransfers": "Transfers",
|
||||||
"deleteConfirm": "Delete this transaction?",
|
"deleteConfirm": "Delete this transaction?",
|
||||||
"amountRequired": "Please enter an amount.",
|
"amountRequired": "Please enter an amount.",
|
||||||
"accountRequired": "Please choose an account.",
|
"accountRequired": "Please choose an account.",
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ export default function Transactions() {
|
|||||||
const [accounts, setAccounts] = useState([]);
|
const [accounts, setAccounts] = useState([]);
|
||||||
const [groups, setGroups] = useState([]);
|
const [groups, setGroups] = useState([]);
|
||||||
const [tags, setTags] = 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 [form, setForm] = useState(null);
|
||||||
const [editing, setEditing] = useState(null);
|
const [editing, setEditing] = useState(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
@@ -92,7 +96,7 @@ export default function Transactions() {
|
|||||||
// Verknüpfungen getrennt: Sie sind eine Schicht über den Buchungen,
|
// Verknüpfungen getrennt: Sie sind eine Schicht über den Buchungen,
|
||||||
// kein Feld an ihnen – deshalb auch ein eigener Abruf.
|
// kein Feld an ihnen – deshalb auch ein eigener Abruf.
|
||||||
const [liste, verkn, vorschlaege, aboListe] = await Promise.all([
|
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(() => ({})),
|
api.transactionLinks().catch(() => ({})),
|
||||||
// Nur fragen, nichts tun: Beim Anschauen der Liste darf nichts
|
// Nur fragen, nichts tun: Beim Anschauen der Liste darf nichts
|
||||||
// verknüpft werden.
|
// verknüpft werden.
|
||||||
@@ -207,7 +211,8 @@ export default function Transactions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
useEffect(() => { ladenStamm(); }, []);
|
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 fromAcc = form && accountById[Number(form.account_id)];
|
||||||
const toAcc = form && accountById[Number(form.to_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);
|
if (gegenseiteDa) paare[tx.id] = txns.find((x) => x.id === link.other_id);
|
||||||
sichtbar.push(tx);
|
sichtbar.push(tx);
|
||||||
}
|
}
|
||||||
return { zeilen: sichtbar, paarZu: paare };
|
// Eine verknuepfte Buchung ist weder Ausgabe noch Einnahme – sie verschiebt
|
||||||
}, [txns, links]);
|
// 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) {
|
function rowLabel(tx) {
|
||||||
const gegen = paarZu[tx.id];
|
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
|
// Eigene Spalte, damit die Auswahl in der Zeile steht und nicht unter dem
|
||||||
// Text – gestapelt wurden die Zeilen hoch und die Spalte ausgefranst.
|
// Text – gestapelt wurden die Zeilen hoch und die Spalte ausgefranst.
|
||||||
@@ -654,12 +673,22 @@ export default function Transactions() {
|
|||||||
placeholder={t("transactions.allAccounts")} showCurrency={false}
|
placeholder={t("transactions.allAccounts")} showCurrency={false}
|
||||||
minWidth={190}
|
minWidth={190}
|
||||||
onChange={(id) => setFilter({ ...filter, account_id: id != null ? String(id) : "" })} />
|
onChange={(id) => setFilter({ ...filter, account_id: id != null ? String(id) : "" })} />
|
||||||
<select value={filter.type} onChange={(e) => setFilter({ ...filter, type: e.target.value })}>
|
<div className="art-filter">
|
||||||
<option value="">{t("transactions.allTypes")}</option>
|
{[["expense", "filterExpenses"], ["income", "filterIncome"],
|
||||||
<option value="expense">{t("transactions.expense")}</option>
|
["uebertrag", "filterTransfers"]].map(([art, schluessel]) => (
|
||||||
<option value="income">{t("transactions.income")}</option>
|
<label className="checkline" key={art}>
|
||||||
<option value="transfer">{t("transactions.transfer")}</option>
|
<input
|
||||||
</select>
|
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>
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
@@ -802,7 +831,7 @@ export default function Transactions() {
|
|||||||
Freier Text, weil sich das nicht in Schubladen fassen laesst. */}
|
Freier Text, weil sich das nicht in Schubladen fassen laesst. */}
|
||||||
<label>
|
<label>
|
||||||
<span>{t("transactions.notes")}</span>
|
<span>{t("transactions.notes")}</span>
|
||||||
<textarea rows={2} value={form.notes}
|
<textarea rows={1} value={form.notes}
|
||||||
placeholder={t("transactions.notesPlaceholder")}
|
placeholder={t("transactions.notesPlaceholder")}
|
||||||
onChange={(e) => setForm({ ...form, notes: e.target.value })} />
|
onChange={(e) => setForm({ ...form, notes: e.target.value })} />
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
+1
-1
@@ -240,7 +240,7 @@ input:focus, select:focus, textarea:focus {
|
|||||||
}
|
}
|
||||||
input::placeholder, textarea::placeholder { color: var(--muted); opacity: 0.7; }
|
input::placeholder, textarea::placeholder { color: var(--muted); opacity: 0.7; }
|
||||||
/* Nur in der Hoehe ziehbar: seitwaerts sprengte das Feld den Dialog. */
|
/* 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)
|
/* 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
|
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
|
der Text daneben ganz rechts. Hier zentral ausgenommen, damit das nicht an
|
||||||
|
|||||||
Reference in New Issue
Block a user