Make duplicate file detection cross-source by removing sourceId from existsByHash query. Add import history table below source list with per-import and delete-all functionality. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
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,
|
|
};
|
|
}
|