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:
83
src/utils/import.ts
Normal file
83
src/utils/import.ts
Normal 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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user