diff --git a/src/components/import/ImportHistoryPanel.tsx b/src/components/import/ImportHistoryPanel.tsx
new file mode 100644
index 0000000..c8d527e
--- /dev/null
+++ b/src/components/import/ImportHistoryPanel.tsx
@@ -0,0 +1,118 @@
+import { useTranslation } from "react-i18next";
+import { Trash2, Inbox } from "lucide-react";
+import { useImportHistory } from "../../hooks/useImportHistory";
+
+interface ImportHistoryPanelProps {
+ onChanged?: () => void;
+}
+
+export default function ImportHistoryPanel({
+ onChanged,
+}: ImportHistoryPanelProps) {
+ const { t } = useTranslation();
+ const { state, handleDelete, handleDeleteAll } = useImportHistory(onChanged);
+
+ return (
+
+
+
+ {t("import.history.title")}
+
+ {state.files.length > 0 && (
+
+ )}
+
+
+ {state.error && (
+
{state.error}
+ )}
+
+ {state.isLoading ? (
+
+ {t("common.loading")}
+
+ ) : state.files.length === 0 ? (
+
+
+
+ {t("import.history.empty")}
+
+
+ ) : (
+
+
+
+
+ |
+ {t("import.history.source")}
+ |
+
+ {t("import.history.filename")}
+ |
+
+ {t("import.history.date")}
+ |
+
+ {t("import.history.rows")}
+ |
+
+ {t("import.history.status")}
+ |
+ |
+
+
+
+ {state.files.map((file) => (
+
+ | {file.source_name} |
+
+ {file.filename}
+ |
+
+ {new Date(file.import_date).toLocaleDateString()}
+ |
+ {file.row_count} |
+
+
+ {file.status}
+
+ |
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+ );
+}
diff --git a/src/hooks/useImportHistory.ts b/src/hooks/useImportHistory.ts
new file mode 100644
index 0000000..5837941
--- /dev/null
+++ b/src/hooks/useImportHistory.ts
@@ -0,0 +1,116 @@
+import { useReducer, useCallback, useEffect, useRef } from "react";
+import { useTranslation } from "react-i18next";
+import type { ImportedFileWithSource } from "../shared/types";
+import {
+ getAllImportedFiles,
+ deleteImportWithTransactions,
+ deleteAllImportsWithTransactions,
+} from "../services/importedFileService";
+
+interface ImportHistoryState {
+ files: ImportedFileWithSource[];
+ isLoading: boolean;
+ isDeleting: boolean;
+ error: string | null;
+}
+
+type ImportHistoryAction =
+ | { type: "SET_LOADING"; payload: boolean }
+ | { type: "SET_DELETING"; payload: boolean }
+ | { type: "SET_ERROR"; payload: string | null }
+ | { type: "SET_FILES"; payload: ImportedFileWithSource[] };
+
+const initialState: ImportHistoryState = {
+ files: [],
+ isLoading: false,
+ isDeleting: false,
+ error: null,
+};
+
+function reducer(
+ state: ImportHistoryState,
+ action: ImportHistoryAction
+): ImportHistoryState {
+ switch (action.type) {
+ case "SET_LOADING":
+ return { ...state, isLoading: action.payload };
+ case "SET_DELETING":
+ return { ...state, isDeleting: action.payload };
+ case "SET_ERROR":
+ return { ...state, error: action.payload };
+ case "SET_FILES":
+ return { ...state, files: action.payload };
+ default:
+ return state;
+ }
+}
+
+export function useImportHistory(onChanged?: () => void) {
+ const [state, dispatch] = useReducer(reducer, initialState);
+ const fetchIdRef = useRef(0);
+ const { t } = useTranslation();
+
+ const loadHistory = useCallback(async () => {
+ const fetchId = ++fetchIdRef.current;
+ dispatch({ type: "SET_LOADING", payload: true });
+ dispatch({ type: "SET_ERROR", payload: null });
+ try {
+ const files = await getAllImportedFiles();
+ if (fetchId !== fetchIdRef.current) return;
+ dispatch({ type: "SET_FILES", payload: files });
+ } catch (err) {
+ if (fetchId !== fetchIdRef.current) return;
+ dispatch({ type: "SET_ERROR", payload: String(err) });
+ } finally {
+ if (fetchId === fetchIdRef.current) {
+ dispatch({ type: "SET_LOADING", payload: false });
+ }
+ }
+ }, []);
+
+ const handleDelete = useCallback(
+ async (fileId: number, rowCount: number) => {
+ const ok = confirm(
+ t("import.history.deleteConfirm", { count: rowCount })
+ );
+ if (!ok) return;
+ dispatch({ type: "SET_DELETING", payload: true });
+ try {
+ await deleteImportWithTransactions(fileId);
+ await loadHistory();
+ onChanged?.();
+ } catch (err) {
+ dispatch({ type: "SET_ERROR", payload: String(err) });
+ } finally {
+ dispatch({ type: "SET_DELETING", payload: false });
+ }
+ },
+ [loadHistory, onChanged, t]
+ );
+
+ const handleDeleteAll = useCallback(async () => {
+ const ok = confirm(t("import.history.deleteAllConfirm"));
+ if (!ok) return;
+ dispatch({ type: "SET_DELETING", payload: true });
+ try {
+ await deleteAllImportsWithTransactions();
+ await loadHistory();
+ onChanged?.();
+ } catch (err) {
+ dispatch({ type: "SET_ERROR", payload: String(err) });
+ } finally {
+ dispatch({ type: "SET_DELETING", payload: false });
+ }
+ }, [loadHistory, onChanged, t]);
+
+ useEffect(() => {
+ loadHistory();
+ }, [loadHistory]);
+
+ return {
+ state,
+ loadHistory,
+ handleDelete,
+ handleDeleteAll,
+ };
+}
diff --git a/src/hooks/useImportWizard.ts b/src/hooks/useImportWizard.ts
index 76a29f6..a4d5ffd 100644
--- a/src/hooks/useImportWizard.ts
+++ b/src/hooks/useImportWizard.ts
@@ -535,7 +535,7 @@ export function useImportWizard() {
const hash = await invoke("hash_file", {
filePath: state.selectedFiles[0].file_path,
});
- const existing = await existsByHash(sourceId, hash);
+ const existing = await existsByHash(hash);
if (existing) {
fileAlreadyImported = true;
existingFileId = existing.id;
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 7ec0d4c..0687271 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -122,6 +122,18 @@
"errorMessage": "Error message",
"done": "Done"
},
+ "history": {
+ "title": "Import History",
+ "empty": "No imports yet.",
+ "deleteAll": "Delete All",
+ "deleteConfirm": "Delete this import and its {{count}} transaction(s)?",
+ "deleteAllConfirm": "Delete ALL imports and their transactions? This cannot be undone.",
+ "source": "Source",
+ "filename": "File",
+ "date": "Date",
+ "rows": "Rows",
+ "status": "Status"
+ },
"wizard": {
"back": "Back",
"next": "Next",
diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json
index cfdaf0a..855f020 100644
--- a/src/i18n/locales/fr.json
+++ b/src/i18n/locales/fr.json
@@ -122,6 +122,18 @@
"errorMessage": "Message d'erreur",
"done": "Terminé"
},
+ "history": {
+ "title": "Historique des imports",
+ "empty": "Aucun import pour le moment.",
+ "deleteAll": "Tout supprimer",
+ "deleteConfirm": "Supprimer cet import et ses {{count}} transaction(s) ?",
+ "deleteAllConfirm": "Supprimer TOUS les imports et leurs transactions ? Cette action est irréversible.",
+ "source": "Source",
+ "filename": "Fichier",
+ "date": "Date",
+ "rows": "Lignes",
+ "status": "Statut"
+ },
"wizard": {
"back": "Retour",
"next": "Suivant",
diff --git a/src/pages/ImportPage.tsx b/src/pages/ImportPage.tsx
index dd94c2b..0b32b5f 100644
--- a/src/pages/ImportPage.tsx
+++ b/src/pages/ImportPage.tsx
@@ -9,6 +9,7 @@ import ImportConfirmation from "../components/import/ImportConfirmation";
import ImportProgress from "../components/import/ImportProgress";
import ImportReportPanel from "../components/import/ImportReportPanel";
import WizardNavigation from "../components/import/WizardNavigation";
+import ImportHistoryPanel from "../components/import/ImportHistoryPanel";
import { AlertCircle } from "lucide-react";
export default function ImportPage() {
@@ -53,12 +54,15 @@ export default function ImportPage() {
{/* Wizard steps */}
{state.step === "source-list" && (
-
+ <>
+
+
+ >
)}
{state.step === "source-config" && state.selectedSource && (
diff --git a/src/services/importedFileService.ts b/src/services/importedFileService.ts
index 9eefd49..2b22145 100644
--- a/src/services/importedFileService.ts
+++ b/src/services/importedFileService.ts
@@ -1,5 +1,5 @@
import { getDb } from "./db";
-import type { ImportedFile } from "../shared/types";
+import type { ImportedFile, ImportedFileWithSource } from "../shared/types";
export async function getFilesBySourceId(
sourceId: number
@@ -12,13 +12,12 @@ export async function getFilesBySourceId(
}
export async function existsByHash(
- sourceId: number,
fileHash: string
): Promise {
const db = await getDb();
const rows = await db.select(
- "SELECT * FROM imported_files WHERE source_id = $1 AND file_hash = $2",
- [sourceId, fileHash]
+ "SELECT * FROM imported_files WHERE file_hash = $1",
+ [fileHash]
);
return rows.length > 0 ? rows[0] : null;
}
@@ -72,3 +71,32 @@ export async function updateFileStatus(
[status, rowCount ?? null, notes ?? null, id]
);
}
+
+export async function getAllImportedFiles(): Promise {
+ const db = await getDb();
+ return db.select(
+ `SELECT f.*, s.name AS source_name
+ FROM imported_files f
+ JOIN import_sources s ON s.id = f.source_id
+ ORDER BY f.import_date DESC`
+ );
+}
+
+export async function deleteImportWithTransactions(
+ fileId: number
+): Promise {
+ const db = await getDb();
+ const result = await db.execute(
+ "DELETE FROM transactions WHERE file_id = $1",
+ [fileId]
+ );
+ await db.execute("DELETE FROM imported_files WHERE id = $1", [fileId]);
+ return result.rowsAffected;
+}
+
+export async function deleteAllImportsWithTransactions(): Promise {
+ const db = await getDb();
+ const result = await db.execute("DELETE FROM transactions");
+ await db.execute("DELETE FROM imported_files");
+ return result.rowsAffected;
+}
diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts
index 5e8309f..da13e77 100644
--- a/src/shared/types/index.ts
+++ b/src/shared/types/index.ts
@@ -22,6 +22,10 @@ export interface ImportedFile {
notes?: string;
}
+export interface ImportedFileWithSource extends ImportedFile {
+ source_name: string;
+}
+
export interface Category {
id: number;
name: string;