From 3b034dc001bada950234544aa325d4b8de853c2c Mon Sep 17 00:00:00 2001 From: Guido Date: Sat, 22 Aug 2026 21:19:46 +0200 Subject: [PATCH] feat: Format wird beim Dateiwaehlen automatisch erkannt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Waehlt man eine Datei (oder uebernimmt eine vom Telefon), erkennt Certain Trennzeichen, Dezimalzeichen, Datumsformat und Spaltenzuordnung selbst und traegt sie ein. Darueber steht, WAS erkannt wurde – "Format erkannt: Revolut" oder die Spaltenzuordnung im Klartext –, damit man es pruefen kann, statt ihm blind zu vertrauen. War etwas nicht eindeutig, steht das ausdruecklich da mit dem Hinweis, die Zuordnung unten anzusehen. Das Formular bleibt also erreichbar, muss aber im Normalfall nicht mehr angefasst werden. Co-Authored-By: Claude Opus 5 --- web/src/api.js | 1 + web/src/locales/de.json | 6 ++++- web/src/locales/en.json | 6 ++++- web/src/pages/Import.jsx | 56 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/web/src/api.js b/web/src/api.js index e395918..c9e5fa4 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -174,6 +174,7 @@ export const api = { request(`/inbox/${id}/category/${categoryId}`, { method: "POST" }), bookInboxItem: (id) => request(`/inbox/${id}/book`, { method: "POST" }), bookAllInbox: () => request("/inbox/book-all", { method: "POST" }), + detectFormat: (formData) => request("/imports/detect", { method: "POST", formData }), listUploads: () => request("/imports/uploads"), deleteUpload: (id) => request(`/imports/uploads/${id}`, { method: "DELETE" }), diff --git a/web/src/locales/de.json b/web/src/locales/de.json index e1fb48f..fea36c1 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -374,7 +374,11 @@ "usePhoneFile": "Übernehmen", "phoneFileLoaded": "„{{name}}“ geladen – jetzt Format prüfen und Vorschau erzeugen.", "discardPhoneFile": "Datei verwerfen", - "discardPhoneFileConfirm": "„{{name}}“ verwerfen? Die Datei wird gelöscht, gebucht war ohnehin nichts." + "discardPhoneFileConfirm": "„{{name}}“ verwerfen? Die Datei wird gelöscht, gebucht war ohnehin nichts.", + "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." }, "password": { "show": "Passwort anzeigen", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index e9d9ab3..7a277e3 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -374,7 +374,11 @@ "usePhoneFile": "Take over", "phoneFileLoaded": "“{{name}}” loaded – check the format and build the preview.", "discardPhoneFile": "Discard file", - "discardPhoneFileConfirm": "Discard “{{name}}”? The file is deleted; nothing was booked anyway." + "discardPhoneFileConfirm": "Discard “{{name}}”? The file is deleted; nothing was booked anyway.", + "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." }, "password": { "show": "Show password", diff --git a/web/src/pages/Import.jsx b/web/src/pages/Import.jsx index e414183..6dc31cd 100644 --- a/web/src/pages/Import.jsx +++ b/web/src/pages/Import.jsx @@ -60,6 +60,8 @@ export default function Import() { const [raw, setRaw] = useState(null); // Dateien, die per Kurzbefehl vom Telefon hereinkamen. const [uploads, setUploads] = useState([]); + // Ergebnis der automatischen Formaterkennung. + const [erkannt, setErkannt] = useState(null); const [quelle, setQuelle] = useState(""); // "preset:i" | "tpl:id" | "" const [rows, setRows] = useState(null); const [recon, setRecon] = useState(null); @@ -248,6 +250,31 @@ export default function Import() { // Die abgelieferte Datei so behandeln, als haette man sie hier ausgewaehlt – // damit gilt der ganze normale Weg samt Vorschau und Saldo-Abgleich. + // Format aus der Datei ableiten, damit niemand Spalten von Hand zuordnet. + // Das war die haeufigste Fehlerquelle: falsches Datumsformat = keine Zeile + // gueltig, falscher Dezimaltrenner = Betraege um Faktor 100 daneben. + async function formatErkennen(f) { + setErkannt(null); + try { + const daten = new FormData(); + daten.append("file", f); + const r = await api.detectFormat(daten); + setErkannt(r); + if (r.ok && r.config) { + const c = r.config; + setCfg({ + delimiter: c.delimiter, decimal_sep: c.decimal_sep, date_format: c.date_format, + encoding: c.encoding, has_header: c.has_header, skip_rows: c.skip_rows, + amount_mode: c.amount_mode, + column_map: Object.fromEntries( + Object.entries(c.column_map).filter(([, v]) => v !== null && v !== undefined)), + }); + } + } catch (e) { + setErkannt({ ok: false, grund: e.message }); + } + } + async function uploadUebernehmen(u) { try { const antwort = await fetch(`/api/imports/uploads/${u.id}/content`, { credentials: "include" }); @@ -258,6 +285,8 @@ export default function Import() { setRaw(null); setRows(null); setMode("csv"); + const datei = new File([blob], u.filename, { type: "text/csv" }); + formatErkennen(datei); toast(t("import.phoneFileLoaded", { name: u.filename })); } catch (e) { toast(e.message, "error"); @@ -450,7 +479,11 @@ export default function Import() { {t("import.file")} { setFile(e.target.files?.[0] || null); setRaw(null); setRows(null); setDocText(""); setDocPages([]); }} /> + onChange={(e) => { + const f = e.target.files?.[0] || null; + setFile(f); setRaw(null); setRows(null); setDocText(""); setDocPages([]); + if (f) formatErkennen(f); + }} /> {mode === "doc" && ( @@ -656,6 +689,27 @@ 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")} +

+ )} +
+ )} +