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:
@@ -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")}
|
||||
|
||||
Reference in New Issue
Block a user