feat: Schlagwörter je Kategorie + auf-/zuklappbares Kategorie-Dropdown

- Manuelle Schlagwörter je Kategorie (Popup mit Chips, Doppelklick zum Bearbeiten)
- Treffer über Empfänger ODER Verwendungszweck; längstes Schlagwort gewinnt;
  Kind-Filter (Ausgabe/Einnahme); wirken nur bei künftigen Importen
- Lernende Korrektur-Erkennung: ab 5 Anwendungen und >= 1/3 Korrekturen
  erscheint eine Prüf-Meldung in den Einstellungen (Behalten/Entfernen)
- Neue CategorySelect-Komponente (Baum + Suche, per Portal) ersetzt die
  nativen optgroup-Selects in Buchungen und Import-Prüfansicht
- Migration 0006 (source/applied_count/overridden_count/confirmed, match_field any)
- Endpunkte, i18n DE/EN, Tests (test_keywords: 7 neu, gesamt 27 grün)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-19 21:31:29 +02:00
co-authored by Claude Opus 4.8
parent f375fc5eaa
commit 37f64999cd
17 changed files with 1137 additions and 45 deletions
+15 -15
View File
@@ -8,6 +8,7 @@ import Icon from "../components/Icon";
import IconChip from "../components/IconChip";
import Money from "../components/Money";
import Modal from "../components/Modal";
import CategorySelect from "../components/CategorySelect";
import { centsToInput, parseAmountToCents, todayIso } from "../format";
const TYPES = ["expense", "income", "transfer"];
@@ -321,14 +322,14 @@ export default function Transactions() {
) : (
<label>
<span>{t("transactions.category")}</span>
<select value={form.category_id} onChange={(e) => setForm({ ...form, category_id: e.target.value })}>
<option value="">{t("transactions.uncategorized")}</option>
{catOptions.map((g) => (
<optgroup key={g.id} label={g.name}>
{g.categories.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
</optgroup>
))}
</select>
<CategorySelect
groups={catOptions}
value={form.category_id}
onChange={(id) => setForm({ ...form, category_id: id != null ? String(id) : "" })}
allowNone
noneLabel={t("transactions.uncategorized")}
placeholder={t("transactions.uncategorized")}
/>
</label>
)}
@@ -357,13 +358,12 @@ export default function Transactions() {
{form.budget_as_expense && (
<label>
<span>{t("transactions.budgetCategory")}</span>
<select value={form.budget_category_id} onChange={(e) => setForm({ ...form, budget_category_id: e.target.value })}>
{expenseGroups.map((g) => (
<optgroup key={g.id} label={g.name}>
{g.categories.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
</optgroup>
))}
</select>
<CategorySelect
groups={expenseGroups}
value={form.budget_category_id}
onChange={(id) => setForm({ ...form, budget_category_id: id != null ? String(id) : "" })}
placeholder={t("transactions.budgetCategory")}
/>
</label>
)}
</div>