feat: Format wird beim Dateiwaehlen automatisch erkannt

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 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-22 21:19:46 +02:00
co-authored by Claude Opus 5
parent e6801b0474
commit 3b034dc001
4 changed files with 66 additions and 3 deletions
+55 -1
View File
@@ -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() {
<span>{t("import.file")}</span>
<input ref={fileRef} type="file"
accept={mode === "doc" ? ".pdf,application/pdf,image/*" : ".csv,text/csv,text/plain"}
onChange={(e) => { 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);
}} />
</label>
</div>
{mode === "doc" && (
@@ -656,6 +689,27 @@ export default function Import() {
<p className="muted small">{t("import.extraColsHint")}</p>
</details>
{erkannt && (
<div className={erkannt.ok ? "alert" : "alert error"}
style={{ display: "block", marginBottom: "var(--sp-3)" }}>
<strong>
{erkannt.ok
? (erkannt.preset
? t("import.detectedBank", { name: erkannt.preset })
: t("import.detectedGeneric"))
: t("import.detectFailed")}
</strong>
<p className="muted small" style={{ margin: "4px 0 0" }}>
{erkannt.begruendung || erkannt.grund}
</p>
{erkannt.ok && erkannt.sicherheit !== "hoch" && (
<p className="muted small" style={{ margin: "4px 0 0" }}>
{t("import.detectUncertain")}
</p>
)}
</div>
)}
<div className="field-inline" style={{ marginTop: "var(--sp-3)" }}>
<button className="btn primary" onClick={loadPreview} disabled={busy}>
<Icon name="chevron-right" size={15} /> {t("import.makePreview")}