import Papa from 'papaparse'; import type { Activity } from '@/types'; import { calculateTSS } from './tssCalculator'; import { validateCSV } from './csvValidator'; export function parseCSV(file: File, userFTP: number = 343): Promise { return new Promise((resolve, reject) => { Papa.parse(file, { header: true, skipEmptyLines: true, complete: (results) => { try { // Validate CSV structure first const validation = validateCSV(results.data); if (!validation.isValid) { const errorMessage = [ 'CSV Validation Failed:', '', ...validation.errors.map(err => `❌ ${err}`), ...(validation.warnings.length > 0 ? ['', 'Warnings:', ...validation.warnings.map(warn => `⚠️ ${warn}`)] : []), ].join('\n'); reject(new Error(errorMessage)); return; } // Log warnings to console if any if (validation.warnings.length > 0) { console.warn('CSV Validation Warnings:', validation.warnings); } const activities = results.data.map((row: any) => { // Parse date - Garmin format: "2025-12-02 09:19:49" const dateStr = row['Date'] || row['Activity Date'] || row['date']; if (!dateStr || dateStr === '--') { return null; } const date = new Date(dateStr); if (isNaN(date.getTime())) { return null; } // Parse distance - Garmin format: "8.34" (in km) let distance: number | undefined; const distanceStr = row['Distance'] || row['distance']; if (distanceStr && distanceStr !== '--' && distanceStr !== '0.00') { const parsed = parseFloat(distanceStr.replace(/,/g, '')); if (!isNaN(parsed) && parsed > 0) { distance = parsed; } } // Parse duration - Garmin format: "00:48:51" (HH:MM:SS) let duration: number | undefined; let durationSeconds: number | undefined; const durationStr = row['Time'] || row['Duration'] || row['Moving Time'] || row['Elapsed Time']; if (durationStr && durationStr !== '--') { if (durationStr.includes(':')) { const parts = durationStr.split(':'); const hours = parseInt(parts[0]) || 0; const minutes = parseInt(parts[1]) || 0; const seconds = parseFloat(parts[2]) || 0; duration = hours * 60 + minutes + seconds / 60; durationSeconds = hours * 3600 + minutes * 60 + seconds; } else { const parsed = parseFloat(durationStr); if (!isNaN(parsed)) { duration = parsed; durationSeconds = parsed * 60; // Assume minutes if no colon } } } // Parse TSS - Try to calculate from power data first let trainingStressScore: number | undefined; // First, check if TSS is already provided in CSV const tssStr = row['Training Stress Score®'] || row['Training Stress Score'] || row['TSS']; if (tssStr && tssStr !== '--' && tssStr !== '0.0' && tssStr !== '0') { const parsed = parseFloat(tssStr); if (!isNaN(parsed) && parsed > 0) { trainingStressScore = parsed; } } // If no TSS provided, try to calculate from power data if (!trainingStressScore && durationSeconds) { // Parse Normalized Power (NP) - find column that starts with "Normalized Power" let normalizedPower: number | undefined; const npKey = Object.keys(row).find(key => key.startsWith('Normalized Power')); if (npKey) { const npStr = row[npKey]; if (npStr && npStr !== '--' && npStr !== '0.0' && npStr !== '0') { normalizedPower = parseFloat(npStr); if (isNaN(normalizedPower) || normalizedPower <= 0) { normalizedPower = undefined; } } } // Use FTP from CSV if available, otherwise use user-provided FTP const ftpStr = row['FTP'] || row['Functional Threshold Power']; const ftp = ftpStr && ftpStr !== '--' ? parseFloat(ftpStr) : userFTP; if (normalizedPower && normalizedPower > 0 && ftp && ftp > 0) { const calculatedTSS = calculateTSS({ durationSeconds, normalizedPower, ftp, }); if (calculatedTSS !== undefined) { trainingStressScore = calculatedTSS; } } } // Get activity type const activityType = row['Activity Type'] || row['Type'] || row['Sport']; const activity: Activity = { date, activityType, distance, duration, trainingStressScore, }; return activity; }).filter((activity): activity is Activity => activity !== null); // Sort by date activities.sort((a, b) => a.date.getTime() - b.date.getTime()); resolve(activities); } catch (error) { reject(new Error('Failed to parse CSV data')); } }, error: (error) => { reject(new Error(`CSV parsing error: ${error.message}`)); }, }); }); }