diff --git a/web/src/finance.css b/web/src/finance.css index 6210c3c..c868b50 100644 --- a/web/src/finance.css +++ b/web/src/finance.css @@ -656,6 +656,27 @@ tr.row-dup { border-radius: var(--radius-sm); margin-top: var(--sp-2); } +/* Beleg-Seiten als Bildstreifen (horizontal scrollbar). */ +.doc-pages { + display: flex; + gap: var(--sp-3); + overflow-x: auto; + padding-bottom: var(--sp-2); +} +.doc-page { + height: 440px; + width: auto; + flex: 0 0 auto; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: #fff; +} +.import-more { + margin-top: var(--sp-3); +} +.import-more > summary { + cursor: pointer; +} /* DataTable-Zellen einzeilig halten, damit „…" greift (Excel-artig). */ .datatable td { white-space: nowrap; vertical-align: middle; } diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 75a517f..29cb451 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -266,7 +266,15 @@ "file": "Datei", "preset": "Bank-Vorlage (vorgefertigt)", "template": "Eigene Vorlage", + "templateAny": "Vorlage", + "presetGroup": "Bank-Vorlagen", + "ownGroup": "Eigene Vorlagen", "templateManual": "Manuell einstellen", + "overwriteConfirm": "Vorlage „{{name}}“ mit den aktuellen Einstellungen überschreiben?", + "autoPreviewNote": "Die Spaltenvorschau erscheint automatisch, sobald Datei und Format passen.", + "moreColumns": "Weitere Spalten (optional)", + "ibanHint": "IBAN der Gegenseite – nur nötig, wenn dein Export eine Spalte mit der IBAN des anderen Kontos enthält; dient der automatischen Transfer-Erkennung.", + "belegPreview": "Beleg", "delimiter": "Trennzeichen", "decimal": "Dezimaltrenner", "dateFormat": "Datumsformat", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index b980450..733d269 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -266,7 +266,15 @@ "file": "File", "preset": "Bank preset", "template": "My template", + "templateAny": "Template", + "presetGroup": "Bank templates", + "ownGroup": "My templates", "templateManual": "Configure manually", + "overwriteConfirm": "Overwrite template “{{name}}” with the current settings?", + "autoPreviewNote": "The column preview appears automatically once the file and format are set.", + "moreColumns": "More columns (optional)", + "ibanHint": "Counterparty IBAN – only needed if your export has a column with the other account's IBAN; used for automatic transfer detection.", + "belegPreview": "Document", "delimiter": "Delimiter", "decimal": "Decimal separator", "dateFormat": "Date format", diff --git a/web/src/pages/Import.jsx b/web/src/pages/Import.jsx index d325559..592a9a0 100644 --- a/web/src/pages/Import.jsx +++ b/web/src/pages/Import.jsx @@ -50,6 +50,7 @@ export default function Import() { const [raw, setRaw] = useState(null); const [rows, setRows] = useState(null); const [docText, setDocText] = useState(""); + const [docPages, setDocPages] = useState([]); const [busy, setBusy] = useState(false); const [saveName, setSaveName] = useState(""); @@ -66,6 +67,13 @@ export default function Import() { } useEffect(() => { loadStamm(); }, []); + // CSV: Spaltenvorschau automatisch erzeugen, sobald Datei/Format bereitstehen – + // kein separater „Datei einlesen"-Klick mehr nötig. + useEffect(() => { + if (mode === "csv" && file) loadRaw(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [file, mode, config.delimiter, config.encoding, config.has_header, config.skip_rows]); + function setCfg(patch) { setConfig((c) => ({ ...c, ...patch })); } function setCol(field, value) { setConfig((c) => ({ ...c, column_map: { ...c.column_map, [field]: value === "" ? null : Number(value) } })); @@ -78,13 +86,17 @@ export default function Import() { amount_mode: src.amount_mode, column_map: src.column_map || {}, }); } - function applyTemplate(id) { - const tpl = templates.find((x) => String(x.id) === String(id)); - if (tpl) applyConfig(tpl); - } - function applyPreset(idx) { - const p = presets[Number(idx)]; - if (p) applyConfig(p); + // Ein zusammengeführtes Vorlagen-Dropdown: Werte typ-getaggt "preset:i" / "tpl:id". + function applySource(value) { + if (!value) return; + const [kind, id] = value.split(":"); + if (kind === "preset") { + const p = presets[Number(id)]; + if (p) { applyConfig(p); setSaveName(p.name); } + } else if (kind === "tpl") { + const tpl = templates.find((x) => String(x.id) === String(id)); + if (tpl) { applyConfig(tpl); setSaveName(tpl.name); } // Name vorbelegen -> Speichern überschreibt + } } function baseForm() { @@ -131,6 +143,7 @@ export default function Import() { const res = await api.documentPreview(fd); setRows(res.rows.map((r) => ({ ...r }))); setDocText(res.text || ""); + setDocPages(res.pages || []); } catch (e) { toast(e.message, "error"); } finally { setBusy(false); } } @@ -144,17 +157,24 @@ export default function Import() { rows, }); toast(t("import.imported", { n: batch.imported_count })); - setRows(null); setRaw(null); setFile(null); + setRows(null); setRaw(null); setFile(null); setDocText(""); setDocPages([]); if (fileRef.current) fileRef.current.value = ""; loadStamm(); } catch (e) { toast(e.message, "error"); } finally { setBusy(false); } } async function saveTemplate() { - if (!saveName.trim()) { toast(t("import.templateName"), "error"); return; } + const name = saveName.trim(); + if (!name) { toast(t("import.templateName"), "error"); return; } + const existing = templates.find((tp) => tp.name.toLowerCase() === name.toLowerCase()); + if (existing && !(await confirm({ + title: t("import.saveTemplate"), + message: t("import.overwriteConfirm", { name: existing.name }), + confirmLabel: t("common.save"), + }))) return; try { - await api.createImportTemplate({ name: saveName.trim(), ...config }); - setSaveName(""); + if (existing) await api.updateImportTemplate(existing.id, { name, ...config }); + else await api.createImportTemplate({ name, ...config }); setTemplates(await api.importTemplates()); toast(t("settings.saved")); } catch (e) { toast(e.message, "error"); } @@ -292,9 +312,9 @@ export default function Import() {
{t("import.autoPreviewNote")}
)} - {/* Extrahierter Text (PDF/Bild) */} - {mode === "doc" && docText && ( + {/* Beleg-Vorschau: Seiten als Bild + extrahierter Text (PDF/Bild) */} + {mode === "doc" && (docPages.length > 0 || docText) && ({docText}
- {docText}
+ {t("import.ibanHint")}
+