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