Simpl-Resultat/src/hooks/useDataImport.ts
Le-King-Fu e23e559ee3 feat: make settings data imports visible in Import History
Create import_sources + imported_files tracking records when importing
transactions from Settings > Data Management, so imports appear in the
Import History panel and can be deleted like CSV imports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:38:51 +00:00

196 lines
5.4 KiB
TypeScript

import { useReducer, useCallback } from "react";
import { invoke } from "@tauri-apps/api/core";
import {
parseImportedJson,
parseImportedCsv,
importCategoriesOnly,
importTransactionsWithCategories,
importTransactionsOnly,
type ExportEnvelope,
type ImportSummary,
} from "../services/dataExportService";
type ImportStatus =
| "idle"
| "reading"
| "needsPassword"
| "confirming"
| "importing"
| "success"
| "error";
interface ImportState {
status: ImportStatus;
filePath: string | null;
summary: ImportSummary | null;
parsedData: ExportEnvelope["data"] | null;
importType: ExportEnvelope["export_type"] | null;
error: string | null;
}
type ImportAction =
| { type: "READ_START" }
| { type: "NEEDS_PASSWORD"; filePath: string }
| {
type: "CONFIRMING";
filePath: string;
summary: ImportSummary;
data: ExportEnvelope["data"];
importType: ExportEnvelope["export_type"];
}
| { type: "IMPORT_START" }
| { type: "IMPORT_SUCCESS" }
| { type: "IMPORT_ERROR"; error: string }
| { type: "RESET" };
const initialState: ImportState = {
status: "idle",
filePath: null,
summary: null,
parsedData: null,
importType: null,
error: null,
};
function reducer(state: ImportState, action: ImportAction): ImportState {
switch (action.type) {
case "READ_START":
return { ...initialState, status: "reading" };
case "NEEDS_PASSWORD":
return { ...initialState, status: "needsPassword", filePath: action.filePath };
case "CONFIRMING":
return {
...state,
status: "confirming",
filePath: action.filePath,
summary: action.summary,
parsedData: action.data,
importType: action.importType,
error: null,
};
case "IMPORT_START":
return { ...state, status: "importing", error: null };
case "IMPORT_SUCCESS":
return { ...state, status: "success", error: null };
case "IMPORT_ERROR":
return { ...state, status: "error", error: action.error };
case "RESET":
return initialState;
}
}
function parseContent(
content: string,
filePath: string
): { summary: ImportSummary; data: ExportEnvelope["data"]; importType: ExportEnvelope["export_type"] } {
const isCsv =
filePath.toLowerCase().endsWith(".csv") ||
(!filePath.toLowerCase().endsWith(".json") &&
!filePath.toLowerCase().endsWith(".sref") &&
content.trimStart().charAt(0) !== "{");
if (isCsv) {
const { transactions, summary } = parseImportedCsv(content);
return {
summary,
data: { transactions },
importType: "transactions_only",
};
}
const { envelope, summary } = parseImportedJson(content);
return {
summary,
data: envelope.data,
importType: envelope.export_type,
};
}
export function useDataImport() {
const [state, dispatch] = useReducer(reducer, initialState);
const pickAndRead = useCallback(async () => {
dispatch({ type: "READ_START" });
try {
const filePath = await invoke<string | null>("pick_import_file", {
filters: [["Simpl'Result Files", ["json", "csv", "sref"]]],
});
if (!filePath) {
dispatch({ type: "RESET" });
return;
}
const encrypted = await invoke<boolean>("is_file_encrypted", { filePath });
if (encrypted) {
dispatch({ type: "NEEDS_PASSWORD", filePath });
return;
}
const content = await invoke<string>("read_import_file", {
filePath,
password: null,
});
const { summary, data, importType } = parseContent(content, filePath);
dispatch({ type: "CONFIRMING", filePath, summary, data, importType });
} catch (e) {
dispatch({
type: "IMPORT_ERROR",
error: e instanceof Error ? e.message : String(e),
});
}
}, []);
const readWithPassword = useCallback(
async (password: string) => {
if (!state.filePath) return;
dispatch({ type: "READ_START" });
try {
const content = await invoke<string>("read_import_file", {
filePath: state.filePath,
password,
});
const { summary, data, importType } = parseContent(content, state.filePath);
dispatch({ type: "CONFIRMING", filePath: state.filePath, summary, data, importType });
} catch (e) {
dispatch({
type: "IMPORT_ERROR",
error: e instanceof Error ? e.message : String(e),
});
}
},
[state.filePath]
);
const executeImport = useCallback(async () => {
if (!state.parsedData || !state.importType) return;
dispatch({ type: "IMPORT_START" });
const filename = state.filePath?.split(/[/\\]/).pop() ?? "unknown";
try {
switch (state.importType) {
case "categories_only":
await importCategoriesOnly(state.parsedData);
break;
case "transactions_with_categories":
await importTransactionsWithCategories(state.parsedData, filename);
break;
case "transactions_only":
await importTransactionsOnly(state.parsedData, filename);
break;
}
dispatch({ type: "IMPORT_SUCCESS" });
} catch (e) {
dispatch({
type: "IMPORT_ERROR",
error: e instanceof Error ? e.message : String(e),
});
}
}, [state.parsedData, state.importType, state.filePath]);
const reset = useCallback(() => dispatch({ type: "RESET" }), []);
return { state, pickAndRead, readWithPassword, executeImport, reset };
}