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:
@@ -174,6 +174,7 @@ export const api = {
|
|||||||
request(`/inbox/${id}/category/${categoryId}`, { method: "POST" }),
|
request(`/inbox/${id}/category/${categoryId}`, { method: "POST" }),
|
||||||
bookInboxItem: (id) => request(`/inbox/${id}/book`, { method: "POST" }),
|
bookInboxItem: (id) => request(`/inbox/${id}/book`, { method: "POST" }),
|
||||||
bookAllInbox: () => request("/inbox/book-all", { method: "POST" }),
|
bookAllInbox: () => request("/inbox/book-all", { method: "POST" }),
|
||||||
|
detectFormat: (formData) => request("/imports/detect", { method: "POST", formData }),
|
||||||
listUploads: () => request("/imports/uploads"),
|
listUploads: () => request("/imports/uploads"),
|
||||||
deleteUpload: (id) => request(`/imports/uploads/${id}`, { method: "DELETE" }),
|
deleteUpload: (id) => request(`/imports/uploads/${id}`, { method: "DELETE" }),
|
||||||
|
|
||||||
|
|||||||
@@ -374,7 +374,11 @@
|
|||||||
"usePhoneFile": "Übernehmen",
|
"usePhoneFile": "Übernehmen",
|
||||||
"phoneFileLoaded": "„{{name}}“ geladen – jetzt Format prüfen und Vorschau erzeugen.",
|
"phoneFileLoaded": "„{{name}}“ geladen – jetzt Format prüfen und Vorschau erzeugen.",
|
||||||
"discardPhoneFile": "Datei verwerfen",
|
"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": {
|
"password": {
|
||||||
"show": "Passwort anzeigen",
|
"show": "Passwort anzeigen",
|
||||||
|
|||||||
@@ -374,7 +374,11 @@
|
|||||||
"usePhoneFile": "Take over",
|
"usePhoneFile": "Take over",
|
||||||
"phoneFileLoaded": "“{{name}}” loaded – check the format and build the preview.",
|
"phoneFileLoaded": "“{{name}}” loaded – check the format and build the preview.",
|
||||||
"discardPhoneFile": "Discard file",
|
"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": {
|
"password": {
|
||||||
"show": "Show password",
|
"show": "Show password",
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export default function Import() {
|
|||||||
const [raw, setRaw] = useState(null);
|
const [raw, setRaw] = useState(null);
|
||||||
// Dateien, die per Kurzbefehl vom Telefon hereinkamen.
|
// Dateien, die per Kurzbefehl vom Telefon hereinkamen.
|
||||||
const [uploads, setUploads] = useState([]);
|
const [uploads, setUploads] = useState([]);
|
||||||
|
// Ergebnis der automatischen Formaterkennung.
|
||||||
|
const [erkannt, setErkannt] = useState(null);
|
||||||
const [quelle, setQuelle] = useState(""); // "preset:i" | "tpl:id" | ""
|
const [quelle, setQuelle] = useState(""); // "preset:i" | "tpl:id" | ""
|
||||||
const [rows, setRows] = useState(null);
|
const [rows, setRows] = useState(null);
|
||||||
const [recon, setRecon] = 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 –
|
// Die abgelieferte Datei so behandeln, als haette man sie hier ausgewaehlt –
|
||||||
// damit gilt der ganze normale Weg samt Vorschau und Saldo-Abgleich.
|
// 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) {
|
async function uploadUebernehmen(u) {
|
||||||
try {
|
try {
|
||||||
const antwort = await fetch(`/api/imports/uploads/${u.id}/content`, { credentials: "include" });
|
const antwort = await fetch(`/api/imports/uploads/${u.id}/content`, { credentials: "include" });
|
||||||
@@ -258,6 +285,8 @@ export default function Import() {
|
|||||||
setRaw(null);
|
setRaw(null);
|
||||||
setRows(null);
|
setRows(null);
|
||||||
setMode("csv");
|
setMode("csv");
|
||||||
|
const datei = new File([blob], u.filename, { type: "text/csv" });
|
||||||
|
formatErkennen(datei);
|
||||||
toast(t("import.phoneFileLoaded", { name: u.filename }));
|
toast(t("import.phoneFileLoaded", { name: u.filename }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(e.message, "error");
|
toast(e.message, "error");
|
||||||
@@ -450,7 +479,11 @@ export default function Import() {
|
|||||||
<span>{t("import.file")}</span>
|
<span>{t("import.file")}</span>
|
||||||
<input ref={fileRef} type="file"
|
<input ref={fileRef} type="file"
|
||||||
accept={mode === "doc" ? ".pdf,application/pdf,image/*" : ".csv,text/csv,text/plain"}
|
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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{mode === "doc" && (
|
{mode === "doc" && (
|
||||||
@@ -656,6 +689,27 @@ export default function Import() {
|
|||||||
<p className="muted small">{t("import.extraColsHint")}</p>
|
<p className="muted small">{t("import.extraColsHint")}</p>
|
||||||
</details>
|
</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)" }}>
|
<div className="field-inline" style={{ marginTop: "var(--sp-3)" }}>
|
||||||
<button className="btn primary" onClick={loadPreview} disabled={busy}>
|
<button className="btn primary" onClick={loadPreview} disabled={busy}>
|
||||||
<Icon name="chevron-right" size={15} /> {t("import.makePreview")}
|
<Icon name="chevron-right" size={15} /> {t("import.makePreview")}
|
||||||
|
|||||||
Reference in New Issue
Block a user