import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { api } from "../api"; import { useSettings } from "../settings"; import { useConfirm } from "../confirm"; import { useToast } from "../toast"; import Icon from "../components/Icon"; import { formatMoney } from "../format"; const DATE_FORMATS = [ { v: "%d.%m.%Y", l: "TT.MM.JJJJ" }, { v: "%Y-%m-%d", l: "JJJJ-MM-TT" }, { v: "%d/%m/%Y", l: "TT/MM/JJJJ" }, { v: "%m/%d/%Y", l: "MM/TT/JJJJ" }, { v: "%d.%m.%y", l: "TT.MM.JJ" }, ]; function defaultConfig() { return { delimiter: ";", decimal_sep: ",", date_format: "%d.%m.%Y", encoding: "utf-8", has_header: true, skip_rows: 0, amount_mode: "signed", column_map: {}, }; } export default function Import() { const { t } = useTranslation(); const { formatDate } = useSettings(); const confirm = useConfirm(); const toast = useToast(); const fileRef = useRef(null); const [accounts, setAccounts] = useState([]); const [groups, setGroups] = useState([]); const [templates, setTemplates] = useState([]); const [presets, setPresets] = useState([]); const [batches, setBatches] = useState([]); const [mode, setMode] = useState("csv"); // "csv" | "doc" const [accountId, setAccountId] = useState(""); const [file, setFile] = useState(null); const [config, setConfig] = useState(defaultConfig()); const [raw, setRaw] = useState(null); const [rows, setRows] = useState(null); const [docText, setDocText] = useState(""); const [busy, setBusy] = useState(false); const [saveName, setSaveName] = useState(""); const accountById = useMemo(() => Object.fromEntries(accounts.map((a) => [a.id, a])), [accounts]); async function loadStamm() { try { const [a, g, tp, pr, b] = await Promise.all([ api.listAccounts(), api.listCategories(), api.importTemplates(), api.importPresets(), api.importBatches(), ]); setAccounts(a); setGroups(g); setTemplates(tp); setPresets(pr); setBatches(b); if (a[0] && !accountId) setAccountId(String(a[0].id)); } catch (e) { toast(e.message, "error"); } } useEffect(() => { loadStamm(); }, []); function setCfg(patch) { setConfig((c) => ({ ...c, ...patch })); } function setCol(field, value) { setConfig((c) => ({ ...c, column_map: { ...c.column_map, [field]: value === "" ? null : Number(value) } })); } function applyConfig(src) { setConfig({ delimiter: src.delimiter, decimal_sep: src.decimal_sep, date_format: src.date_format, encoding: src.encoding, has_header: src.has_header, skip_rows: src.skip_rows, 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); } function baseForm() { const fd = new FormData(); fd.append("file", file); fd.append("delimiter", config.delimiter); fd.append("encoding", config.encoding); fd.append("has_header", config.has_header ? "true" : "false"); fd.append("skip_rows", String(config.skip_rows || 0)); return fd; } async function loadRaw() { if (!file) { toast(t("import.needFile"), "error"); return; } setBusy(true); try { setRaw(await api.importRaw(baseForm())); setRows(null); } catch (e) { toast(e.message, "error"); } finally { setBusy(false); } } async function loadPreview() { if (!file || !accountId) { toast(t("import.needFileAccount"), "error"); return; } setBusy(true); try { const fd = baseForm(); fd.append("account_id", accountId); fd.append("decimal_sep", config.decimal_sep); fd.append("date_format", config.date_format); fd.append("amount_mode", config.amount_mode); fd.append("column_map", JSON.stringify(config.column_map)); const res = await api.importPreview(fd); setRows(res.rows.map((r) => ({ ...r }))); } catch (e) { toast(e.message, "error"); } finally { setBusy(false); } } async function loadDoc() { if (!file || !accountId) { toast(t("import.needFileAccount"), "error"); return; } setBusy(true); try { const fd = new FormData(); fd.append("file", file); fd.append("account_id", accountId); const res = await api.documentPreview(fd); setRows(res.rows.map((r) => ({ ...r }))); setDocText(res.text || ""); } catch (e) { toast(e.message, "error"); } finally { setBusy(false); } } async function commit() { setBusy(true); try { const batch = await api.importCommit({ account_id: Number(accountId), template_id: null, filename: file?.name || "", rows, }); toast(t("import.imported", { n: batch.imported_count })); setRows(null); setRaw(null); setFile(null); 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; } try { await api.createImportTemplate({ name: saveName.trim(), ...config }); setSaveName(""); setTemplates(await api.importTemplates()); toast(t("settings.saved")); } catch (e) { toast(e.message, "error"); } } async function undo(b) { if (!(await confirm({ title: t("import.undo"), message: t("import.undoConfirm", { file: b.filename }), danger: true, confirmLabel: t("import.undo") }))) return; try { await api.undoImportBatch(b.id); loadStamm(); } catch (e) { toast(e.message, "error"); } } const colOptions = useMemo(() => { const n = raw ? Math.max(raw.header?.length || 0, ...(raw.rows || []).map((r) => r.length), 0) : 0; return Array.from({ length: n }, (_, i) => ({ i, label: `${i}${raw?.header?.[i] ? ` · ${raw.header[i]}` : ""}`, })); }, [raw]); const expenseGroups = useMemo(() => groups.filter((g) => g.kind === "expense"), [groups]); const incomeGroups = useMemo(() => groups.filter((g) => g.kind === "income"), [groups]); const includeCount = rows ? rows.filter((r) => r.include).length : 0; function updateRow(idx, patch) { setRows((rs) => rs.map((r, i) => (i === idx ? { ...r, ...patch } : r))); } const acc = accountById[Number(accountId)]; return (

{t("import.title")}

{/* Schritt 1: Konto + Datei */}

{t("import.step1")}

{mode === "doc" && ( <>

{t("import.docHint")}

)}
{presets.length > 0 && ( )} {templates.length > 0 && ( )}
{/* Schritt 2: Format (nur CSV) */} {mode === "csv" && (

{t("import.step2")}

)} {/* Extrahierter Text (PDF/Bild) */} {mode === "doc" && docText && (
{t("import.extractedText")}
{docText}
)} {/* Schritt 3: Spaltenzuordnung */} {raw && (

{t("import.step3")}

{colOptions.map((c) => )} {raw.rows.map((r, ri) => ( {colOptions.map((c) => )} ))}
{c.label}
{r[c.i] ?? ""}
{[ ["date", t("import.colDate"), true], ...(config.amount_mode === "debit_credit" ? [["debit", t("import.colDebit"), true], ["credit", t("import.colCredit"), true]] : [["amount", t("import.colAmount"), true]]), ["payee", t("import.colPayee"), false], ["purpose", t("import.colPurpose"), false], ["counterparty_iban", t("import.colIban"), false], ].map(([field, label]) => ( ))}
setSaveName(e.target.value)} />
)} {/* Schritt 4: Prüfansicht */} {rows && (

{t("import.step4")}

{rows.map((r, idx) => ( ))}
{t("transactions.date")} {t("transactions.amount")} {t("import.typeCol")} {t("import.payeeCol")} {t("transactions.category")}
updateRow(idx, { include: e.target.checked })} style={{ width: "auto" }} /> {r.date ? formatDate(r.date) : {t("import.badDate")}} {formatMoney(Math.abs(r.amount_cents || 0), acc?.currency || "EUR")} {r.payee || r.purpose || ""} {r.is_duplicate && {t("import.duplicate")}} {r.type === "transfer" ? ( ) : ( )}
)} {/* Verlauf */} {batches.length > 0 && (

{t("import.history")}

{batches.map((b) => ( ))}
{b.filename || "—"} {accountById[b.account_id]?.name} {formatDate(b.created_at)} {t("import.importedCount", { n: b.imported_count })} {b.status === "committed" ? ( ) : {t("import.undone")}}
)}
); }