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.begruendung || erkannt.grund} +
+ {erkannt.ok && erkannt.sicherheit !== "hoch" && ( ++ {t("import.detectUncertain")} +
+ )} +