Plan state is persistent during session

This commit is contained in:
AG
2025-11-28 22:29:09 +02:00
parent a98839585d
commit 0dab43148f
5 changed files with 238 additions and 42 deletions

14
App.tsx
View File

@@ -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) {
await deleteSetFromActiveSession(currentUser.id, setId);
const updatedSession = {
...activeSession,
sets: activeSession.sets.filter(s => s.id !== setId)
};
setActiveSession(updatedSession);
// Save to database
await updateActiveSession(currentUser.id, updatedSession);
}
};
const handleUpdateSetInActive = async (updatedSet: WorkoutSet) => {
if (activeSession && currentUser) {
const response = await updateSetInActiveSession(currentUser.id, updatedSet.id, updatedSet);
const updatedSession = {
...activeSession,
sets: activeSession.sets.map(s => s.id === updatedSet.id ? updatedSet : s)
sets: activeSession.sets.map(s => s.id === updatedSet.id ? response : s)
};
setActiveSession(updatedSession);
// Save to database
await updateActiveSession(currentUser.id, updatedSession);
}
};

View File

@@ -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<TrackerProps> = ({ userId, userWeight, activeSession, ac
return () => clearInterval(interval);
}, [activeSession]);
// Recalculate current step when sets change
useEffect(() => {
if (activeSession && activePlan) {
const performedCounts = new Map<string, number>();
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<string, number>();
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<TrackerProps> = ({ userId, userWeight, activeSession, ac
}
}
const handleAddSet = () => {
const handleAddSet = async () => {
if (!activeSession || !selectedExercise) return;
const newSet: WorkoutSet = {
id: generateId(),
const setData: Partial<WorkoutSet> = {
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;
}
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
}
};

Binary file not shown.

View File

@@ -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<string, number>();
for (const set of allPerformedSets) {
performedCounts.set(set.exerciseId, (performedCounts.get(set.exerciseId) || 0) + 1);
}
let activeExerciseId = null;
const plannedCounts = new Map<string, number>();
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 {

View File

@@ -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<void> => {
await api.delete(`/sessions/active/set/${setId}`);
};
export const updateSetInActiveSession = async (userId: string, setId: string, setData: Partial<WorkoutSet>): Promise<WorkoutSet> => {
const response = await api.put(`/sessions/active/set/${setId}`, setData);
return response.updatedSet;
};
export const deleteActiveSession = async (userId: string): Promise<void> => {
await api.delete('/sessions/active');
};