From 88219e657f4e5643de9e51de5d07e17739a49774 Mon Sep 17 00:00:00 2001 From: Le-King-Fu Date: Sun, 8 Feb 2026 04:08:33 +0000 Subject: [PATCH] fix: reload column headers on config change and add YYYYMMDD date format Headers now refresh live when delimiter, encoding, skipLines, or hasHeader changes. Added YYYYMMDD compact date format to parser and dropdown. Co-Authored-By: Claude Opus 4.6 --- src/components/import/SourceConfigPanel.tsx | 1 + src/hooks/useImportWizard.ts | 99 ++++++++++++++------- src/utils/dateParser.ts | 19 +++- 3 files changed, 87 insertions(+), 32 deletions(-) diff --git a/src/components/import/SourceConfigPanel.tsx b/src/components/import/SourceConfigPanel.tsx index 1c88131..ad61d11 100644 --- a/src/components/import/SourceConfigPanel.tsx +++ b/src/components/import/SourceConfigPanel.tsx @@ -104,6 +104,7 @@ export default function SourceConfigPanel({ + diff --git a/src/hooks/useImportWizard.ts b/src/hooks/useImportWizard.ts index 702711b..76a29f6 100644 --- a/src/hooks/useImportWizard.ts +++ b/src/hooks/useImportWizard.ts @@ -244,6 +244,11 @@ export function useImportWizard() { const existing = await getSourceByName(source.folder_name); dispatch({ type: "SET_EXISTING_SOURCE", payload: existing }); + let activeDelimiter = defaultConfig.delimiter; + let activeEncoding = "utf-8"; + let activeSkipLines = 0; + const activeHasHeader = true; + if (existing) { // Restore config from DB const mapping = JSON.parse(existing.column_mapping) as ColumnMapping; @@ -260,12 +265,14 @@ export function useImportWizard() { hasHeader: true, }; dispatch({ type: "SET_SOURCE_CONFIG", payload: config }); + activeDelimiter = existing.delimiter; + activeEncoding = existing.encoding; + activeSkipLines = existing.skip_lines; } else { // Auto-detect encoding for first file - let encoding = "utf-8"; if (source.files.length > 0) { try { - encoding = await invoke("detect_encoding", { + activeEncoding = await invoke("detect_encoding", { filePath: source.files[0].file_path, }); } catch { @@ -278,14 +285,20 @@ export function useImportWizard() { payload: { ...defaultConfig, name: source.folder_name, - encoding, + encoding: activeEncoding, }, }); } // Load preview headers from first file if (source.files.length > 0) { - await loadHeaders(source.files[0].file_path, existing); + await loadHeadersWithConfig( + source.files[0].file_path, + activeDelimiter, + activeEncoding, + activeSkipLines, + activeHasHeader + ); } dispatch({ type: "SET_STEP", payload: "source-config" }); @@ -293,36 +306,60 @@ export function useImportWizard() { [] // eslint-disable-line react-hooks/exhaustive-deps ); - const loadHeaders = async ( - filePath: string, - existing: ImportSource | null - ) => { - try { - const encoding = existing?.encoding || "utf-8"; - const preview = await invoke("get_file_preview", { - filePath, - encoding, - maxLines: 5, - }); - const delimiter = existing?.delimiter || ";"; - const parsed = Papa.parse(preview, { delimiter }); - if (parsed.data.length > 0) { - dispatch({ - type: "SET_PARSED_PREVIEW", - payload: { - rows: [], - headers: (parsed.data[0] as string[]).map((h) => h.trim()), - }, + const loadHeadersWithConfig = useCallback( + async (filePath: string, delimiter: string, encoding: string, skipLines: number, hasHeader: boolean) => { + try { + const preview = await invoke("get_file_preview", { + filePath, + encoding, + maxLines: skipLines + 5, }); + const parsed = Papa.parse(preview, { delimiter, skipEmptyLines: true }); + const data = parsed.data as string[][]; + const headerRow = hasHeader && data.length > skipLines ? skipLines : -1; + if (headerRow >= 0 && data[headerRow]) { + dispatch({ + type: "SET_PARSED_PREVIEW", + payload: { + rows: [], + headers: data[headerRow].map((h) => h.trim()), + }, + }); + } else if (data.length > 0) { + // No header row — generate column indices as headers + const firstDataRow = data[skipLines] || data[0]; + dispatch({ + type: "SET_PARSED_PREVIEW", + payload: { + rows: [], + headers: firstDataRow.map((_, i) => `Col ${i}`), + }, + }); + } + } catch { + // ignore preview errors } - } catch { - // ignore preview errors - } - }; + }, + [] + ); - const updateConfig = useCallback((config: SourceConfig) => { - dispatch({ type: "SET_SOURCE_CONFIG", payload: config }); - }, []); + const updateConfig = useCallback( + (config: SourceConfig) => { + dispatch({ type: "SET_SOURCE_CONFIG", payload: config }); + + // Reload headers when delimiter, encoding, skipLines, or hasHeader changes + if (state.selectedFiles.length > 0) { + loadHeadersWithConfig( + state.selectedFiles[0].file_path, + config.delimiter, + config.encoding, + config.skipLines, + config.hasHeader + ); + } + }, + [state.selectedFiles, loadHeadersWithConfig] + ); const toggleFile = useCallback( (file: ScannedFile) => { diff --git a/src/utils/dateParser.ts b/src/utils/dateParser.ts index 079d828..272fd0a 100644 --- a/src/utils/dateParser.ts +++ b/src/utils/dateParser.ts @@ -1,6 +1,6 @@ /** * Parse a date string with a given format and return ISO YYYY-MM-DD. - * Supported formats: DD/MM/YYYY, MM/DD/YYYY, YYYY-MM-DD, DD-MM-YYYY, DD.MM.YYYY + * Supported formats: DD/MM/YYYY, MM/DD/YYYY, YYYY-MM-DD, DD-MM-YYYY, DD.MM.YYYY, YYYYMMDD */ export function parseDate(raw: string, format: string): string { if (!raw || typeof raw !== "string") return ""; @@ -8,6 +8,23 @@ export function parseDate(raw: string, format: string): string { const cleaned = raw.trim(); let day: string, month: string, year: string; + // Handle compact format YYYYMMDD (no separator) + if (format === "YYYYMMDD") { + const digits = cleaned.replace(/\D/g, ""); + if (digits.length !== 8) return ""; + year = digits.substring(0, 4); + month = digits.substring(4, 6); + day = digits.substring(6, 8); + + const y = parseInt(year, 10); + const m = parseInt(month, 10); + const d = parseInt(day, 10); + if (isNaN(y) || isNaN(m) || isNaN(d)) return ""; + if (m < 1 || m > 12 || d < 1 || d > 31) return ""; + + return `${year}-${month}-${day}`; + } + // Extract parts based on separator const parts = cleaned.split(/[/\-\.]/); if (parts.length !== 3) return "";