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:
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSettings } from "../settings";
|
||||
import { useAuth } from "../auth";
|
||||
@@ -10,6 +11,22 @@ export default function Settings() {
|
||||
const s = useSettings();
|
||||
const { isAdmin } = useAuth();
|
||||
const toast = useToast();
|
||||
const [flagged, setFlagged] = useState([]);
|
||||
|
||||
async function ladenFlagged() {
|
||||
try { setFlagged(await api.flaggedKeywords()); }
|
||||
catch { /* still ignorieren – nicht kritisch für die Einstellungen */ }
|
||||
}
|
||||
useEffect(() => { ladenFlagged(); }, []);
|
||||
|
||||
async function keywordBehalten(id) {
|
||||
try { await api.confirmKeyword(id); setFlagged((f) => f.filter((x) => x.id !== id)); }
|
||||
catch (err) { toast(err.message, "error"); }
|
||||
}
|
||||
async function keywordEntfernen(id) {
|
||||
try { await api.deleteKeyword(id); setFlagged((f) => f.filter((x) => x.id !== id)); }
|
||||
catch (err) { toast(err.message, "error"); }
|
||||
}
|
||||
|
||||
async function set(key, value) {
|
||||
try {
|
||||
@@ -32,6 +49,30 @@ export default function Settings() {
|
||||
<div>
|
||||
<h1>{t("settings.title")}</h1>
|
||||
|
||||
{flagged.length > 0 && (
|
||||
<div className="card" style={{ maxWidth: 520, marginBottom: "var(--sp-4)", borderColor: "var(--danger)" }}>
|
||||
<h2 style={{ display: "flex", alignItems: "center", gap: "var(--sp-2)", marginTop: 0 }}>
|
||||
<Icon name="alert" size={18} /> {t("settings.keywordReview")}
|
||||
</h2>
|
||||
<p className="muted small">{t("settings.keywordReviewHint")}</p>
|
||||
{flagged.map((f) => (
|
||||
<div className="kw-flag" key={f.id}>
|
||||
<div className="kw-flag-main">
|
||||
<div className="ellipsis">
|
||||
<span className="kw-flag-word">{f.word}</span>
|
||||
<Icon name="chevron-right" size={12} /> {f.category_name}
|
||||
</div>
|
||||
<div className="muted small">
|
||||
{t("settings.keywordCorrected", { overridden: f.overridden_count, applied: f.applied_count })}
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn sm" onClick={() => keywordBehalten(f.id)}>{t("settings.keep")}</button>
|
||||
<button className="btn sm danger" onClick={() => keywordEntfernen(f.id)}>{t("settings.remove")}</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card" style={{ maxWidth: 520 }}>
|
||||
<label>
|
||||
<span>{t("settings.baseCurrency")}</span>
|
||||
|
||||
Reference in New Issue
Block a user