diff --git a/src/components/import/SourceConfigPanel.tsx b/src/components/import/SourceConfigPanel.tsx
index 56eac8c..59ebb82 100644
--- a/src/components/import/SourceConfigPanel.tsx
+++ b/src/components/import/SourceConfigPanel.tsx
@@ -1,6 +1,6 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
-import { Wand2, Check, Save, X } from "lucide-react";
+import { Wand2, Check, Save, RefreshCw, X } from "lucide-react";
import type {
ScannedSource,
ScannedFile,
@@ -24,7 +24,9 @@ interface SourceConfigPanelProps {
onAutoDetect: () => void;
onSaveAsTemplate: (name: string) => void;
onApplyTemplate: (id: number) => void;
+ onUpdateTemplate: () => void;
onDeleteTemplate: (id: number) => void;
+ selectedTemplateId: number | null;
isLoading?: boolean;
}
@@ -41,7 +43,9 @@ export default function SourceConfigPanel({
onAutoDetect,
onSaveAsTemplate,
onApplyTemplate,
+ onUpdateTemplate,
onDeleteTemplate,
+ selectedTemplateId,
isLoading,
}: SourceConfigPanelProps) {
const { t } = useTranslation();
@@ -79,7 +83,7 @@ export default function SourceConfigPanel({
{t("import.config.loadTemplate")}
+ {selectedTemplateId && (
+
+ )}
{configTemplates.length > 0 && (
{configTemplates.map((tpl) => (
diff --git a/src/hooks/useImportWizard.ts b/src/hooks/useImportWizard.ts
index 1d0071b..4602428 100644
--- a/src/hooks/useImportWizard.ts
+++ b/src/hooks/useImportWizard.ts
@@ -37,6 +37,7 @@ import { categorizeBatch } from "../services/categorizationService";
import {
getAllTemplates,
createTemplate,
+ updateTemplate,
deleteTemplate as deleteTemplateService,
} from "../services/importConfigTemplateService";
import { parseDate } from "../utils/dateParser";
@@ -65,6 +66,7 @@ interface WizardState {
configuredSourceNames: Set
;
importedFilesBySource: Map>;
configTemplates: ImportConfigTemplate[];
+ selectedTemplateId: number | null;
}
type WizardAction =
@@ -85,6 +87,7 @@ type WizardAction =
| { type: "SET_IMPORT_PROGRESS"; payload: { current: number; total: number; file: string } }
| { type: "SET_CONFIGURED_SOURCES"; payload: { names: Set; files: Map> } }
| { type: "SET_CONFIG_TEMPLATES"; payload: ImportConfigTemplate[] }
+ | { type: "SET_SELECTED_TEMPLATE_ID"; payload: number | null }
| { type: "RESET" };
const defaultConfig: SourceConfig = {
@@ -118,6 +121,7 @@ const initialState: WizardState = {
configuredSourceNames: new Set(),
importedFilesBySource: new Map(),
configTemplates: [],
+ selectedTemplateId: null,
};
function reducer(state: WizardState, action: WizardAction): WizardState {
@@ -182,6 +186,8 @@ function reducer(state: WizardState, action: WizardAction): WizardState {
};
case "SET_CONFIG_TEMPLATES":
return { ...state, configTemplates: action.payload };
+ case "SET_SELECTED_TEMPLATE_ID":
+ return { ...state, selectedTemplateId: action.payload };
case "RESET":
return {
...initialState,
@@ -291,6 +297,7 @@ export function useImportWizard() {
dispatch({ type: "SET_SELECTED_SOURCE", payload: sortedSource });
dispatch({ type: "SET_SELECTED_FILES", payload: newFiles });
+ dispatch({ type: "SET_SELECTED_TEMPLATE_ID", payload: null });
// Check if this source already has config in DB
const existing = await getSourceByName(source.folder_name);
@@ -945,6 +952,7 @@ export function useImportWizard() {
hasHeader: !!template.has_header,
};
dispatch({ type: "SET_SOURCE_CONFIG", payload: newConfig });
+ dispatch({ type: "SET_SELECTED_TEMPLATE_ID", payload: templateId });
// Reload headers with new config
if (state.selectedFiles.length > 0) {
@@ -958,11 +966,34 @@ export function useImportWizard() {
}
}, [state.configTemplates, state.sourceConfig.name, state.selectedFiles, loadHeadersWithConfig]);
- const deleteConfigTemplate = useCallback(async (id: number) => {
- await deleteTemplateService(id);
+ const updateConfigTemplate = useCallback(async () => {
+ if (!state.selectedTemplateId) return;
+ const template = state.configTemplates.find((t) => t.id === state.selectedTemplateId);
+ if (!template) return;
+ const config = state.sourceConfig;
+ await updateTemplate(state.selectedTemplateId, {
+ name: template.name,
+ delimiter: config.delimiter,
+ encoding: config.encoding,
+ date_format: config.dateFormat,
+ skip_lines: config.skipLines,
+ has_header: config.hasHeader ? 1 : 0,
+ column_mapping: JSON.stringify(config.columnMapping),
+ amount_mode: config.amountMode,
+ sign_convention: config.signConvention,
+ });
const templates = await getAllTemplates();
dispatch({ type: "SET_CONFIG_TEMPLATES", payload: templates });
- }, []);
+ }, [state.selectedTemplateId, state.configTemplates, state.sourceConfig]);
+
+ const deleteConfigTemplate = useCallback(async (id: number) => {
+ await deleteTemplateService(id);
+ if (state.selectedTemplateId === id) {
+ dispatch({ type: "SET_SELECTED_TEMPLATE_ID", payload: null });
+ }
+ const templates = await getAllTemplates();
+ dispatch({ type: "SET_CONFIG_TEMPLATES", payload: templates });
+ }, [state.selectedTemplateId]);
return {
state,
@@ -981,6 +1012,7 @@ export function useImportWizard() {
autoDetectConfig,
saveConfigAsTemplate,
applyConfigTemplate,
+ updateConfigTemplate,
deleteConfigTemplate,
toggleDuplicateRow: (index: number) =>
dispatch({ type: "TOGGLE_DUPLICATE_ROW", payload: index }),
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index a38044a..9a37b7f 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -90,7 +90,8 @@
"templateName": "Template name",
"templateSaved": "Template saved",
"deleteTemplate": "Delete template",
- "noTemplates": "No templates saved"
+ "noTemplates": "No templates saved",
+ "updateTemplate": "Update template"
},
"preview": {
"title": "Data Preview",
diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json
index 28e58bf..addba46 100644
--- a/src/i18n/locales/fr.json
+++ b/src/i18n/locales/fr.json
@@ -90,7 +90,8 @@
"templateName": "Nom du modèle",
"templateSaved": "Modèle sauvegardé",
"deleteTemplate": "Supprimer le modèle",
- "noTemplates": "Aucun modèle sauvegardé"
+ "noTemplates": "Aucun modèle sauvegardé",
+ "updateTemplate": "Mettre à jour le modèle"
},
"preview": {
"title": "Aperçu des données",
diff --git a/src/pages/ImportPage.tsx b/src/pages/ImportPage.tsx
index 6a6ea49..4ce7744 100644
--- a/src/pages/ImportPage.tsx
+++ b/src/pages/ImportPage.tsx
@@ -32,6 +32,7 @@ export default function ImportPage() {
autoDetectConfig,
saveConfigAsTemplate,
applyConfigTemplate,
+ updateConfigTemplate,
deleteConfigTemplate,
toggleDuplicateRow,
setSkipAllDuplicates,
@@ -99,7 +100,9 @@ export default function ImportPage() {
onAutoDetect={autoDetectConfig}
onSaveAsTemplate={saveConfigAsTemplate}
onApplyTemplate={applyConfigTemplate}
+ onUpdateTemplate={updateConfigTemplate}
onDeleteTemplate={deleteConfigTemplate}
+ selectedTemplateId={state.selectedTemplateId}
isLoading={state.isLoading}
/>
diff --git a/src/services/importConfigTemplateService.ts b/src/services/importConfigTemplateService.ts
index 586bc1d..29d86b7 100644
--- a/src/services/importConfigTemplateService.ts
+++ b/src/services/importConfigTemplateService.ts
@@ -30,6 +30,31 @@ export async function createTemplate(
return result.lastInsertId as number;
}
+export async function updateTemplate(
+ id: number,
+ template: Omit
+): Promise {
+ const db = await getDb();
+ await db.execute(
+ `UPDATE import_config_templates
+ SET name=$1, delimiter=$2, encoding=$3, date_format=$4, skip_lines=$5,
+ has_header=$6, column_mapping=$7, amount_mode=$8, sign_convention=$9
+ WHERE id=$10`,
+ [
+ template.name,
+ template.delimiter,
+ template.encoding,
+ template.date_format,
+ template.skip_lines,
+ template.has_header,
+ template.column_mapping,
+ template.amount_mode,
+ template.sign_convention,
+ id,
+ ]
+ );
+}
+
export async function deleteTemplate(id: number): Promise {
const db = await getDb();
await db.execute("DELETE FROM import_config_templates WHERE id = $1", [id]);