Compare commits
125 Commits
ae02cba958
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb0c65c29a | ||
|
|
a0bb7d3ea8 | ||
|
|
514090eed4 | ||
|
|
2608f447de | ||
|
|
5300ceeb12 | ||
|
|
875d3ca23b | ||
|
|
fa7b70aa61 | ||
|
|
773ba1d338 | ||
|
|
a41ce44f13 | ||
|
|
ace66a6093 | ||
|
|
3fd389da11 | ||
|
|
159bb927af | ||
|
|
29f29e3368 | ||
|
|
991866f017 | ||
|
|
d4fa17d315 | ||
|
|
cb1c6c9b5d | ||
|
|
a3ea37baa1 | ||
|
|
54f75c15ed | ||
|
|
14c45c67e5 | ||
|
|
15c8db6572 | ||
|
|
dcfcdaf1b0 | ||
|
|
6e05ddcf89 | ||
|
|
529461f12c | ||
|
|
fb38d98612 | ||
|
|
115bdd33db | ||
|
|
4589fea5ce | ||
|
|
522efbf230 | ||
|
|
43bd3b9b41 | ||
|
|
35e97cbe7b | ||
|
|
25c6c55eb2 | ||
|
|
1f21cd61c3 | ||
|
|
3968a818c5 | ||
|
|
85b39e41f6 | ||
|
|
24b3caf0da | ||
|
|
6ed462853c | ||
|
|
dea742707f | ||
|
|
a3c0d43f67 | ||
|
|
78026c8bf0 | ||
|
|
b8239c6e1b | ||
|
|
4462d832d2 | ||
|
|
edccc12c34 | ||
|
|
5680194ef4 | ||
|
|
08d61b40a0 | ||
|
|
6e00b8b8a3 | ||
|
|
f46424141d | ||
|
|
fd4cc29d53 | ||
|
|
f40cc97668 | ||
|
|
a313477cd7 | ||
|
|
c5380568ca | ||
|
|
886a2b100e | ||
|
|
cca06e851b | ||
|
|
9e9c0c78f1 | ||
|
|
5e47700c93 | ||
|
|
162acccc2c | ||
|
|
06dc063125 | ||
|
|
3cadb42f8b | ||
|
|
673da2aab8 | ||
|
|
3928904c40 | ||
|
|
f1a5428dd5 | ||
|
|
50734dee03 | ||
|
|
a527a5ceca | ||
|
|
32ee6284da | ||
|
|
04d4220604 | ||
|
|
bd0dbaf91d | ||
|
|
5630751adc | ||
|
|
6b7dcc7317 | ||
|
|
c7b9822e48 | ||
|
|
87b1853f39 | ||
|
|
787f8bbacf | ||
|
|
7e7e04e4d4 | ||
|
|
f6955d1bd7 | ||
|
|
af95a53c4e | ||
|
|
e143b069db | ||
|
|
28d199bddc | ||
|
|
b650e981fc | ||
|
|
32d22bf877 | ||
|
|
ba185a1ac9 | ||
|
|
318570295f | ||
|
|
2ddd2ce5d8 | ||
|
|
f0885921ae | ||
|
|
1ee4562647 | ||
|
|
85c20247f5 | ||
|
|
26f1b19dde | ||
|
|
afa8bce2c9 | ||
|
|
5ad901ca4f | ||
|
|
137be610f8 | ||
|
|
519bdabe61 | ||
|
|
fe0b20f247 | ||
|
|
8112fe8fd6 | ||
|
|
a13dff96c8 | ||
|
|
7d43f02e59 | ||
|
|
06d646c8de | ||
|
|
7fed47e54f | ||
|
|
21f762edd9 | ||
|
|
93bc6713b4 | ||
|
|
e3f7e2f470 | ||
|
|
c949a08981 | ||
|
|
5ab96769ac | ||
|
|
29a0510192 | ||
|
|
0901673c28 | ||
|
|
3dbe4b4ac8 | ||
|
|
40f87c9e04 | ||
|
|
b9aace912b | ||
|
|
8cdd30b9e4 | ||
|
|
c8a6fd294e | ||
|
|
ea9f374568 | ||
|
|
a300d85f6f | ||
|
|
3c8868c899 | ||
|
|
a8bec56d96 | ||
|
|
71d3d9ba8b | ||
|
|
f9b87cc41c | ||
|
|
59bfc9fa5a | ||
|
|
eab5e94452 | ||
|
|
fc43d2bc29 | ||
|
|
8264082719 | ||
|
|
28088b9566 | ||
|
|
644f9ee3ce | ||
|
|
f70ba8bc31 | ||
|
|
6082e75734 | ||
|
|
919fb5f499 | ||
|
|
d1fd1c9ea8 | ||
|
|
30eeb15ece | ||
|
|
9f79309ada | ||
|
|
520846511b | ||
|
|
552e8c0607 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zeroclock",
|
"name": "zeroclock",
|
||||||
"version": "1.0.2",
|
"version": "1.0.0",
|
||||||
"description": "Time tracking desktop application",
|
"description": "Time tracking desktop application",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -5743,7 +5743,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroclock"
|
name = "zeroclock"
|
||||||
version = "1.0.1"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "zeroclock"
|
name = "zeroclock"
|
||||||
version = "1.0.2"
|
version = "1.0.0"
|
||||||
description = "A local time tracking app with invoicing"
|
description = "A local time tracking app with invoicing"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "ZeroClock",
|
"productName": "ZeroClock",
|
||||||
"version": "1.0.2",
|
"version": "1.0.0",
|
||||||
"identifier": "com.localtimetracker.app",
|
"identifier": "com.localtimetracker.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -324,8 +324,6 @@ onMounted(async () => {
|
|||||||
if (settingsStore.settings.close_to_tray === 'true') {
|
if (settingsStore.settings.close_to_tray === 'true') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
await win.hide()
|
await win.hide()
|
||||||
} else {
|
|
||||||
await win.destroy()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -9,23 +9,6 @@ export interface ImportEntry {
|
|||||||
tags?: string[]
|
tags?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DateFormat = 'DMY' | 'MDY'
|
|
||||||
export type DateDetectResult = DateFormat | 'ambiguous'
|
|
||||||
|
|
||||||
export function detectDateFormat(rows: string[][], dateColIndex: number): DateDetectResult {
|
|
||||||
if (dateColIndex < 0) return 'ambiguous'
|
|
||||||
for (let i = 1; i < rows.length; i++) {
|
|
||||||
const val = (rows[i]?.[dateColIndex] || '').trim()
|
|
||||||
const match = val.match(/^(\d{1,2})[\/.](\d{1,2})[\/.](\d{2,4})$/)
|
|
||||||
if (!match) continue
|
|
||||||
const a = parseInt(match[1])
|
|
||||||
const b = parseInt(match[2])
|
|
||||||
if (a > 12) return 'DMY'
|
|
||||||
if (b > 12) return 'MDY'
|
|
||||||
}
|
|
||||||
return 'ambiguous'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseCSV(text: string): string[][] {
|
export function parseCSV(text: string): string[][] {
|
||||||
const lines = text.split('\n').filter(l => l.trim())
|
const lines = text.split('\n').filter(l => l.trim())
|
||||||
return lines.map(line => {
|
return lines.map(line => {
|
||||||
@@ -48,7 +31,6 @@ export function parseCSV(text: string): string[][] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseDurationString(dur: string): number {
|
export function parseDurationString(dur: string): number {
|
||||||
if (!dur) return 0
|
|
||||||
// Handle HH:MM:SS, HH:MM, or decimal hours
|
// Handle HH:MM:SS, HH:MM, or decimal hours
|
||||||
if (dur.includes(':')) {
|
if (dur.includes(':')) {
|
||||||
const parts = dur.split(':').map(Number)
|
const parts = dur.split(':').map(Number)
|
||||||
@@ -60,160 +42,26 @@ export function parseDurationString(dur: string): number {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse locale-variable date strings into YYYY-MM-DD
|
export function mapTogglCSV(rows: string[][]): ImportEntry[] {
|
||||||
function parseFlexDate(dateStr: string, formatHint?: DateFormat): string {
|
// Toggl CSV format: Description, Project, Client, Task, Tag, Start date, Start time, End date, End time, Duration
|
||||||
if (!dateStr) return new Date().toISOString().split('T')[0]
|
const header = rows[0].map(h => h.toLowerCase())
|
||||||
const s = dateStr.trim()
|
const descIdx = header.findIndex(h => h.includes('description'))
|
||||||
|
const projIdx = header.findIndex(h => h.includes('project'))
|
||||||
// YYYY-MM-DD (ISO)
|
const clientIdx = header.findIndex(h => h.includes('client'))
|
||||||
if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(s)) {
|
const taskIdx = header.findIndex(h => h.includes('task'))
|
||||||
const [y, m, d] = s.split('-')
|
const tagIdx = header.findIndex(h => h.includes('tag'))
|
||||||
return `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`
|
const startDateIdx = header.findIndex(h => h.includes('start date'))
|
||||||
}
|
const startTimeIdx = header.findIndex(h => h.includes('start time'))
|
||||||
|
const durationIdx = header.findIndex(h => h.includes('duration'))
|
||||||
// Slash or dot separated: could be MM/DD/YYYY, DD/MM/YYYY, DD.MM.YYYY
|
|
||||||
const match = s.match(/^(\d{1,2})[\/.](\d{1,2})[\/.](\d{2,4})$/)
|
|
||||||
if (match) {
|
|
||||||
let [, a, b, year] = match
|
|
||||||
if (year.length === 2) year = '20' + year
|
|
||||||
// If first part > 12, must be day-first (DD/MM/YYYY)
|
|
||||||
if (parseInt(a) > 12) return `${year}-${b.padStart(2, '0')}-${a.padStart(2, '0')}`
|
|
||||||
// If second part > 12, must be month-first (MM/DD/YYYY)
|
|
||||||
if (parseInt(b) > 12) return `${year}-${a.padStart(2, '0')}-${b.padStart(2, '0')}`
|
|
||||||
// Use format hint if provided
|
|
||||||
if (formatHint === 'DMY') return `${year}-${b.padStart(2, '0')}-${a.padStart(2, '0')}`
|
|
||||||
// Default to MDY
|
|
||||||
return `${year}-${a.padStart(2, '0')}-${b.padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last resort: try Date constructor
|
|
||||||
const d = new Date(s)
|
|
||||||
if (!isNaN(d.getTime())) return d.toISOString().split('T')[0]
|
|
||||||
return new Date().toISOString().split('T')[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse 12h or 24h time strings into HH:MM:SS
|
|
||||||
function parseFlexTime(timeStr: string): string {
|
|
||||||
if (!timeStr) return '00:00:00'
|
|
||||||
const s = timeStr.trim()
|
|
||||||
|
|
||||||
// 12-hour: "1:00 PM", "11:30:00 AM"
|
|
||||||
const ampm = s.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(AM|PM)$/i)
|
|
||||||
if (ampm) {
|
|
||||||
let hour = parseInt(ampm[1])
|
|
||||||
const min = ampm[2]
|
|
||||||
const sec = ampm[3] || '00'
|
|
||||||
const period = ampm[4].toUpperCase()
|
|
||||||
if (period === 'PM' && hour !== 12) hour += 12
|
|
||||||
if (period === 'AM' && hour === 12) hour = 0
|
|
||||||
return `${hour.toString().padStart(2, '0')}:${min}:${sec}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 24-hour: "13:00", "09:30:00"
|
|
||||||
const h24 = s.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/)
|
|
||||||
if (h24) {
|
|
||||||
return `${h24[1].padStart(2, '0')}:${h24[2]}:${h24[3] || '00'}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return '00:00:00'
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeDateTime(date: string, time?: string, dateFormat?: DateFormat): string {
|
|
||||||
const d = parseFlexDate(date, dateFormat)
|
|
||||||
const t = time ? parseFlexTime(time) : '00:00:00'
|
|
||||||
return `${d}T${t}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findCol(header: string[], ...terms: string[]): number {
|
|
||||||
for (const term of terms) {
|
|
||||||
const idx = header.findIndex(h => h === term)
|
|
||||||
if (idx >= 0) return idx
|
|
||||||
}
|
|
||||||
for (const term of terms) {
|
|
||||||
const idx = header.findIndex(h => h.includes(term))
|
|
||||||
if (idx >= 0) return idx
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
function col(row: string[], idx: number): string {
|
|
||||||
return idx >= 0 ? (row[idx] || '') : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapTogglCSV(rows: string[][], dateFormat?: DateFormat): ImportEntry[] {
|
|
||||||
const header = rows[0].map(h => h.toLowerCase().trim())
|
|
||||||
const descIdx = findCol(header, 'description')
|
|
||||||
const projIdx = findCol(header, 'project')
|
|
||||||
const clientIdx = findCol(header, 'client')
|
|
||||||
const taskIdx = findCol(header, 'task')
|
|
||||||
const tagIdx = findCol(header, 'tags', 'tag')
|
|
||||||
const startDateIdx = findCol(header, 'start date')
|
|
||||||
const startTimeIdx = findCol(header, 'start time')
|
|
||||||
const durationIdx = findCol(header, 'duration')
|
|
||||||
|
|
||||||
return rows.slice(1).map(row => ({
|
return rows.slice(1).map(row => ({
|
||||||
project_name: col(row, projIdx) || 'Imported',
|
project_name: row[projIdx] || 'Imported',
|
||||||
client_name: col(row, clientIdx) || undefined,
|
client_name: clientIdx >= 0 ? row[clientIdx] : undefined,
|
||||||
task_name: col(row, taskIdx) || undefined,
|
task_name: taskIdx >= 0 ? row[taskIdx] : undefined,
|
||||||
description: col(row, descIdx) || undefined,
|
description: descIdx >= 0 ? row[descIdx] : undefined,
|
||||||
start_time: safeDateTime(col(row, startDateIdx), col(row, startTimeIdx) || undefined, dateFormat),
|
start_time: combineDateTime(row[startDateIdx], row[startTimeIdx]),
|
||||||
duration: parseDurationString(col(row, durationIdx)),
|
duration: durationIdx >= 0 ? parseDurationString(row[durationIdx]) : 0,
|
||||||
tags: tagIdx >= 0 && row[tagIdx] ? row[tagIdx].split(',').map(t => t.trim()).filter(Boolean) : undefined,
|
tags: tagIdx >= 0 && row[tagIdx] ? row[tagIdx].split(',').map(t => t.trim()) : undefined,
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapClockifyCSV(rows: string[][], dateFormat?: DateFormat): ImportEntry[] {
|
|
||||||
const header = rows[0].map(h => h.toLowerCase().trim())
|
|
||||||
const descIdx = findCol(header, 'description')
|
|
||||||
const projIdx = findCol(header, 'project')
|
|
||||||
const clientIdx = findCol(header, 'client')
|
|
||||||
const taskIdx = findCol(header, 'task')
|
|
||||||
const tagIdx = findCol(header, 'tags', 'tag')
|
|
||||||
const startDateIdx = findCol(header, 'start date')
|
|
||||||
const startTimeIdx = findCol(header, 'start time')
|
|
||||||
// Prefer decimal duration - always a simple number, no format ambiguity
|
|
||||||
const durDecIdx = header.findIndex(h => h.includes('duration (decimal)'))
|
|
||||||
const durHIdx = header.findIndex(h => h.includes('duration (h)'))
|
|
||||||
const durPlainIdx = header.findIndex(h => h === 'duration')
|
|
||||||
|
|
||||||
return rows.slice(1).map(row => {
|
|
||||||
let duration = 0
|
|
||||||
if (durDecIdx >= 0 && row[durDecIdx]) {
|
|
||||||
duration = Math.round(parseFloat(row[durDecIdx]) * 3600)
|
|
||||||
} else if (durHIdx >= 0 && row[durHIdx]) {
|
|
||||||
duration = parseDurationString(row[durHIdx])
|
|
||||||
} else if (durPlainIdx >= 0 && row[durPlainIdx]) {
|
|
||||||
duration = parseDurationString(row[durPlainIdx])
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
project_name: col(row, projIdx) || 'Imported',
|
|
||||||
client_name: col(row, clientIdx) || undefined,
|
|
||||||
task_name: col(row, taskIdx) || undefined,
|
|
||||||
description: col(row, descIdx) || undefined,
|
|
||||||
start_time: safeDateTime(col(row, startDateIdx), col(row, startTimeIdx) || undefined, dateFormat),
|
|
||||||
duration,
|
|
||||||
tags: tagIdx >= 0 && row[tagIdx] ? row[tagIdx].split(',').map(t => t.trim()).filter(Boolean) : undefined,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapHarvestCSV(rows: string[][], dateFormat?: DateFormat): ImportEntry[] {
|
|
||||||
const header = rows[0].map(h => h.toLowerCase().trim())
|
|
||||||
const dateIdx = findCol(header, 'date')
|
|
||||||
const projIdx = header.findIndex(h => h === 'project')
|
|
||||||
const clientIdx = findCol(header, 'client')
|
|
||||||
const taskIdx = findCol(header, 'task')
|
|
||||||
const notesIdx = findCol(header, 'notes')
|
|
||||||
const hoursIdx = header.findIndex(h => h === 'hours')
|
|
||||||
|
|
||||||
return rows.slice(1).map(row => ({
|
|
||||||
project_name: col(row, projIdx) || 'Imported',
|
|
||||||
client_name: col(row, clientIdx) || undefined,
|
|
||||||
task_name: col(row, taskIdx) || undefined,
|
|
||||||
description: col(row, notesIdx) || undefined,
|
|
||||||
start_time: safeDateTime(col(row, dateIdx), undefined, dateFormat),
|
|
||||||
duration: hoursIdx >= 0 && row[hoursIdx] ? Math.round(parseFloat(row[hoursIdx]) * 3600) : 0,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,3 +75,9 @@ export function mapGenericCSV(rows: string[][], mapping: Record<string, number>)
|
|||||||
duration: mapping.duration >= 0 ? parseDurationString(row[mapping.duration]) : 0,
|
duration: mapping.duration >= 0 ? parseDurationString(row[mapping.duration]) : 0,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function combineDateTime(date: string, time: string): string {
|
||||||
|
if (!date) return new Date().toISOString()
|
||||||
|
if (!time) return new Date(date).toISOString()
|
||||||
|
return new Date(`${date} ${time}`).toISOString()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1445,32 +1445,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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
|
<button
|
||||||
@click="executeImport"
|
@click="executeImport"
|
||||||
:disabled="isImporting"
|
:disabled="isImporting"
|
||||||
@@ -1567,7 +1541,7 @@ import AppSelect from '../components/AppSelect.vue'
|
|||||||
import AppShortcutRecorder from '../components/AppShortcutRecorder.vue'
|
import AppShortcutRecorder from '../components/AppShortcutRecorder.vue'
|
||||||
import AppTimePicker from '../components/AppTimePicker.vue'
|
import AppTimePicker from '../components/AppTimePicker.vue'
|
||||||
import { LOCALES, getCurrencies, getCurrencySymbol } from '../utils/locale'
|
import { LOCALES, getCurrencies, getCurrencySymbol } from '../utils/locale'
|
||||||
import { parseCSV, mapTogglCSV, mapClockifyCSV, mapHarvestCSV, mapGenericCSV, detectDateFormat, findCol, type ImportEntry, type DateFormat, type DateDetectResult } from '../utils/import'
|
import { parseCSV, mapTogglCSV, mapGenericCSV, type ImportEntry } from '../utils/import'
|
||||||
import { TIMER_FONTS, loadGoogleFont, loadAndApplyTimerFont } from '../utils/fonts'
|
import { TIMER_FONTS, loadGoogleFont, loadAndApplyTimerFont } from '../utils/fonts'
|
||||||
import { UI_FONTS, loadUIFont } from '../utils/uiFonts'
|
import { UI_FONTS, loadUIFont } from '../utils/uiFonts'
|
||||||
import { useFocusTrap } from '../utils/focusTrap'
|
import { useFocusTrap } from '../utils/focusTrap'
|
||||||
@@ -2004,18 +1978,10 @@ const importFileName = ref('')
|
|||||||
const importPreview = ref<string[][]>([])
|
const importPreview = ref<string[][]>([])
|
||||||
const importStatus = ref('')
|
const importStatus = ref('')
|
||||||
const isImporting = ref(false)
|
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 = [
|
const importFormats = [
|
||||||
{ label: 'Toggl CSV', value: 'toggl' },
|
{ label: 'Toggl CSV', value: 'toggl' },
|
||||||
{ label: 'Clockify CSV', value: 'clockify' },
|
{ label: 'Clockify CSV', value: 'clockify' },
|
||||||
{ label: 'Harvest CSV', value: 'harvest' },
|
|
||||||
{ label: 'Generic CSV', value: 'generic' },
|
{ label: 'Generic CSV', value: 'generic' },
|
||||||
{ label: 'ZeroClock JSON', value: 'json' },
|
{ label: 'ZeroClock JSON', value: 'json' },
|
||||||
]
|
]
|
||||||
@@ -2391,23 +2357,8 @@ async function handleImportFile() {
|
|||||||
|
|
||||||
if (importFormat.value === 'json') {
|
if (importFormat.value === 'json') {
|
||||||
importPreview.value = []
|
importPreview.value = []
|
||||||
importDateDetected.value = 'ambiguous'
|
|
||||||
importDateFormat.value = 'MDY'
|
|
||||||
} else {
|
} else {
|
||||||
const allRows = parseCSV(content)
|
importPreview.value = parseCSV(content).slice(0, 6)
|
||||||
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) {
|
} catch (e) {
|
||||||
console.error('Failed to read file:', e)
|
console.error('Failed to read file:', e)
|
||||||
@@ -2428,13 +2379,8 @@ async function executeImport() {
|
|||||||
const rows = parseCSV(importFileContent.value)
|
const rows = parseCSV(importFileContent.value)
|
||||||
let entries: ImportEntry[]
|
let entries: ImportEntry[]
|
||||||
|
|
||||||
const df = importDateFormat.value
|
if (importFormat.value === 'toggl' || importFormat.value === 'clockify') {
|
||||||
if (importFormat.value === 'toggl') {
|
entries = mapTogglCSV(rows)
|
||||||
entries = mapTogglCSV(rows, df)
|
|
||||||
} else if (importFormat.value === 'clockify') {
|
|
||||||
entries = mapClockifyCSV(rows, df)
|
|
||||||
} else if (importFormat.value === 'harvest') {
|
|
||||||
entries = mapHarvestCSV(rows, df)
|
|
||||||
} else {
|
} else {
|
||||||
entries = mapGenericCSV(rows, { project: 0, description: 1, start_time: 2, duration: 3, client: -1, task: -1 })
|
entries = mapGenericCSV(rows, { project: 0, description: 1, start_time: 2, duration: 3, client: -1, task: -1 })
|
||||||
}
|
}
|
||||||
@@ -2446,8 +2392,6 @@ async function executeImport() {
|
|||||||
importFileContent.value = ''
|
importFileContent.value = ''
|
||||||
importFileName.value = ''
|
importFileName.value = ''
|
||||||
importPreview.value = []
|
importPreview.value = []
|
||||||
importDateDetected.value = 'ambiguous'
|
|
||||||
importDateFormat.value = 'MDY'
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
importStatus.value = `Error: ${e}`
|
importStatus.value = `Error: ${e}`
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user