+ {g.name && (
{g.name}
+ )}
{!isCollapsed &&
cats.map((c) => (
diff --git a/web/src/components/DataTable.jsx b/web/src/components/DataTable.jsx
index d14beeb..0cca1c2 100644
--- a/web/src/components/DataTable.jsx
+++ b/web/src/components/DataTable.jsx
@@ -30,18 +30,28 @@ export default function DataTable({
}) {
const colByKey = useMemo(() => Object.fromEntries(columns.map((c) => [c.key, c])), [columns]);
- const [order, setOrder] = useState(() => {
- const p = loadPersisted(id);
- const saved = Array.isArray(p.order) ? p.order.filter((k) => colByKey[k]) : [];
- const rest = columns.map((c) => c.key).filter((k) => !saved.includes(k));
- return [...saved, ...rest];
- });
+ // Eine neue Spalte landet dort, wo sie definiert ist – nicht am Ende.
+ //
+ // Angehaengt stand sie hinter der Aktionsspalte, also ganz rechts ausserhalb
+ // des Sichtbereichs: Im Menue angehakt, in der Tabelle unauffindbar. Wer die
+ // Reihenfolge selbst umgestellt hat, behaelt sie; eingefuegt wird nur die
+ // neue Spalte, hinter ihrem linken Nachbarn aus der Definition.
+ const einsortieren = (vorhanden) => {
+ const alle = columns.map((c) => c.key);
+ const out = vorhanden.filter((k) => colByKey[k]);
+ for (const k of alle) {
+ if (out.includes(k)) continue;
+ const davor = alle.slice(0, alle.indexOf(k)).reverse().find((x) => out.includes(x));
+ out.splice(davor ? out.indexOf(davor) + 1 : 0, 0, k);
+ }
+ return out;
+ };
+
+ const [order, setOrder] = useState(
+ () => einsortieren(loadPersisted(id).order || []));
const schluessel = columns.map((c) => c.key).join("|");
useEffect(() => {
- setOrder((alt) => {
- const fehlend = columns.map((c) => c.key).filter((k) => !alt.includes(k));
- return fehlend.length ? [...alt, ...fehlend] : alt;
- });
+ setOrder((alt) => (columns.every((c) => alt.includes(c.key)) ? alt : einsortieren(alt)));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [schluessel]);
diff --git a/web/src/locales/de.json b/web/src/locales/de.json
index aa63b7c..19965fd 100644
--- a/web/src/locales/de.json
+++ b/web/src/locales/de.json
@@ -359,6 +359,7 @@
"title": "Abos & Verträge",
"noneAssigned": "— kein Abo —",
"column": "Abo",
+ "searchPlaceholder": "Abo suchen …",
"new": "Neuer Vertrag",
"edit": "Vertrag bearbeiten",
"delete": "Löschen",
diff --git a/web/src/locales/en.json b/web/src/locales/en.json
index c720640..25884f5 100644
--- a/web/src/locales/en.json
+++ b/web/src/locales/en.json
@@ -359,6 +359,7 @@
"title": "Subscriptions & contracts",
"noneAssigned": "— no subscription —",
"column": "Subscription",
+ "searchPlaceholder": "Search subscriptions …",
"new": "New contract",
"edit": "Edit contract",
"delete": "Delete",
diff --git a/web/src/pages/Abos.jsx b/web/src/pages/Abos.jsx
index fd6ce09..6d63f9b 100644
--- a/web/src/pages/Abos.jsx
+++ b/web/src/pages/Abos.jsx
@@ -6,6 +6,7 @@ import { useConfirm } from "../confirm";
import { useToast } from "../toast";
import Icon from "../components/Icon";
import IconChip from "../components/IconChip";
+import AboMarke from "../components/AboMarke";
import Modal from "../components/Modal";
import IconPicker, { CATEGORY_ICONS } from "../components/IconPicker";
import CategorySelect from "../components/CategorySelect";
@@ -213,12 +214,7 @@ export default function Abos() {
{/* Das Logo schlaegt das Symbol: Netflix erkennt man am roten N,
nicht am Wort – und erst recht nicht an einem Zahnrad. */}
- {s.has_logo ? (
-
- ) : (
-
- )}
+
{s.name}
diff --git a/web/src/pages/Transactions.jsx b/web/src/pages/Transactions.jsx
index 7f5ca9c..0595ff0 100644
--- a/web/src/pages/Transactions.jsx
+++ b/web/src/pages/Transactions.jsx
@@ -11,6 +11,7 @@ import Modal from "../components/Modal";
import BudgetZeitraum from "../components/BudgetZeitraum";
import AccountSelect from "../components/AccountSelect";
import CategorySelect from "../components/CategorySelect";
+import AboMarke from "../components/AboMarke";
import UebertragDialog from "../components/UebertragDialog";
import DataTable from "../components/DataTable";
import { gruppenFuer } from "../kategorien";
@@ -183,6 +184,14 @@ export default function Transactions() {
&& (toAcc.savings_category_id != null || SAVINGS_TYPES.includes(toAcc.type));
// Standard-Kategorie für budgetierte Sparen-Transfers (falls vorhanden).
+ // Eine namenlose Gruppe: CategorySelect zeigt dann keine Kopfzeile.
+ // Alphabetisch, nicht in Anlagereihenfolge – bei zwanzig Vertraegen sucht
+ // man einen bestimmten, und die Reihenfolge der Eingabe kennt niemand.
+ const aboGruppe = useMemo(() => [{
+ id: 0, name: null,
+ categories: [...abos].sort((a, b) => a.name.localeCompare(b.name, i18n.language)),
+ }], [abos, i18n.language]);
+
const expenseGroups = useMemo(() => gruppenFuer(groups, "expense"), [groups]);
const incomeGroups = useMemo(() => gruppenFuer(groups, "income"), [groups]);
const sparenCatId = useMemo(() => {
@@ -481,11 +490,18 @@ export default function Transactions() {
tx.type === "transfer" || links[String(tx.id)] ? (
—
) : (
-
+ // Dieselbe Auswahl wie bei der Kategorie – nur flach (die Vertraege
+ // haben keine Gruppen) und mit dem Markenlogo statt eines Symbols.
+ aboSetzen(tx, id)}
+ renderMarke={(a, size) => }
+ searchPlaceholder={t("abos.searchPlaceholder")}
+ allowNone
+ noneLabel={t("abos.noneAssigned")}
+ placeholder={t("abos.noneAssigned")}
+ />
)
),
},