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:
+56
-5
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user