From 0dab43148f67f28e7738ba5567f5c1a804f9b8e9 Mon Sep 17 00:00:00 2001 From: AG Date: Fri, 28 Nov 2025 22:29:09 +0200 Subject: [PATCH] Plan state is persistent during session --- App.tsx | 32 +++---- components/Tracker.tsx | 82 ++++++++++++------ server/prisma/dev.db | Bin 61440 -> 61440 bytes server/src/routes/sessions.ts | 157 ++++++++++++++++++++++++++++++++++ services/storage.ts | 9 ++ 5 files changed, 238 insertions(+), 42 deletions(-) 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 c829101903f94f2a70dafe091a13914e1246a17d..f7513f9655e37032bec03182f249dbf295520012 100644 GIT binary patch delta 1379 zcma)*Jxmlq9L0AzP;Wo|j6^<;#6!+v;>_;s>`VhDQW$Lvvcy7+Gdr_Xh8PneK>`%g zn2*yw(-hb+bKUI&E8qks=fG8 z>a@HG!<+CIy?r11<6hn0^|t)?zODb}uGg$DwT~3A#q3%sF$>rrRxWIAuT)zh$6sag#e2)PAFv*WwH=>{Yf>krh9X; zc|>L@<5`4bKvSYEE3BbN|A8b}MT~_(3}unPnup8PvG!BbJ%7NI#vQk)?jPgV%sXjI2a&UE)N+Az#Aj+G)ZOM%d91vpg_2#yJmv5tX65{EDre- z?KosH5K+(FPT%fL+Y@tbe{-#42_-sX|lo^ZWqlMAl* zc6IOGXvJ?i(aY#Ycog0a-uWMcCBIdF-WeHQtIs~USgR(<(2(mSvzlW@X^cUdO9ZHq z8YoQ&K*pHZ#3|&sIQ}s0ySw}Qjjr7f-p>_Nk6EYn=&Ltc3A;|T6`n*iT8R$BZuBOq zNA=Fg)dwMHd5K$sE^9qu@oBr+IevQ0^~MLsit}yayL10mF*5vnX{}L9lEigf$EoTR zYAjL(41*fb)EJ;yrT{aP5@rmfNR+v|2wyaphkVBwSUp7xA+X743WV0&q6q>rBOGAH za-PaG&uCT}J!sMHw;tM_Rj6c^X95F-t#yiH0R)i*q!J?{G*uY!Qs`H^&1LU`MczGi z%|(vmlqisKC;^QX0U99=5Q0oem}+V-I6G=t%axp{I6Ky`bL>JC#`a^OgIx(I8$cUmwV49imOiL H^`C{mFijd~ 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'); };