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>
This commit is contained in:
parent
172be36f1d
commit
e23e559ee3
2 changed files with 56 additions and 13 deletions
|
|
@ -33,6 +33,7 @@ type ImportAction =
|
||||||
| { type: "NEEDS_PASSWORD"; filePath: string }
|
| { type: "NEEDS_PASSWORD"; filePath: string }
|
||||||
| {
|
| {
|
||||||
type: "CONFIRMING";
|
type: "CONFIRMING";
|
||||||
|
filePath: string;
|
||||||
summary: ImportSummary;
|
summary: ImportSummary;
|
||||||
data: ExportEnvelope["data"];
|
data: ExportEnvelope["data"];
|
||||||
importType: ExportEnvelope["export_type"];
|
importType: ExportEnvelope["export_type"];
|
||||||
|
|
@ -61,6 +62,7 @@ function reducer(state: ImportState, action: ImportAction): ImportState {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
status: "confirming",
|
status: "confirming",
|
||||||
|
filePath: action.filePath,
|
||||||
summary: action.summary,
|
summary: action.summary,
|
||||||
parsedData: action.data,
|
parsedData: action.data,
|
||||||
importType: action.importType,
|
importType: action.importType,
|
||||||
|
|
@ -132,7 +134,7 @@ export function useDataImport() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const { summary, data, importType } = parseContent(content, filePath);
|
const { summary, data, importType } = parseContent(content, filePath);
|
||||||
dispatch({ type: "CONFIRMING", summary, data, importType });
|
dispatch({ type: "CONFIRMING", filePath, summary, data, importType });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "IMPORT_ERROR",
|
type: "IMPORT_ERROR",
|
||||||
|
|
@ -152,7 +154,7 @@ export function useDataImport() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const { summary, data, importType } = parseContent(content, state.filePath);
|
const { summary, data, importType } = parseContent(content, state.filePath);
|
||||||
dispatch({ type: "CONFIRMING", summary, data, importType });
|
dispatch({ type: "CONFIRMING", filePath: state.filePath, summary, data, importType });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "IMPORT_ERROR",
|
type: "IMPORT_ERROR",
|
||||||
|
|
@ -166,16 +168,17 @@ export function useDataImport() {
|
||||||
const executeImport = useCallback(async () => {
|
const executeImport = useCallback(async () => {
|
||||||
if (!state.parsedData || !state.importType) return;
|
if (!state.parsedData || !state.importType) return;
|
||||||
dispatch({ type: "IMPORT_START" });
|
dispatch({ type: "IMPORT_START" });
|
||||||
|
const filename = state.filePath?.split(/[/\\]/).pop() ?? "unknown";
|
||||||
try {
|
try {
|
||||||
switch (state.importType) {
|
switch (state.importType) {
|
||||||
case "categories_only":
|
case "categories_only":
|
||||||
await importCategoriesOnly(state.parsedData);
|
await importCategoriesOnly(state.parsedData);
|
||||||
break;
|
break;
|
||||||
case "transactions_with_categories":
|
case "transactions_with_categories":
|
||||||
await importTransactionsWithCategories(state.parsedData);
|
await importTransactionsWithCategories(state.parsedData, filename);
|
||||||
break;
|
break;
|
||||||
case "transactions_only":
|
case "transactions_only":
|
||||||
await importTransactionsOnly(state.parsedData);
|
await importTransactionsOnly(state.parsedData, filename);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
dispatch({ type: "IMPORT_SUCCESS" });
|
dispatch({ type: "IMPORT_SUCCESS" });
|
||||||
|
|
@ -185,7 +188,7 @@ export function useDataImport() {
|
||||||
error: e instanceof Error ? e.message : String(e),
|
error: e instanceof Error ? e.message : String(e),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [state.parsedData, state.importType]);
|
}, [state.parsedData, state.importType, state.filePath]);
|
||||||
|
|
||||||
const reset = useCallback(() => dispatch({ type: "RESET" }), []);
|
const reset = useCallback(() => dispatch({ type: "RESET" }), []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -254,13 +254,15 @@ export async function importCategoriesOnly(data: ExportEnvelope["data"]): Promis
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importTransactionsWithCategories(
|
export async function importTransactionsWithCategories(
|
||||||
data: ExportEnvelope["data"]
|
data: ExportEnvelope["data"],
|
||||||
|
filename: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
|
||||||
// Wipe everything
|
// Wipe everything
|
||||||
await db.execute("DELETE FROM transactions");
|
await db.execute("DELETE FROM transactions");
|
||||||
await db.execute("DELETE FROM imported_files");
|
await db.execute("DELETE FROM imported_files");
|
||||||
|
await db.execute("DELETE FROM import_sources");
|
||||||
await db.execute("DELETE FROM keywords");
|
await db.execute("DELETE FROM keywords");
|
||||||
await db.execute("DELETE FROM suppliers");
|
await db.execute("DELETE FROM suppliers");
|
||||||
await db.execute("DELETE FROM categories");
|
await db.execute("DELETE FROM categories");
|
||||||
|
|
@ -308,12 +310,28 @@ export async function importTransactionsWithCategories(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-insert transactions
|
// Create tracking records for import history
|
||||||
|
const sourceResult = await db.execute(
|
||||||
|
`INSERT INTO import_sources (name, description, date_format, delimiter, encoding, column_mapping, skip_lines)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||||
|
["Data Import", "Imported from settings", "%Y-%m-%d", ",", "utf-8", "{}", 0]
|
||||||
|
);
|
||||||
|
const sourceId = sourceResult.lastInsertId;
|
||||||
|
|
||||||
|
const txCount = data.transactions?.length ?? 0;
|
||||||
|
const fileResult = await db.execute(
|
||||||
|
`INSERT INTO imported_files (source_id, filename, file_hash, row_count, status)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)`,
|
||||||
|
[sourceId, filename, `data-import-${Date.now()}`, txCount, "completed"]
|
||||||
|
);
|
||||||
|
const fileId = fileResult.lastInsertId;
|
||||||
|
|
||||||
|
// Re-insert transactions linked to the import
|
||||||
if (data.transactions) {
|
if (data.transactions) {
|
||||||
for (const tx of data.transactions) {
|
for (const tx of data.transactions) {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
`INSERT INTO transactions (date, description, amount, category_id, original_description, notes, is_manually_categorized, is_split, parent_transaction_id)
|
`INSERT INTO transactions (date, description, amount, category_id, original_description, notes, is_manually_categorized, is_split, parent_transaction_id, source_id, file_id)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
|
||||||
[
|
[
|
||||||
tx.date,
|
tx.date,
|
||||||
tx.description,
|
tx.description,
|
||||||
|
|
@ -324,6 +342,8 @@ export async function importTransactionsWithCategories(
|
||||||
tx.is_manually_categorized,
|
tx.is_manually_categorized,
|
||||||
tx.is_split,
|
tx.is_split,
|
||||||
tx.parent_transaction_id,
|
tx.parent_transaction_id,
|
||||||
|
sourceId,
|
||||||
|
fileId,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -331,20 +351,38 @@ export async function importTransactionsWithCategories(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importTransactionsOnly(
|
export async function importTransactionsOnly(
|
||||||
data: ExportEnvelope["data"]
|
data: ExportEnvelope["data"],
|
||||||
|
filename: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
|
||||||
// Wipe transactions and import history
|
// Wipe transactions and import history
|
||||||
await db.execute("DELETE FROM transactions");
|
await db.execute("DELETE FROM transactions");
|
||||||
await db.execute("DELETE FROM imported_files");
|
await db.execute("DELETE FROM imported_files");
|
||||||
|
await db.execute("DELETE FROM import_sources");
|
||||||
|
|
||||||
// Re-insert transactions
|
// Create tracking records for import history
|
||||||
|
const sourceResult = await db.execute(
|
||||||
|
`INSERT INTO import_sources (name, description, date_format, delimiter, encoding, column_mapping, skip_lines)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||||
|
["Data Import", "Imported from settings", "%Y-%m-%d", ",", "utf-8", "{}", 0]
|
||||||
|
);
|
||||||
|
const sourceId = sourceResult.lastInsertId;
|
||||||
|
|
||||||
|
const txCount = data.transactions?.length ?? 0;
|
||||||
|
const fileResult = await db.execute(
|
||||||
|
`INSERT INTO imported_files (source_id, filename, file_hash, row_count, status)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)`,
|
||||||
|
[sourceId, filename, `data-import-${Date.now()}`, txCount, "completed"]
|
||||||
|
);
|
||||||
|
const fileId = fileResult.lastInsertId;
|
||||||
|
|
||||||
|
// Re-insert transactions linked to the import
|
||||||
if (data.transactions) {
|
if (data.transactions) {
|
||||||
for (const tx of data.transactions) {
|
for (const tx of data.transactions) {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
`INSERT INTO transactions (date, description, amount, category_id, original_description, notes, is_manually_categorized, is_split, parent_transaction_id)
|
`INSERT INTO transactions (date, description, amount, category_id, original_description, notes, is_manually_categorized, is_split, parent_transaction_id, source_id, file_id)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
|
||||||
[
|
[
|
||||||
tx.date,
|
tx.date,
|
||||||
tx.description,
|
tx.description,
|
||||||
|
|
@ -355,6 +393,8 @@ export async function importTransactionsOnly(
|
||||||
tx.is_manually_categorized,
|
tx.is_manually_categorized,
|
||||||
tx.is_split,
|
tx.is_split,
|
||||||
tx.parent_transaction_id,
|
tx.parent_transaction_id,
|
||||||
|
sourceId,
|
||||||
|
fileId,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue