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.
84 lines
3.1 KiB
TypeScript
84 lines
3.1 KiB
TypeScript
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()
|
|
}
|