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 { if (!dur) return 0 // 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 } // Parse locale-variable date strings into YYYY-MM-DD function parseFlexDate(dateStr: string): string { if (!dateStr) return new Date().toISOString().split('T')[0] const s = dateStr.trim() // YYYY-MM-DD (ISO) if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(s)) { const [y, m, d] = s.split('-') return `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}` } // 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')}` // Ambiguous - assume MM/DD/YYYY (US default, most common Clockify setting) 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): string { const d = parseFlexDate(date) const t = time ? parseFlexTime(time) : '00:00:00' return `${d}T${t}` } 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[][]): 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 => ({ 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), duration: parseDurationString(col(row, durationIdx)), tags: tagIdx >= 0 && row[tagIdx] ? row[tagIdx].split(',').map(t => t.trim()).filter(Boolean) : undefined, })) } export function mapClockifyCSV(rows: string[][]): 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), duration, tags: tagIdx >= 0 && row[tagIdx] ? row[tagIdx].split(',').map(t => t.trim()).filter(Boolean) : undefined, } }) } export function mapHarvestCSV(rows: string[][]): 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)), duration: hoursIdx >= 0 && row[hoursIdx] ? Math.round(parseFloat(row[hoursIdx]) * 3600) : 0, })) } export function mapGenericCSV(rows: string[][], mapping: Record): 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, })) }