workout-session.store.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. import { create } from "zustand";
  2. import { workoutSessionLocal } from "@/shared/lib/workout-session/workout-session.local";
  3. import { WorkoutSession } from "@/shared/lib/workout-session/types/workout-session";
  4. import { convertWeight, type WeightUnit } from "@/shared/lib/weight-conversion";
  5. import { WorkoutSessionExercise, WorkoutSet, WorkoutSetType, WorkoutSetUnit } from "@/features/workout-session/types/workout-set";
  6. import { useWorkoutBuilderStore } from "@/features/workout-builder/model/workout-builder.store";
  7. import { ExerciseWithAttributes } from "../../workout-builder/types";
  8. interface WorkoutSessionProgress {
  9. exerciseId: string;
  10. sets: {
  11. reps: number;
  12. weight?: number;
  13. duration?: number;
  14. }[];
  15. completed: boolean;
  16. }
  17. interface WorkoutSessionState {
  18. session: WorkoutSession | null;
  19. progress: Record<string, WorkoutSessionProgress>;
  20. elapsedTime: number;
  21. isTimerRunning: boolean;
  22. isWorkoutActive: boolean;
  23. currentExerciseIndex: number;
  24. currentExercise: WorkoutSessionExercise | null;
  25. // Progression
  26. exercisesCompleted: number;
  27. totalExercises: number;
  28. progressPercent: number;
  29. // Actions
  30. startWorkout: (exercises: ExerciseWithAttributes[], equipment: any[], muscles: any[]) => void;
  31. quitWorkout: () => void;
  32. completeWorkout: () => void;
  33. toggleTimer: () => void;
  34. resetTimer: () => void;
  35. updateExerciseProgress: (exerciseId: string, progressData: Partial<WorkoutSessionProgress>) => void;
  36. addSet: () => void;
  37. updateSet: (exerciseIndex: number, setIndex: number, data: Partial<WorkoutSet>) => void;
  38. removeSet: (exerciseIndex: number, setIndex: number) => void;
  39. finishSet: (exerciseIndex: number, setIndex: number) => void;
  40. goToNextExercise: () => void;
  41. goToPrevExercise: () => void;
  42. goToExercise: (targetIndex: number) => void;
  43. formatElapsedTime: () => string;
  44. getExercisesCompleted: () => number;
  45. getTotalExercises: () => number;
  46. getTotalVolume: () => number;
  47. getTotalVolumeInUnit: (unit: WeightUnit) => number;
  48. loadSessionFromLocal: () => void;
  49. }
  50. export const useWorkoutSessionStore = create<WorkoutSessionState>((set, get) => ({
  51. session: null,
  52. progress: {},
  53. elapsedTime: 0,
  54. isTimerRunning: false,
  55. isWorkoutActive: false,
  56. currentExerciseIndex: 0,
  57. currentExercise: null,
  58. exercisesCompleted: 0,
  59. totalExercises: 0,
  60. progressPercent: 0,
  61. startWorkout: (exercises, _equipment, muscles) => {
  62. const sessionExercises: WorkoutSessionExercise[] = exercises.map((ex, idx) => ({
  63. ...ex,
  64. order: idx,
  65. sets: [
  66. {
  67. id: `${ex.id}-set-1`,
  68. setIndex: 0,
  69. types: ["REPS", "WEIGHT"],
  70. valuesInt: [],
  71. valuesSec: [],
  72. units: [],
  73. completed: false,
  74. },
  75. ],
  76. }));
  77. const newSession: WorkoutSession = {
  78. id: Date.now().toString(),
  79. userId: "local",
  80. startedAt: new Date().toISOString(),
  81. exercises: sessionExercises,
  82. status: "active",
  83. muscles,
  84. };
  85. workoutSessionLocal.add(newSession);
  86. workoutSessionLocal.setCurrent(newSession.id);
  87. set({
  88. session: newSession,
  89. elapsedTime: 0,
  90. isTimerRunning: false,
  91. isWorkoutActive: true,
  92. currentExercise: sessionExercises[0],
  93. });
  94. },
  95. quitWorkout: () => {
  96. const { session } = get();
  97. if (session) {
  98. workoutSessionLocal.remove(session.id);
  99. }
  100. set({
  101. session: null,
  102. progress: {},
  103. elapsedTime: 0,
  104. isTimerRunning: false,
  105. isWorkoutActive: false,
  106. currentExerciseIndex: 0,
  107. currentExercise: null,
  108. });
  109. },
  110. completeWorkout: () => {
  111. const { session } = get();
  112. if (session) {
  113. workoutSessionLocal.update(session.id, { status: "completed", endedAt: new Date().toISOString() });
  114. set({
  115. session: { ...session, status: "completed", endedAt: new Date().toISOString() },
  116. progress: {},
  117. elapsedTime: 0,
  118. isTimerRunning: false,
  119. isWorkoutActive: false,
  120. });
  121. }
  122. useWorkoutBuilderStore.getState().setStep(1);
  123. },
  124. toggleTimer: () => {
  125. set((state) => {
  126. const newIsRunning = !state.isTimerRunning;
  127. if (state.session) {
  128. workoutSessionLocal.update(state.session.id, { isActive: newIsRunning });
  129. }
  130. return { isTimerRunning: newIsRunning };
  131. });
  132. },
  133. resetTimer: () => {
  134. set((state) => {
  135. if (state.session) {
  136. workoutSessionLocal.update(state.session.id, { duration: 0 });
  137. }
  138. return { elapsedTime: 0 };
  139. });
  140. },
  141. updateExerciseProgress: (exerciseId, progressData) => {
  142. set((state) => ({
  143. progress: {
  144. ...state.progress,
  145. [exerciseId]: {
  146. ...state.progress[exerciseId],
  147. exerciseId,
  148. sets: [],
  149. completed: false,
  150. ...progressData,
  151. },
  152. },
  153. }));
  154. },
  155. addSet: () => {
  156. const { session, currentExerciseIndex } = get();
  157. if (!session) return;
  158. const exIdx = currentExerciseIndex;
  159. const currentExercise = session.exercises[exIdx];
  160. const sets = currentExercise.sets;
  161. let typesToCopy: WorkoutSetType[] = ["REPS"];
  162. let unitsToCopy: WorkoutSetUnit[] = [];
  163. if (sets.length > 0) {
  164. const lastSet = sets[sets.length - 1];
  165. if (lastSet.types && lastSet.types.length > 0) {
  166. typesToCopy = [...lastSet.types];
  167. if (lastSet.units && lastSet.units.length > 0) {
  168. unitsToCopy = [...lastSet.units];
  169. }
  170. }
  171. }
  172. const newSet: WorkoutSet = {
  173. id: `${currentExercise.id}-set-${sets.length + 1}`,
  174. setIndex: sets.length,
  175. types: typesToCopy,
  176. valuesInt: [],
  177. valuesSec: [],
  178. units: unitsToCopy,
  179. completed: false,
  180. };
  181. const updatedExercises = session.exercises.map((ex, idx) => (idx === exIdx ? { ...ex, sets: [...ex.sets, newSet] } : ex));
  182. workoutSessionLocal.update(session.id, { exercises: updatedExercises });
  183. set({
  184. session: { ...session, exercises: updatedExercises },
  185. currentExercise: { ...updatedExercises[exIdx] },
  186. });
  187. },
  188. updateSet: (exerciseIndex, setIndex, data) => {
  189. const { session } = get();
  190. if (!session) return;
  191. const targetExercise = session.exercises[exerciseIndex];
  192. if (!targetExercise) return;
  193. const updatedSets = targetExercise.sets.map((set, idx) => (idx === setIndex ? { ...set, ...data } : set));
  194. const updatedExercises = session.exercises.map((ex, idx) => (idx === exerciseIndex ? { ...ex, sets: updatedSets } : ex));
  195. workoutSessionLocal.update(session.id, { exercises: updatedExercises });
  196. set({
  197. session: { ...session, exercises: updatedExercises },
  198. currentExercise: { ...updatedExercises[exerciseIndex] },
  199. });
  200. // handle exercisesCompleted
  201. },
  202. removeSet: (exerciseIndex, setIndex) => {
  203. const { session } = get();
  204. if (!session) return;
  205. const targetExercise = session.exercises[exerciseIndex];
  206. if (!targetExercise) return;
  207. const updatedSets = targetExercise.sets.filter((_, idx) => idx !== setIndex);
  208. const updatedExercises = session.exercises.map((ex, idx) => (idx === exerciseIndex ? { ...ex, sets: updatedSets } : ex));
  209. workoutSessionLocal.update(session.id, { exercises: updatedExercises });
  210. set({
  211. session: { ...session, exercises: updatedExercises },
  212. currentExercise: { ...updatedExercises[exerciseIndex] },
  213. });
  214. },
  215. finishSet: (exerciseIndex, setIndex) => {
  216. get().updateSet(exerciseIndex, setIndex, { completed: true });
  217. // if has completed all sets, go to next exercise
  218. const { session } = get();
  219. if (!session) return;
  220. const exercise = session.exercises[exerciseIndex];
  221. if (!exercise) return;
  222. if (exercise.sets.every((set) => set.completed)) {
  223. // get().goToNextExercise();
  224. // update exercisesCompleted
  225. const exercisesCompleted = get().exercisesCompleted;
  226. set({ exercisesCompleted: exercisesCompleted + 1 });
  227. }
  228. },
  229. goToNextExercise: () => {
  230. const { session, currentExerciseIndex } = get();
  231. if (!session) return;
  232. const idx = currentExerciseIndex;
  233. if (idx < session.exercises.length - 1) {
  234. workoutSessionLocal.update(session.id, { currentExerciseIndex: idx + 1 });
  235. set({
  236. currentExerciseIndex: idx + 1,
  237. currentExercise: session.exercises[idx + 1],
  238. });
  239. }
  240. },
  241. goToPrevExercise: () => {
  242. const { session, currentExerciseIndex } = get();
  243. if (!session) return;
  244. const idx = currentExerciseIndex;
  245. if (idx > 0) {
  246. workoutSessionLocal.update(session.id, { currentExerciseIndex: idx - 1 });
  247. set({
  248. currentExerciseIndex: idx - 1,
  249. currentExercise: session.exercises[idx - 1],
  250. });
  251. }
  252. },
  253. goToExercise: (targetIndex) => {
  254. const { session } = get();
  255. if (!session) return;
  256. if (targetIndex >= 0 && targetIndex < session.exercises.length) {
  257. workoutSessionLocal.update(session.id, { currentExerciseIndex: targetIndex });
  258. set({
  259. currentExerciseIndex: targetIndex,
  260. currentExercise: session.exercises[targetIndex],
  261. });
  262. }
  263. },
  264. getExercisesCompleted: () => {
  265. const { session } = get();
  266. if (!session) return 0;
  267. // only count exercises with at least one set
  268. return session.exercises
  269. .filter((exercise) => exercise.sets.length > 0)
  270. .filter((exercise) => exercise.sets.every((set) => set.completed)).length;
  271. },
  272. getTotalExercises: () => {
  273. const { session } = get();
  274. if (!session) return 0;
  275. return session.exercises.length;
  276. },
  277. getTotalVolume: () => {
  278. const { session } = get();
  279. if (!session) return 0;
  280. let totalVolume = 0;
  281. session.exercises.forEach((exercise) => {
  282. exercise.sets.forEach((set) => {
  283. // Vérifier si le set est complété et contient REPS et WEIGHT
  284. if (set.completed && set.types.includes("REPS") && set.types.includes("WEIGHT") && set.valuesInt) {
  285. const repsIndex = set.types.indexOf("REPS");
  286. const weightIndex = set.types.indexOf("WEIGHT");
  287. const reps = set.valuesInt[repsIndex] || 0;
  288. const weight = set.valuesInt[weightIndex] || 0;
  289. // Convertir les livres en kg si nécessaire
  290. const weightInKg =
  291. set.units && set.units[weightIndex] === "lbs"
  292. ? weight * 0.453592 // 1 lb = 0.453592 kg
  293. : weight;
  294. totalVolume += reps * weightInKg;
  295. }
  296. });
  297. });
  298. return Math.round(totalVolume);
  299. },
  300. getTotalVolumeInUnit: (unit: WeightUnit) => {
  301. const { session } = get();
  302. if (!session) return 0;
  303. let totalVolume = 0;
  304. session.exercises.forEach((exercise) => {
  305. exercise.sets.forEach((set) => {
  306. // Vérifier si le set est complété et contient REPS et WEIGHT
  307. if (set.completed && set.types.includes("REPS") && set.types.includes("WEIGHT") && set.valuesInt) {
  308. const repsIndex = set.types.indexOf("REPS");
  309. const weightIndex = set.types.indexOf("WEIGHT");
  310. const reps = set.valuesInt[repsIndex] || 0;
  311. const weight = set.valuesInt[weightIndex] || 0;
  312. // Déterminer l'unité de poids originale de la série
  313. const originalUnit: WeightUnit = set.units && set.units[weightIndex] === "lbs" ? "lbs" : "kg";
  314. // Convertir vers l'unité demandée
  315. const convertedWeight = convertWeight(weight, originalUnit, unit);
  316. totalVolume += reps * convertedWeight;
  317. }
  318. });
  319. });
  320. return Math.round(totalVolume * 10) / 10; // Arrondir à 1 décimale
  321. },
  322. formatElapsedTime: () => {
  323. const { elapsedTime } = get();
  324. const hours = Math.floor(elapsedTime / 3600);
  325. const minutes = Math.floor((elapsedTime % 3600) / 60);
  326. const secs = elapsedTime % 60;
  327. if (hours > 0) {
  328. return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
  329. }
  330. return `${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
  331. },
  332. loadSessionFromLocal: () => {
  333. const currentId = workoutSessionLocal.getCurrent();
  334. if (currentId) {
  335. const session = workoutSessionLocal.getById(currentId);
  336. if (session && session.status === "active") {
  337. set({
  338. session,
  339. isWorkoutActive: true,
  340. currentExerciseIndex: session.currentExerciseIndex ?? 0,
  341. currentExercise: session.exercises[session.currentExerciseIndex ?? 0],
  342. elapsedTime: 0,
  343. isTimerRunning: false,
  344. });
  345. }
  346. }
  347. },
  348. }));