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" && (
)}
{/* Extrahierter Text (PDF/Bild) */}
{mode === "doc" && docText && (
{t("import.extractedText")}
{docText}
)}
{/* Schritt 3: Spaltenzuordnung */}
{raw && (
{t("import.step3")}
{colOptions.map((c) => | {c.label} | )}
{raw.rows.map((r, ri) => (
{colOptions.map((c) => | {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]) => (
))}
)}
{/* Schritt 4: Prüfansicht */}
{rows && (
{t("import.step4")}
)}
{/* 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")}}
|
))}
)}
);
}