auto-detect date format (DD/MM vs MM/DD) in CSV imports

Scans all date values in imported CSVs to determine whether the file
uses DD/MM/YYYY or MM/DD/YYYY format. When the format is ambiguous
(all day and month values are <= 12), shows an inline dropdown for the
user to choose. Bump version to 1.0.2.
This commit is contained in:
2026-02-21 16:56:27 +02:00
parent b1a0c03988
commit 31e26a9aa4
6 changed files with 90 additions and 20 deletions
+56 -5
View File
@@ -1445,6 +1445,32 @@
</table>
</div>
<!-- Date format detection -->
<div v-if="importFormat !== 'json' && importFormat !== 'generic'" class="flex items-center gap-3 mb-3">
<template v-if="importDateDetected !== 'ambiguous'">
<span class="text-[0.6875rem] text-text-tertiary">
Date format detected: {{ importDateFormat === 'DMY' ? 'DD/MM/YYYY' : 'MM/DD/YYYY' }}
</span>
<button
@click="importDateDetected = 'ambiguous'"
class="text-[0.6875rem] text-accent hover:text-accent-hover transition-colors"
>
Change
</button>
</template>
<template v-else>
<label class="text-[0.6875rem] text-text-tertiary">Date format:</label>
<div class="w-36">
<AppSelect
v-model="importDateFormat"
:options="dateFormatOptions"
label-key="label"
value-key="value"
/>
</div>
</template>
</div>
<button
@click="executeImport"
:disabled="isImporting"
@@ -1541,7 +1567,7 @@ import AppSelect from '../components/AppSelect.vue'
import AppShortcutRecorder from '../components/AppShortcutRecorder.vue'
import AppTimePicker from '../components/AppTimePicker.vue'
import { LOCALES, getCurrencies, getCurrencySymbol } from '../utils/locale'
import { parseCSV, mapTogglCSV, mapClockifyCSV, mapHarvestCSV, mapGenericCSV, type ImportEntry } from '../utils/import'
import { parseCSV, mapTogglCSV, mapClockifyCSV, mapHarvestCSV, mapGenericCSV, detectDateFormat, findCol, type ImportEntry, type DateFormat, type DateDetectResult } from '../utils/import'
import { TIMER_FONTS, loadGoogleFont, loadAndApplyTimerFont } from '../utils/fonts'
import { UI_FONTS, loadUIFont } from '../utils/uiFonts'
import { useFocusTrap } from '../utils/focusTrap'
@@ -1978,6 +2004,13 @@ const importFileName = ref('')
const importPreview = ref<string[][]>([])
const importStatus = ref('')
const isImporting = ref(false)
const importDateDetected = ref<DateDetectResult>('ambiguous')
const importDateFormat = ref<DateFormat>('MDY')
const dateFormatOptions = [
{ label: 'MM/DD/YYYY', value: 'MDY' },
{ label: 'DD/MM/YYYY', value: 'DMY' },
]
const importFormats = [
{ label: 'Toggl CSV', value: 'toggl' },
@@ -2358,8 +2391,23 @@ async function handleImportFile() {
if (importFormat.value === 'json') {
importPreview.value = []
importDateDetected.value = 'ambiguous'
importDateFormat.value = 'MDY'
} else {
importPreview.value = parseCSV(content).slice(0, 6)
const allRows = parseCSV(content)
importPreview.value = allRows.slice(0, 6)
// Detect date format from CSV data
const header = allRows[0]?.map(h => h.toLowerCase().trim()) || []
let dateColIdx = -1
if (importFormat.value === 'harvest') {
dateColIdx = findCol(header, 'date')
} else {
dateColIdx = findCol(header, 'start date')
}
const detected = detectDateFormat(allRows, dateColIdx)
importDateDetected.value = detected
importDateFormat.value = detected === 'ambiguous' ? 'MDY' : detected
}
} catch (e) {
console.error('Failed to read file:', e)
@@ -2380,12 +2428,13 @@ async function executeImport() {
const rows = parseCSV(importFileContent.value)
let entries: ImportEntry[]
const df = importDateFormat.value
if (importFormat.value === 'toggl') {
entries = mapTogglCSV(rows)
entries = mapTogglCSV(rows, df)
} else if (importFormat.value === 'clockify') {
entries = mapClockifyCSV(rows)
entries = mapClockifyCSV(rows, df)
} else if (importFormat.value === 'harvest') {
entries = mapHarvestCSV(rows)
entries = mapHarvestCSV(rows, df)
} else {
entries = mapGenericCSV(rows, { project: 0, description: 1, start_time: 2, duration: 3, client: -1, task: -1 })
}
@@ -2397,6 +2446,8 @@ async function executeImport() {
importFileContent.value = ''
importFileName.value = ''
importPreview.value = []
importDateDetected.value = 'ambiguous'
importDateFormat.value = 'MDY'
} catch (e) {
importStatus.value = `Error: ${e}`
} finally {