feat: Kategorie als eigene Spalte in der Buchungsliste

Die Auswahl sass unter dem Buchungstext. Damit wurden die Zeilen unterschiedlich
hoch, die Spalte franste aus, und vergleichen liess sich nichts – genau das,
wofuer eine Tabelle da ist.

Jetzt zwei Spalten: 'Buchung' mit dem Text und darunter dem Verwendungszweck,
'Kategorie' mit der Auswahl. Ein Uebertrag zeigt dort einen Strich – er ordnet
kein Geld ein, er verschiebt es.

Die Spaltenliste hat sich damit geaendert, deshalb eine neue Kennung fuer die
Tabelle: Die gespeicherte Reihenfolge kennt die neue Spalte nicht und haette
die Textspalte ans Ende geschoben. Einmalig sind damit Breiten und Filter
dieser Tabelle zurueckgesetzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-27 10:51:23 +02:00
co-authored by Claude Opus 5
parent b303c87801
commit 654c53e3f5
4 changed files with 37 additions and 33 deletions
+5 -13
View File
@@ -1308,18 +1308,10 @@ tr.row-dup {
} }
} }
/* Kategorie-Auswahl in einer Tabellenzeile. Sie steht dort, wo bisher nur der /* Kategorie-Auswahl in einer Tabellenzeile: eigene Spalte, also volle
Name der Kategorie stand – also klein und gedeckelt, nicht über die ganze Zellenbreite – aber ohne den Abstand, den sie im Formular unter ihrer
(wachsende) Spalte. */ Beschriftung braucht. Der verschöbe sie gegen die übrigen Zellen. */
.zeilen-kategorie { .datatable td .catsel-btn {
max-width: 240px;
margin-top: 2px;
}
.zeilen-kategorie .catsel-btn {
margin-top: 0; margin-top: 0;
padding: 3px 6px; padding: 4px 8px;
font-size: 0.8rem;
}
.zeilen-kategorie .catsel-value {
font-weight: 500;
} }
+2 -1
View File
@@ -253,7 +253,8 @@
"amountRequired": "Bitte einen Betrag eingeben.", "amountRequired": "Bitte einen Betrag eingeben.",
"accountRequired": "Bitte ein Konto wählen.", "accountRequired": "Bitte ein Konto wählen.",
"budgetAsSavings": "Als Sparen ins Budget zählen", "budgetAsSavings": "Als Sparen ins Budget zählen",
"budgetCategory": "Budget-Kategorie" "budgetCategory": "Budget-Kategorie",
"subject": "Buchung"
}, },
"budget": { "budget": {
"title": "Budget", "title": "Budget",
+2 -1
View File
@@ -253,7 +253,8 @@
"amountRequired": "Please enter an amount.", "amountRequired": "Please enter an amount.",
"accountRequired": "Please choose an account.", "accountRequired": "Please choose an account.",
"budgetAsSavings": "Count as savings in the budget", "budgetAsSavings": "Count as savings in the budget",
"budgetCategory": "Budget category" "budgetCategory": "Budget category",
"subject": "Subject"
}, },
"budget": { "budget": {
"title": "Budget", "title": "Budget",
+28 -18
View File
@@ -234,7 +234,10 @@ export default function Transactions() {
icon: cat?.icon || "more-horizontal", icon: cat?.icon || "more-horizontal",
color: cat?.color || "#6b7480", color: cat?.color || "#6b7480",
title: zweck || cat?.name || t("transactions.uncategorized"), title: zweck || cat?.name || t("transactions.uncategorized"),
sub: zweck ? (cat?.name || t("transactions.uncategorized")) : "", // Der Verwendungszweck, falls er neben dem Empfänger noch etwas sagt.
// Die Kategorie steht in ihrer eigenen Spalte.
sub: tx.payee && tx.description && tx.description !== tx.payee
? tx.description : "",
}; };
} }
@@ -245,7 +248,8 @@ export default function Transactions() {
render: (tx) => <span className="nowrap">{formatDate(tx.date)}</span>, render: (tx) => <span className="nowrap">{formatDate(tx.date)}</span>,
}, },
{ {
key: "category", header: t("transactions.category"), grow: true, min: 200, // Wofür die Zahlung war – danach sucht man in der Liste.
key: "text", header: t("transactions.subject"), grow: true, min: 220,
filterText: (tx) => rowLabel(tx).title, filterText: (tx) => rowLabel(tx).title,
sortValue: (tx) => rowLabel(tx).title, sortValue: (tx) => rowLabel(tx).title,
render: (tx) => { render: (tx) => {
@@ -255,21 +259,7 @@ export default function Transactions() {
<IconChip name={l.icon} color={l.color} size={26} /> <IconChip name={l.icon} color={l.color} size={26} />
<div className="named-text"> <div className="named-text">
<div className="ellipsis">{l.title}</div> <div className="ellipsis">{l.title}</div>
{/* Die Kategorie direkt in der Zeile ändern – dafür musste man {l.sub && <div className="named-sub ellipsis">{l.sub}</div>}
vorher jede Buchung einzeln zum Bearbeiten öffnen. Ein
Übertrag bekommt keine, dort bleibt der Text stehen. */}
{tx.type === "transfer" ? (
l.sub && <div className="named-sub ellipsis">{l.sub}</div>
) : (
<div className="zeilen-kategorie">
<CategorySelect
groups={tx.type === "income" ? incomeGroups : expenseGroups}
value={tx.category_id}
onChange={(id) => kategorieSetzen(tx, id)}
placeholder={t("transactions.uncategorized")}
/>
</div>
)}
{/* Verknüpft heisst: zählt nicht als Ausgabe oder Einnahme. {/* Verknüpft heisst: zählt nicht als Ausgabe oder Einnahme.
Ohne das Gegenkonto sagt „Übertrag" nichts. */} Ohne das Gegenkonto sagt „Übertrag" nichts. */}
{links[String(tx.id)] && ( {links[String(tx.id)] && (
@@ -284,6 +274,26 @@ export default function Transactions() {
); );
}, },
}, },
{
// Eigene Spalte, damit die Auswahl in der Zeile steht und nicht unter dem
// Text – gestapelt wurden die Zeilen hoch und die Spalte ausgefranst.
key: "category", header: t("transactions.category"), width: 210,
filterText: (tx) => (tx.category_id ? categoryById[tx.category_id]?.name : "") || "",
sortValue: (tx) => (tx.category_id ? categoryById[tx.category_id]?.name : "") || "",
render: (tx) => (
// Ein Übertrag bekommt keine Kategorie – er ordnet kein Geld ein.
tx.type === "transfer" ? (
<span className="muted">—</span>
) : (
<CategorySelect
groups={tx.type === "income" ? incomeGroups : expenseGroups}
value={tx.category_id}
onChange={(id) => kategorieSetzen(tx, id)}
placeholder={t("transactions.uncategorized")}
/>
)
),
},
{ {
key: "account", header: t("transactions.account"), width: 150, key: "account", header: t("transactions.account"), width: 150,
filterText: (tx) => accountById[tx.account_id]?.name || "", filterText: (tx) => accountById[tx.account_id]?.name || "",
@@ -353,7 +363,7 @@ export default function Transactions() {
<div className="card"> <div className="card">
<DataTable <DataTable
id="transactions" id="transactions-v2"
rows={txns} rows={txns}
getRowKey={(tx) => tx.id} getRowKey={(tx) => tx.id}
empty={t("transactions.empty")} empty={t("transactions.empty")}