From 2891270ac2f5d2dc139f9e3544adad477a21c3df Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Wed, 30 Sep 2026 20:58:38 +0200 Subject: [PATCH] feat: Der Import fragt nur noch, was er nicht selbst weiss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher standen alle Einstellungen offen nebeneinander – auch die, die Certain laengst erkannt hatte. Rund zwanzig Bedienelemente fuer einen Vorgang, bei dem im Normalfall keine einzige Entscheidung ansteht. Jetzt: Datei waehlen, Pruefansicht. Dazwischen nichts. - Format und Spaltenzuordnung klappen ein, solange die Erkennung sicher ist. Ein Klick auf "Alle Einstellungen anzeigen" holt sie zurueck – samt der Begruendung, WAS erkannt wurde und warum die Vorschau so aussieht, wie sie aussieht. Wer nachsehen will, kann; muessen soll niemand. - Die Erkennungs-Meldung steht jetzt unter der Datei, auf die sie sich bezieht, statt weit unten zwischen den Spaltenfeldern. - "Vorschau erstellen" entfaellt. Der Knopf tat genau das, was ohnehin als naechstes kommen musste; die Vorschau laeuft jetzt von selbst, entprellt, sobald Datum und Betrag zugeordnet sind. Fehlt etwas, steht da, was fehlt. - "Als Vorlage speichern" ist keine primaere Aktion mehr. Das ist Pflege und sah aus wie eine Entscheidung, die jetzt ansteht. Der Zeilenfilter klappt ausdruecklich NICHT mit ein. Revolut legt alle Pockets in eine Datei; ohne Filter zaehlen die Bewegungen doppelt. Das ist kein Detail fuer Fortgeschrittene, sondern die Bedingung dafuer, dass die Zahlen stimmen. Co-Authored-By: Claude Opus 5 --- web/src/locales/de.json | 6 +- web/src/locales/en.json | 6 +- web/src/pages/Import.jsx | 130 +++++++++++++++++++++++++++++---------- 3 files changed, 103 insertions(+), 39 deletions(-) diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 4c179b2..e5586be 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -440,7 +440,6 @@ "modeDebitCredit": "Getrennte Soll-/Haben-Spalten", "modeAbsExpense": "Ein Betrag (immer Ausgabe)", "readFile": "Datei einlesen", - "makePreview": "Vorschau erstellen", "saveTemplate": "Als Vorlage speichern", "templateName": "Vorlagenname", "colDate": "Datum", @@ -452,7 +451,6 @@ "colIban": "Gegen-IBAN", "makePreviewHint": "Ordne mindestens Datum und Betrag zu.", "rawHint": "Nur die ersten Zeilen – sie dienen nur der Spaltenzuordnung.", - "makePreviewNote": "„Vorschau erstellen“ zeigt danach ALLE Buchungen zum Prüfen und Buchen.", "summary": "{{total}} Zeilen erkannt · {{n}} werden gebucht (Duplikate/ungültige ausgenommen).", "setTypeForAll": "Alle gewählten Zeilen:", "flipAll": "Richtung umdrehen", @@ -510,7 +508,9 @@ "detectedBank": "Format erkannt: {{name}}", "detectedGeneric": "Format aus der Datei abgeleitet", "detectFailed": "Format nicht erkannt", - "detectUncertain": "Nicht alles war eindeutig – wirf einen Blick auf die Spaltenzuordnung unten, bevor du die Vorschau erzeugst." + "detectUncertain": "Nicht alles war eindeutig – wirf einen Blick auf die Spaltenzuordnung unten, bevor du die Vorschau erzeugst.", + "showAllSettings": "Alle Einstellungen anzeigen", + "hideAllSettings": "Einstellungen ausblenden" }, "password": { "show": "Passwort anzeigen", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 9bbc145..68d4df0 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -440,7 +440,6 @@ "modeDebitCredit": "Separate debit/credit columns", "modeAbsExpense": "Single amount (always expense)", "readFile": "Read file", - "makePreview": "Build preview", "saveTemplate": "Save as template", "templateName": "Template name", "colDate": "Date", @@ -452,7 +451,6 @@ "colIban": "Counterparty IBAN", "makePreviewHint": "Map at least date and amount.", "rawHint": "Only the first rows – just for mapping the columns.", - "makePreviewNote": "“Build preview” then shows ALL transactions to review and import.", "summary": "{{total}} rows detected · {{n}} will be imported (duplicates/invalid excluded).", "setTypeForAll": "All selected rows:", "flipAll": "Flip direction", @@ -510,7 +508,9 @@ "detectedBank": "Format detected: {{name}}", "detectedGeneric": "Format derived from the file", "detectFailed": "Format not recognised", - "detectUncertain": "Not everything was clear – check the column mapping below before building the preview." + "detectUncertain": "Not everything was clear – check the column mapping below before building the preview.", + "showAllSettings": "Show all settings", + "hideAllSettings": "Hide settings" }, "password": { "show": "Show password", diff --git a/web/src/pages/Import.jsx b/web/src/pages/Import.jsx index 1c0d846..fc610da 100644 --- a/web/src/pages/Import.jsx +++ b/web/src/pages/Import.jsx @@ -77,6 +77,8 @@ export default function Import() { const [erkannt, setErkannt] = useState(null); // 0 = noch nichts gebucht, also auch nichts zu zeigen. const [uebertragsLauf, setUebertragsLauf] = useState(0); + // Hat der Nutzer die vollstaendige Ansicht ausdruecklich geoeffnet? + const [alleEinstellungen, setAlleEinstellungen] = useState(false); const [quelle, setQuelle] = useState(""); // "preset:i" | "tpl:id" | "" const [rows, setRows] = useState(null); const [recon, setRecon] = useState(null); @@ -106,6 +108,27 @@ export default function Import() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [file, mode, config.delimiter, config.encoding, config.has_header, config.skip_rows]); + // Auch die Pruefansicht laeuft von selbst, sobald Datum und Betrag zugeordnet + // sind. Der Knopf „Vorschau erstellen" war eine Pflichtuebung: Er tat genau + // das, was ohnehin als naechstes kommen musste. + // + // Entprellt, weil jede Aenderung an „Zeilen ueberspringen" sonst eine + // Anfrage samt Dateiuebertragung ausloest. + const vorschauBereit = mode === "csv" && !!file && !!accountId && !!raw + && config.column_map.date != null + && (config.amount_mode === "debit_credit" + ? (config.column_map.debit != null || config.column_map.credit != null) + : config.column_map.amount != null); + + useEffect(() => { + if (!vorschauBereit) return; + const id = setTimeout(() => { loadPreview(); }, 500); + return () => clearTimeout(id); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [vorschauBereit, raw, accountId, config.decimal_sep, config.date_format, + config.amount_mode, config.filter_column, config.filter_value, + JSON.stringify(config.column_map)]); + function setCfg(patch) { setConfig((c) => ({ ...c, ...patch })); } function setCol(field, value) { setConfig((c) => ({ ...c, column_map: { ...c.column_map, [field]: value === "" ? null : Number(value) } })); @@ -219,6 +242,9 @@ export default function Import() { n: batch.imported_count, u: batch.uebertraege_verknuepft }) : t("import.imported", { n: batch.imported_count })); setRows(null); setRecon(null); setRaw(null); setFile(null); setDocText(""); setDocPages([]); + // Auch die Erkennungs-Meldung: Sie bezoege sich sonst auf eine Datei, + // die nicht mehr gewaehlt ist. + setErkannt(null); setAlleEinstellungen(false); if (fileRef.current) fileRef.current.value = ""; loadStamm(); // Uebertraege sind nach einem Import am wahrscheinlichsten – eine @@ -406,6 +432,11 @@ export default function Import() { const acc = accountById[Number(accountId)]; + // Sicher erkannt heisst: Der Nutzer muss nichts einstellen. Dann stehen + // Format und Spaltenzuordnung nicht offen im Weg – erreichbar bleiben sie. + const sicherErkannt = !!erkannt?.ok && erkannt.sicherheit === "hoch"; + const zeigeEinstellungen = !sicherErkannt || alleEinstellungen; + const reviewColumns = useMemo(() => [ { key: "include", header: "", width: 42, fixed: true, @@ -592,6 +623,7 @@ export default function Import() { onChange={(e) => { const f = e.target.files?.[0] || null; setFile(f); setRaw(null); setRows(null); setDocText(""); setDocPages([]); + setAlleEinstellungen(false); if (f) formatErkennen(f); }} /> @@ -604,6 +636,43 @@ export default function Import() {

{t("import.docHint")}

)} + {/* Was erkannt wurde, gehoert hierher – direkt unter die Datei, auf + die es sich bezieht. Vorher stand es weit unten zwischen den + Spaltenfeldern, wo es niemand suchte. */} + {mode === "csv" && erkannt && ( +
+ + {erkannt.ok + ? (erkannt.preset + ? t("import.detectedBank", { name: erkannt.preset }) + : t("import.detectedGeneric")) + : t("import.detectFailed")} + + {/* Im Normalfall genuegt „erkannt". Wer wissen will, WAS erkannt + wurde und warum es so aussieht, wie es aussieht, klappt auf – + und findet dort auch jedes Feld zum Aendern. */} + {(!sicherErkannt || alleEinstellungen) && ( +

+ {erkannt.begruendung || erkannt.grund} +

+ )} + {erkannt.ok && erkannt.sicherheit !== "hoch" && ( +

+ {t("import.detectUncertain")} +

+ )} + {sicherErkannt && ( + + )} +
+ )}
- {/* Schritt 2: Format (nur CSV) */} - {mode === "csv" && ( + {/* Schritt 2: Format (nur CSV) – eingeklappt, solange nichts einzustellen + ist. Erreichbar ueber „Alle Einstellungen anzeigen" in Schritt 1. */} + {mode === "csv" && zeigeEinstellungen && (

{t("import.step2")}

@@ -704,9 +774,13 @@ export default function Import() {
)} - {/* Schritt 3: Spaltenzuordnung */} - {raw && ( + {/* Schritt 3: Spaltenzuordnung. + Die Karte bleibt stehen, wenn ein Zeilenfilter noetig sein koennte – + der darf NIE mit eingeklappt werden: Revolut legt alle Pockets in + eine Datei, und ohne Filter zaehlen die Bewegungen doppelt. */} + {raw && (zeigeEinstellungen || filterSpalten.length > 0) && (
+ {zeigeEinstellungen && (<>

{t("import.step3")}

{t("import.rawHint")}

@@ -741,6 +815,8 @@ export default function Import() { ))}
+ )} + {/* 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. */} @@ -777,6 +853,7 @@ export default function Import() {
)} + {zeigeEinstellungen && (<>
{t("import.moreColumns")}

{t("import.ibanHint")}

@@ -800,37 +877,24 @@ export default function Import() {

{t("import.extraColsHint")}

- {erkannt && ( -
- - {erkannt.ok - ? (erkannt.preset - ? t("import.detectedBank", { name: erkannt.preset }) - : t("import.detectedGeneric")) - : t("import.detectFailed")} - -

- {erkannt.begruendung || erkannt.grund} -

- {erkannt.ok && erkannt.sicherheit !== "hoch" && ( -

- {t("import.detectUncertain")} -

- )} -
- )} + {/* Eine Vorlage zu speichern ist Pflege, nicht der naechste Schritt. + Als primaere Aktion stand sie neben „Vorschau erstellen" und sah + aus wie eine Entscheidung, die jetzt ansteht. */} + {/* Ohne den Knopf „Vorschau erstellen" muss gesagt werden, warum + noch nichts kommt. Sonst wartet man auf etwas, das nie laeuft. */} + {!vorschauBereit && ( +

+ {t("import.makePreviewHint")} +

+ )} -
- -
- setSaveName(e.target.value)} /> - -
+
+ setSaveName(e.target.value)} /> +
-

{t("import.makePreviewNote")}

+ )}
)}