feat: Zeilenfilter sichtbar, mit auswaehlbaren Werten

Der Filter steckte in einem zugeklappten Bereich und der Wert musste
blind getippt werden – entsprechend importierte man den Revolut-Auszug
mit allen Produkten in ein Konto, was den Saldo-Abgleich unmoeglich
macht.

Die Rohvorschau liefert jetzt je Spalte die wiederkehrenden Werte samt
Zeilenzahl. Hat eine Datei so eine Spalte, steht der Filter offen sichtbar
mit der Ueberschrift "Diese Datei enthaelt mehrere Konten", und der Wert
wird aus einer Liste gewaehlt ("Giro (530 Zeilen)").

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-21 18:38:05 +02:00
co-authored by Claude Opus 5
parent d05f4915ad
commit bc5349f193
5 changed files with 111 additions and 23 deletions
+42 -17
View File
@@ -218,6 +218,12 @@ export default function Import() {
}));
}, [raw]);
// Spalten, die die Datei in wenige Gruppen teilen (Revolut: „Produkt").
const filterSpalten = useMemo(
() => colOptions.filter((c) => raw?.filter_values?.[String(c.i)]),
[colOptions, raw],
);
const expenseGroups = useMemo(() => gruppenFuer(groups, "expense"), [groups]);
const incomeGroups = useMemo(() => gruppenFuer(groups, "income"), [groups]);
@@ -521,6 +527,42 @@ export default function Import() {
))}
</div>
{/* Revolut legt alle Produkte in EINE Datei. Ohne Filter zaehlen die
Bewegungen doppelt – deshalb steht der Filter offen, sobald die
Datei eine Spalte mit wenigen wiederkehrenden Werten hat. */}
{filterSpalten.length > 0 && (
<div className="alert" style={{ marginTop: "var(--sp-3)", display: "block" }}>
<strong>{t("import.filterTitle")}</strong>
<p className="muted small">{t("import.filterHint")}</p>
<div className="map-grid">
<label>
<span>{t("import.filterColumn")}</span>
<select value={config.filter_column ?? ""}
onChange={(e) => setCfg({
filter_column: e.target.value === "" ? null : Number(e.target.value),
filter_value: "",
})}>
<option value="">{t("import.filterNone")}</option>
{filterSpalten.map((c) => <option key={c.i} value={c.i}>{c.label}</option>)}
</select>
</label>
<label>
<span>{t("import.filterValue")}</span>
<select value={config.filter_value}
disabled={config.filter_column == null}
onChange={(e) => setCfg({ filter_value: e.target.value })}>
<option value="">{t("import.filterNone")}</option>
{(raw?.filter_values?.[String(config.filter_column)] || []).map((w) => (
<option key={w.value} value={w.value}>
{t("import.filterValueOption", { value: w.value, count: w.count })}
</option>
))}
</select>
</label>
</div>
</div>
)}
<details className="import-more">
<summary className="muted small">{t("import.moreColumns")}</summary>
<p className="muted small">{t("import.ibanHint")}</p>
@@ -542,23 +584,6 @@ export default function Import() {
))}
</div>
<p className="muted small">{t("import.extraColsHint")}</p>
<div className="map-grid">
<label>
<span>{t("import.filterColumn")}</span>
<select value={config.filter_column ?? ""}
onChange={(e) => setCfg({ filter_column: e.target.value === "" ? null : Number(e.target.value) })}>
<option value="">{t("import.filterNone")}</option>
{colOptions.map((c) => <option key={c.i} value={c.i}>{c.label}</option>)}
</select>
</label>
<label>
<span>{t("import.filterValue")}</span>
<input value={config.filter_value}
placeholder={t("import.filterValuePlaceholder")}
onChange={(e) => setCfg({ filter_value: e.target.value })} />
</label>
</div>
<p className="muted small">{t("import.filterHint")}</p>
</details>
<div className="field-inline" style={{ marginTop: "var(--sp-3)" }}>