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 "";