import express from 'express'; import { PrismaClient } from '@prisma/client'; import jwt from 'jsonwebtoken'; const router = express.Router(); const prisma = new PrismaClient(); const JWT_SECRET = process.env.JWT_SECRET || 'secret'; const authenticate = (req: any, res: any, next: any) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ error: 'Unauthorized' }); try { const decoded = jwt.verify(token, JWT_SECRET) as any; req.user = decoded; next(); } catch { res.status(401).json({ error: 'Invalid token' }); } }; router.use(authenticate); // Get all sessions router.get('/', async (req: any, res) => { try { const userId = req.user.userId; const sessions = await prisma.workoutSession.findMany({ where: { userId }, include: { sets: { include: { exercise: true } } }, orderBy: { startTime: 'desc' } }); res.json(sessions); } catch (error) { res.status(500).json({ error: 'Server error' }); } }); // Save session (create or update) router.post('/', async (req: any, res) => { try { const userId = req.user.userId; const { id, startTime, endTime, userBodyWeight, note, planId, planName, sets } = req.body; // Convert timestamps to Date objects if they are numbers const start = new Date(startTime); const end = endTime ? new Date(endTime) : null; const weight = userBodyWeight ? parseFloat(userBodyWeight) : null; // Check if session exists const existing = await prisma.workoutSession.findUnique({ where: { id } }); if (existing) { // Update // First delete existing sets to replace them (simplest strategy for now) await prisma.workoutSet.deleteMany({ where: { sessionId: id } }); const updated = await prisma.workoutSession.update({ where: { id }, data: { startTime: start, endTime: end, userBodyWeight: weight, note, planId, planName, sets: { create: sets.map((s: any, idx: number) => ({ exerciseId: s.exerciseId, order: idx, weight: s.weight, reps: s.reps, distanceMeters: s.distanceMeters, durationSeconds: s.durationSeconds, completed: s.completed !== undefined ? s.completed : true })) } }, include: { sets: true } }); return res.json(updated); } else { // Create const created = await prisma.workoutSession.create({ data: { id, // Use provided ID or let DB gen? Frontend usually generates UUIDs. userId, startTime: start, endTime: end, userBodyWeight: weight, note, planId, planName, sets: { create: sets.map((s: any, idx: number) => ({ exerciseId: s.exerciseId, order: idx, weight: s.weight, reps: s.reps, distanceMeters: s.distanceMeters, durationSeconds: s.durationSeconds, completed: s.completed !== undefined ? s.completed : true })) } }, include: { sets: true } }); // Update user profile weight if session has weight and is finished if (weight && end) { await prisma.userProfile.upsert({ where: { userId }, create: { userId, weight }, update: { weight } }); } return res.json(created); } // Update user profile weight if session has weight and is finished (for update case too) if (weight && end) { await prisma.userProfile.upsert({ where: { userId }, create: { userId, weight }, update: { weight } }); } } catch (error) { console.error(error); res.status(500).json({ error: 'Server error' }); } }); // Get active session (session without endTime) router.get('/active', async (req: any, res) => { try { const userId = req.user.userId; const activeSession = await prisma.workoutSession.findFirst({ where: { userId, endTime: null }, include: { sets: { include: { exercise: true }, orderBy: { order: 'asc' } } } }); if (!activeSession) { return res.json({ success: true, session: null }); } res.json({ success: true, session: activeSession }); } catch (error) { console.error(error); res.status(500).json({ error: 'Server error' }); } }); // Update active session (for real-time set updates) router.put('/active', async (req: any, res) => { try { const userId = req.user.userId; const { id, startTime, endTime, userBodyWeight, note, planId, planName, sets } = req.body; // Convert timestamps to Date objects if they are numbers const start = new Date(startTime); const end = endTime ? new Date(endTime) : null; const weight = userBodyWeight ? parseFloat(userBodyWeight) : null; // Check if session exists and belongs to user const existing = await prisma.workoutSession.findFirst({ where: { id, userId } }); if (!existing) { return res.status(404).json({ error: 'Session not found' }); } // Delete existing sets to replace them await prisma.workoutSet.deleteMany({ where: { sessionId: id } }); const updated = await prisma.workoutSession.update({ where: { id }, data: { startTime: start, endTime: end, userBodyWeight: weight, note, planId, planName, sets: { create: sets.map((s: any, idx: number) => ({ exerciseId: s.exerciseId, order: idx, weight: s.weight, reps: s.reps, distanceMeters: s.distanceMeters, durationSeconds: s.durationSeconds, completed: s.completed !== undefined ? s.completed : true })) } }, include: { sets: { include: { exercise: true } } } }); // Update user profile weight if session has weight and is finished if (weight && end) { await prisma.userProfile.upsert({ where: { userId }, create: { userId, weight }, update: { weight } }); } res.json({ success: true, session: updated }); } catch (error) { console.error(error); res.status(500).json({ error: 'Server error' }); } }); // Log a set to the active session router.post('/active/log-set', async (req: any, res) => { try { const userId = req.user.userId; const { exerciseId, reps, weight, distanceMeters, durationSeconds } = req.body; // Find active session const activeSession = await prisma.workoutSession.findFirst({ where: { userId, endTime: null }, include: { sets: true } }); if (!activeSession) { return res.status(404).json({ error: 'No active session found' }); } // Get the highest order value from the existing sets const maxOrder = activeSession.sets.reduce((max, set) => Math.max(max, set.order), -1); // Create the new set const newSet = await prisma.workoutSet.create({ data: { sessionId: activeSession.id, exerciseId, order: maxOrder + 1, reps: reps ? parseInt(reps) : null, weight: weight ? parseFloat(weight) : null, distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null, durationSeconds: durationSeconds ? parseInt(durationSeconds) : null, completed: true }, include: { exercise: true } }); // Recalculate active step if (activeSession.planId) { const plan = await prisma.workoutPlan.findUnique({ where: { id: activeSession.planId } }); if (plan) { const planExercises: { id: string }[] = JSON.parse(plan.exercises); const allPerformedSets = await prisma.workoutSet.findMany({ where: { sessionId: activeSession.id } }); const performedCounts = new Map(); for (const set of allPerformedSets) { performedCounts.set(set.exerciseId, (performedCounts.get(set.exerciseId) || 0) + 1); } let activeExerciseId = null; const plannedCounts = new Map(); for (const planExercise of planExercises) { const exerciseId = planExercise.id; plannedCounts.set(exerciseId, (plannedCounts.get(exerciseId) || 0) + 1); const performedCount = performedCounts.get(exerciseId) || 0; if (performedCount < plannedCounts.get(exerciseId)!) { activeExerciseId = exerciseId; break; } } const mappedNewSet = { ...newSet, exerciseName: newSet.exercise.name, type: newSet.exercise.type }; return res.json({ success: true, newSet: mappedNewSet, activeExerciseId }); } } // If no plan or plan not found, just return the new set const mappedNewSet = { ...newSet, exerciseName: newSet.exercise.name, type: newSet.exercise.type }; res.json({ success: true, newSet: mappedNewSet, activeExerciseId: null }); } catch (error) { console.error(error); res.status(500).json({ error: 'Server error' }); } }); // Update a set in the active session router.put('/active/set/:setId', async (req: any, res) => { try { const userId = req.user.userId; const { setId } = req.params; const { reps, weight, distanceMeters, durationSeconds } = req.body; // Find active session const activeSession = await prisma.workoutSession.findFirst({ where: { userId, endTime: null }, }); if (!activeSession) { return res.status(404).json({ error: 'No active session found' }); } const updatedSet = await prisma.workoutSet.update({ where: { id: setId }, data: { reps: reps ? parseInt(reps) : null, weight: weight ? parseFloat(weight) : null, distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null, durationSeconds: durationSeconds ? parseInt(durationSeconds) : null, }, include: { exercise: true } }); const mappedUpdatedSet = { ...updatedSet, exerciseName: updatedSet.exercise.name, type: updatedSet.exercise.type }; res.json({ success: true, updatedSet: mappedUpdatedSet }); } catch (error) { console.error(error); res.status(500).json({ error: 'Server error' }); } }); // Delete a set from the active session router.delete('/active/set/:setId', async (req: any, res) => { try { const userId = req.user.userId; const { setId } = req.params; // Find active session const activeSession = await prisma.workoutSession.findFirst({ where: { userId, endTime: null }, }); if (!activeSession) { return res.status(404).json({ error: 'No active session found' }); } await prisma.workoutSet.delete({ where: { id: setId } }); res.json({ success: true }); } catch (error) { console.error(error); res.status(500).json({ error: 'Server error' }); } }); // Delete active session (quit without saving) router.delete('/active', async (req: any, res) => { try { const userId = req.user.userId; // Find active session const activeSession = await prisma.workoutSession.findFirst({ where: { userId, endTime: null } }); if (!activeSession) { return res.json({ success: true, message: 'No active session found' }); } // Delete the session (cascade will delete sets) await prisma.workoutSession.delete({ where: { id: activeSession.id } }); res.json({ success: true }); } catch (error) { console.error(error); res.status(500).json({ error: 'Server error' }); } }); // Delete session router.delete('/:id', async (req: any, res) => { try { const userId = req.user.userId; const { id } = req.params; await prisma.workoutSession.delete({ where: { id, userId } // Ensure user owns it }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: 'Server error' }); } }); export default router;