feat: add data import from CSV and JSON

Import utility with CSV parser, Toggl/Clockify format mapping, and
generic CSV column mapping. Settings Data tab has import UI with
file picker, format selector, preview table, and import execution.
This commit is contained in:
Your Name
2026-02-18 10:46:33 +02:00
parent bd3e0ba5a6
commit 8eb2d135c8

83
src/utils/import.ts Normal file
View File

@@ -0,0 +1,83 @@
export interface ImportEntry {
project_name: string
client_name?: string
task_name?: string
description?: string
start_time: string
end_time?: string
duration: number
tags?: string[]
}
export function parseCSV(text: string): string[][] {
const lines = text.split('\n').filter(l => l.trim())
return lines.map(line => {
const cells: string[] = []
let current = ''
let inQuotes = false
for (const char of line) {
if (char === '"') {
inQuotes = !inQuotes
} else if (char === ',' && !inQuotes) {
cells.push(current.trim())
current = ''
} else {
current += char
}
}
cells.push(current.trim())
return cells
})
}
export function parseDurationString(dur: string): number {
// Handle HH:MM:SS, HH:MM, or decimal hours
if (dur.includes(':')) {
const parts = dur.split(':').map(Number)
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]
if (parts.length === 2) return parts[0] * 3600 + parts[1] * 60
}
const hours = parseFloat(dur)
if (!isNaN(hours)) return Math.round(hours * 3600)
return 0
}
export function mapTogglCSV(rows: string[][]): ImportEntry[] {
// Toggl CSV format: Description, Project, Client, Task, Tag, Start date, Start time, End date, End time, Duration
const header = rows[0].map(h => h.toLowerCase())
const descIdx = header.findIndex(h => h.includes('description'))
const projIdx = header.findIndex(h => h.includes('project'))
const clientIdx = header.findIndex(h => h.includes('client'))
const taskIdx = header.findIndex(h => h.includes('task'))
const tagIdx = header.findIndex(h => h.includes('tag'))
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'))
return rows.slice(1).map(row => ({
project_name: row[projIdx] || 'Imported',
client_name: clientIdx >= 0 ? row[clientIdx] : undefined,
task_name: taskIdx >= 0 ? row[taskIdx] : undefined,
description: descIdx >= 0 ? row[descIdx] : undefined,
start_time: combineDateTime(row[startDateIdx], row[startTimeIdx]),
duration: durationIdx >= 0 ? parseDurationString(row[durationIdx]) : 0,
tags: tagIdx >= 0 && row[tagIdx] ? row[tagIdx].split(',').map(t => t.trim()) : undefined,
}))
}
export function mapGenericCSV(rows: string[][], mapping: Record<string, number>): ImportEntry[] {
return rows.slice(1).map(row => ({
project_name: row[mapping.project] || 'Imported',
client_name: mapping.client >= 0 ? row[mapping.client] : undefined,
task_name: mapping.task >= 0 ? row[mapping.task] : undefined,
description: mapping.description >= 0 ? row[mapping.description] : undefined,
start_time: row[mapping.start_time] || new Date().toISOString(),
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()
}