diff --git a/App.tsx b/App.tsx index 3bacdb4..3afa478 100644 --- a/App.tsx +++ b/App.tsx @@ -9,7 +9,7 @@ import Plans from './components/Plans'; import Login from './components/Login'; import Profile from './components/Profile'; import { TabView, WorkoutSession, WorkoutSet, WorkoutPlan, User, Language } from './types'; -import { getSessions, saveSession, deleteSession, getPlans, getActiveSession, updateActiveSession, deleteActiveSession } from './services/storage'; +import { getSessions, saveSession, deleteSession, getPlans, getActiveSession, updateActiveSession, deleteActiveSession, updateSetInActiveSession, deleteSetFromActiveSession } from './services/storage'; import { getCurrentUserProfile, getMe } from './services/auth'; import { getSystemLanguage } from './services/i18n'; import { generateId } from './utils/uuid'; @@ -132,39 +132,35 @@ function App() { } }; - const handleAddSet = async (set: WorkoutSet) => { + const handleAddSet = (set: WorkoutSet) => { if (activeSession && currentUser) { const updatedSession = { ...activeSession, sets: [...activeSession.sets, set] }; setActiveSession(updatedSession); - // Save to database - await updateActiveSession(currentUser.id, updatedSession); } }; const handleRemoveSetFromActive = async (setId: string) => { if (activeSession && currentUser) { - const updatedSession = { - ...activeSession, - sets: activeSession.sets.filter(s => s.id !== setId) - }; - setActiveSession(updatedSession); - // Save to database - await updateActiveSession(currentUser.id, updatedSession); + await deleteSetFromActiveSession(currentUser.id, setId); + const updatedSession = { + ...activeSession, + sets: activeSession.sets.filter(s => s.id !== setId) + }; + setActiveSession(updatedSession); } }; const handleUpdateSetInActive = async (updatedSet: WorkoutSet) => { if (activeSession && currentUser) { - const updatedSession = { - ...activeSession, - sets: activeSession.sets.map(s => s.id === updatedSet.id ? updatedSet : s) - }; - setActiveSession(updatedSession); - // Save to database - await updateActiveSession(currentUser.id, updatedSession); + const response = await updateSetInActiveSession(currentUser.id, updatedSet.id, updatedSet); + const updatedSession = { + ...activeSession, + sets: activeSession.sets.map(s => s.id === updatedSet.id ? response : s) + }; + setActiveSession(updatedSession); } }; diff --git a/components/Tracker.tsx b/components/Tracker.tsx index c9ab2b9..da3238c 100644 --- a/components/Tracker.tsx +++ b/components/Tracker.tsx @@ -6,6 +6,7 @@ import { getExercises, getLastSetForExercise, saveExercise, getPlans } from '../ import { getCurrentUserProfile } from '../services/auth'; import { t } from '../services/i18n'; import { generateId } from '../utils/uuid'; +import { api } from '../services/api'; interface TrackerProps { userId: string; @@ -99,6 +100,32 @@ const Tracker: React.FC = ({ userId, userWeight, activeSession, ac return () => clearInterval(interval); }, [activeSession]); + // Recalculate current step when sets change + useEffect(() => { + if (activeSession && activePlan) { + const performedCounts = new Map(); + for (const set of activeSession.sets) { + performedCounts.set(set.exerciseId, (performedCounts.get(set.exerciseId) || 0) + 1); + } + + let nextStepIndex = activePlan.steps.length; // Default to finished + const plannedCounts = new Map(); + for (let i = 0; i < activePlan.steps.length; i++) { + const step = activePlan.steps[i]; + const exerciseId = step.exerciseId; + plannedCounts.set(exerciseId, (plannedCounts.get(exerciseId) || 0) + 1); + const performedCount = performedCounts.get(exerciseId) || 0; + + if (performedCount < plannedCounts.get(exerciseId)!) { + nextStepIndex = i; + break; + } + } + setCurrentStepIndex(nextStepIndex); + } + }, [activeSession, activePlan]); + + useEffect(() => { if (activeSession && activePlan && exercises.length > 0 && activePlan.steps.length > 0) { if (currentStepIndex < activePlan.steps.length) { @@ -166,54 +193,61 @@ const Tracker: React.FC = ({ userId, userWeight, activeSession, ac } } - const handleAddSet = () => { + const handleAddSet = async () => { if (!activeSession || !selectedExercise) return; - const newSet: WorkoutSet = { - id: generateId(), + const setData: Partial = { exerciseId: selectedExercise.id, - exerciseName: selectedExercise.name, - type: selectedExercise.type, - timestamp: Date.now(), }; switch (selectedExercise.type) { case ExerciseType.STRENGTH: - if (weight) newSet.weight = parseFloat(weight); - if (reps) newSet.reps = parseInt(reps); + if (weight) setData.weight = parseFloat(weight); + if (reps) setData.reps = parseInt(reps); break; case ExerciseType.BODYWEIGHT: - if (weight) newSet.weight = parseFloat(weight); - if (reps) newSet.reps = parseInt(reps); - newSet.bodyWeightPercentage = parseFloat(bwPercentage) || 100; + if (weight) setData.weight = parseFloat(weight); + if (reps) setData.reps = parseInt(reps); + setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; break; case ExerciseType.CARDIO: - if (duration) newSet.durationSeconds = parseInt(duration); - if (distance) newSet.distanceMeters = parseFloat(distance); + if (duration) setData.durationSeconds = parseInt(duration); + if (distance) setData.distanceMeters = parseFloat(distance); break; case ExerciseType.STATIC: - if (duration) newSet.durationSeconds = parseInt(duration); - newSet.bodyWeightPercentage = parseFloat(bwPercentage) || 100; + if (duration) setData.durationSeconds = parseInt(duration); + setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; break; case ExerciseType.HIGH_JUMP: - if (height) newSet.height = parseFloat(height); + if (height) setData.height = parseFloat(height); break; case ExerciseType.LONG_JUMP: - if (distance) newSet.distanceMeters = parseFloat(distance); + if (distance) setData.distanceMeters = parseFloat(distance); break; case ExerciseType.PLYOMETRIC: - if (reps) newSet.reps = parseInt(reps); + if (reps) setData.reps = parseInt(reps); break; } - onSetAdded(newSet); + try { + const response = await api.post('/sessions/active/log-set', setData); + if (response.success) { + const { newSet, activeExerciseId } = response; + onSetAdded(newSet); - if (activePlan) { - const currentStep = activePlan.steps[currentStepIndex]; - if (currentStep && currentStep.exerciseId === selectedExercise.id) { - const nextIndex = currentStepIndex + 1; - setCurrentStepIndex(nextIndex); + if (activePlan && activeExerciseId) { + const nextStepIndex = activePlan.steps.findIndex(step => step.exerciseId === activeExerciseId); + if (nextStepIndex !== -1) { + setCurrentStepIndex(nextStepIndex); + } + } else if (activePlan && !activeExerciseId) { + // Plan is finished + setCurrentStepIndex(activePlan.steps.length); + } } + } catch (error) { + console.error("Failed to log set:", error); + // Optionally, show an error message to the user } }; diff --git a/server/prisma/dev.db b/server/prisma/dev.db index c829101..f7513f9 100644 Binary files a/server/prisma/dev.db and b/server/prisma/dev.db differ diff --git a/server/src/routes/sessions.ts b/server/src/routes/sessions.ts index f9dece2..78eea1a 100644 --- a/server/src/routes/sessions.ts +++ b/server/src/routes/sessions.ts @@ -219,6 +219,163 @@ router.put('/active', async (req: any, res) => { } }); +// 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 { diff --git a/services/storage.ts b/services/storage.ts index d1b1d10..2b00de6 100644 --- a/services/storage.ts +++ b/services/storage.ts @@ -51,6 +51,15 @@ export const updateActiveSession = async (userId: string, session: WorkoutSessio await api.put('/sessions/active', session); }; +export const deleteSetFromActiveSession = async (userId: string, setId: string): Promise => { + await api.delete(`/sessions/active/set/${setId}`); +}; + +export const updateSetInActiveSession = async (userId: string, setId: string, setData: Partial): Promise => { + const response = await api.put(`/sessions/active/set/${setId}`, setData); + return response.updatedSet; +}; + export const deleteActiveSession = async (userId: string): Promise => { await api.delete('/sessions/active'); };