import { NextRequest, NextResponse } from "next/server"; import { getCurrentUser } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; // Exercise name mapping - CSV shorthand to DB names const NAME_MAP: Record = { "Ab Wheel": "Ab Wheel Rollout", "BB Upright Row": "Upright Row", "Ball Situp": "Exercise Ball Situp", "Bench": "Bench Press", "DB Lateral Raise": "Lateral Raise", "Dip": "Dips (Chest)", "Face Pull": "Face Pulls", "SA Lat Pulldown": "Lat Pulldown", "SL Calf Raise": "Calf Raise", "BB Row": "Barbell Row", "DB Row": "Dumbbell Row", "GHD": "Glute ham developer", "Hamstring DL": "Hamstring deadlift", "BB Curl": "Barbell Curl", "BB Hip Bridge": "Hip Thrust", "Cable Trap": "Rear delt", "Chinup (Narrow)": "Chinup", "Chinup Negatives": "Chinup", "Squat (Foot Elevated)": "Squat", "Ball Bicep Curl": "Dumbbell Curl", "KB Hip Flexor": "Hip Flexor", "Hamstring Deadlift": "Hamstring deadlift", "Shoulder Press": "Overhead Press", "CoC": "Captains of Crush", "Hex DL": "Hex Bar Deadlift", "KB Extension": "Kettlebell Leg Extension", "Ski": "SkiErg", }; interface ParsedSet { setNumber: number; weight?: number; weightUnit: string; reps?: number; notes?: string; } interface ParsedExercise { exerciseId: string; exerciseName: string; sets: ParsedSet[]; } interface ParsedWorkout { date: string; exercises: ParsedExercise[]; } interface ParseResponse { workouts: ParsedWorkout[]; unmapped: string[]; } function parseCSV(content: string): Array> { const lines = content.trim().split("\n"); if (lines.length === 0) return []; // Parse header const header = lines[0].split(",").map((h) => h.trim().toLowerCase()); const rows = []; // Parse data rows for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const values = line.split(",").map((v) => v.trim()); const row: Record = {}; header.forEach((col, idx) => { if (values[idx]) { row[col] = values[idx]; } }); rows.push(row); } return rows; } function getVariationNote(originalName: string): string | null { if (originalName.includes("Narrow")) return "narrow"; if (originalName.includes("Negatives")) return "negatives"; if (originalName.includes("Foot Elevated")) return "foot elevated"; return null; } function resolveExerciseName(csvName: string): string { // Check if it's in the name map if (NAME_MAP[csvName]) { return NAME_MAP[csvName]; } // Return as-is for direct lookup return csvName; } // Parse dates like "1/27/2026" or "2026-01-27" into ISO date string function parseDate(dateStr: string): string { // Try M/D/YYYY format const mdyMatch = dateStr.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if (mdyMatch) { const month = mdyMatch[1].padStart(2, "0"); const day = mdyMatch[2].padStart(2, "0"); const year = mdyMatch[3]; return `${year}-${month}-${day}T12:00:00.000Z`; } // Try ISO format if (dateStr.includes("-")) { return new Date(dateStr + "T12:00:00.000Z").toISOString(); } // Fallback return new Date(dateStr).toISOString(); } export async function POST(request: NextRequest) { try { const user = await getCurrentUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const formData = await request.formData(); const file = formData.get("file") as File; if (!file) { return NextResponse.json({ error: "No file provided" }, { status: 400 }); } if (!file.name.endsWith(".csv")) { return NextResponse.json( { error: "File must be a CSV file" }, { status: 400 } ); } const content = await file.text(); const rows = parseCSV(content); if (rows.length === 0) { return NextResponse.json( { error: "CSV is empty or invalid format" }, { status: 400 } ); } // Get all user exercises for matching const exercises = await prisma.exercise.findMany({ where: { userId: user.id }, select: { id: true, name: true }, }); // Build case-insensitive lookup map const exerciseMap = new Map(); for (const ex of exercises) { exerciseMap.set(ex.name.toLowerCase(), ex.id); } // Group rows by date const workoutsByDate = new Map>>(); const unmappedExercises = new Set(); for (const row of rows) { const date = row.date || row.date_str || row.workout_date || ""; const exerciseName = row.exercise || row.exercise_name || ""; if (!date || !exerciseName) { continue; } if (!workoutsByDate.has(date)) { workoutsByDate.set(date, []); } workoutsByDate.get(date)!.push(row); // Check if exercise can be resolved const resolvedName = resolveExerciseName(exerciseName); const isKnown = exerciseMap.has(resolvedName.toLowerCase()); if (!isKnown) { unmappedExercises.add(exerciseName); } } // Build parsed workouts const parsedWorkouts: ParsedWorkout[] = []; for (const [date, rowsForDate] of workoutsByDate) { const exercisesMap = new Map< string, { exerciseId: string; exerciseName: string; sets: ParsedSet[]; } >(); for (const row of rowsForDate) { const csvExerciseName = row.exercise || row.exercise_name || ""; const resolvedName = resolveExerciseName(csvExerciseName); const exerciseId = exerciseMap.get(resolvedName.toLowerCase()) || ""; if (!exerciseId) { unmappedExercises.add(csvExerciseName); continue; } if (!exercisesMap.has(exerciseId)) { exercisesMap.set(exerciseId, { exerciseId, exerciseName: resolvedName, sets: [], }); } const exerciseData = exercisesMap.get(exerciseId)!; const weight = row.weight ? parseFloat(row.weight) : undefined; const reps = row.reps ? parseInt(row.reps, 10) : undefined; let notes = row.notes || ""; // Detect weight unit from notes let weightUnit = "lbs"; if (notes.toLowerCase().includes("kg")) { weightUnit = "kg"; } // Add variation note if applicable const variationNote = getVariationNote(csvExerciseName); if (variationNote) { notes = notes ? `${notes} (${variationNote})` : `(${variationNote})`; } const setNumber = exerciseData.sets.length + 1; exerciseData.sets.push({ setNumber, weight, weightUnit, reps, notes: notes || undefined, }); } const workoutExercises = Array.from(exercisesMap.values()); if (workoutExercises.length > 0) { parsedWorkouts.push({ date: parseDate(date), exercises: workoutExercises, }); } } // Sort by date ascending (oldest first) parsedWorkouts.sort( (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() ); const response: ParseResponse = { workouts: parsedWorkouts, unmapped: Array.from(unmappedExercises), }; return NextResponse.json(response); } catch (error) { console.error("CSV parsing error:", error); return NextResponse.json( { error: "Failed to parse CSV file" }, { status: 500 } ); } }