workout-session.store.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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 } 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 sets = session.exercises[exIdx].sets;
  160. const newSet: WorkoutSet = {
  161. id: `${session.exercises[exIdx].id}-set-${sets.length + 1}`,
  162. setIndex: sets.length,
  163. types: ["REPS"],
  164. valuesInt: [],
  165. valuesSec: [],
  166. units: [],
  167. completed: false,
  168. };
  169. const updatedExercises = session.exercises.map((ex, idx) => (idx === exIdx ? { ...ex, sets: [...ex.sets, newSet] } : ex));
  170. workoutSessionLocal.update(session.id, { exercises: updatedExercises });
  171. set({
  172. session: { ...session, exercises: updatedExercises },
  173. currentExercise: { ...updatedExercises[exIdx] },
  174. });
  175. },
  176. updateSet: (exerciseIndex, setIndex, data) => {
  177. const { session } = get();
  178. if (!session) return;
  179. const targetExercise = session.exercises[exerciseIndex];
  180. if (!targetExercise) return;
  181. const updatedSets = targetExercise.sets.map((set, idx) => (idx === setIndex ? { ...set, ...data } : set));
  182. const updatedExercises = session.exercises.map((ex, idx) => (idx === exerciseIndex ? { ...ex, sets: updatedSets } : ex));
  183. workoutSessionLocal.update(session.id, { exercises: updatedExercises });
  184. set({
  185. session: { ...session, exercises: updatedExercises },
  186. currentExercise: { ...updatedExercises[exerciseIndex] },
  187. });
  188. // handle exercisesCompleted
  189. },
  190. removeSet: (exerciseIndex, setIndex) => {
  191. const { session } = get();
  192. if (!session) return;
  193. const targetExercise = session.exercises[exerciseIndex];
  194. if (!targetExercise) return;
  195. const updatedSets = targetExercise.sets.filter((_, idx) => idx !== setIndex);
  196. const updatedExercises = session.exercises.map((ex, idx) => (idx === exerciseIndex ? { ...ex, sets: updatedSets } : ex));
  197. workoutSessionLocal.update(session.id, { exercises: updatedExercises });
  198. set({
  199. session: { ...session, exercises: updatedExercises },
  200. currentExercise: { ...updatedExercises[exerciseIndex] },
  201. });
  202. },
  203. finishSet: (exerciseIndex, setIndex) => {
  204. get().updateSet(exerciseIndex, setIndex, { completed: true });
  205. // if has completed all sets, go to next exercise
  206. const { session } = get();
  207. if (!session) return;
  208. const exercise = session.exercises[exerciseIndex];
  209. if (!exercise) return;
  210. if (exercise.sets.every((set) => set.completed)) {
  211. // get().goToNextExercise();
  212. // update exercisesCompleted
  213. const exercisesCompleted = get().exercisesCompleted;
  214. set({ exercisesCompleted: exercisesCompleted + 1 });
  215. }
  216. },
  217. goToNextExercise: () => {
  218. const { session, currentExerciseIndex } = get();
  219. if (!session) return;
  220. const idx = currentExerciseIndex;
  221. if (idx < session.exercises.length - 1) {
  222. workoutSessionLocal.update(session.id, { currentExerciseIndex: idx + 1 });
  223. set({
  224. currentExerciseIndex: idx + 1,
  225. currentExercise: session.exercises[idx + 1],
  226. });
  227. }
  228. },
  229. goToPrevExercise: () => {
  230. const { session, currentExerciseIndex } = get();
  231. if (!session) return;
  232. const idx = currentExerciseIndex;
  233. if (idx > 0) {
  234. workoutSessionLocal.update(session.id, { currentExerciseIndex: idx - 1 });
  235. set({
  236. currentExerciseIndex: idx - 1,
  237. currentExercise: session.exercises[idx - 1],
  238. });
  239. }
  240. },
  241. goToExercise: (targetIndex) => {
  242. const { session } = get();
  243. if (!session) return;
  244. if (targetIndex >= 0 && targetIndex < session.exercises.length) {
  245. workoutSessionLocal.update(session.id, { currentExerciseIndex: targetIndex });
  246. set({
  247. currentExerciseIndex: targetIndex,
  248. currentExercise: session.exercises[targetIndex],
  249. });
  250. }
  251. },
  252. getExercisesCompleted: () => {
  253. const { session } = get();
  254. if (!session) return 0;
  255. // only count exercises with at least one set
  256. return session.exercises
  257. .filter((exercise) => exercise.sets.length > 0)
  258. .filter((exercise) => exercise.sets.every((set) => set.completed)).length;
  259. },
  260. getTotalExercises: () => {
  261. const { session } = get();
  262. if (!session) return 0;
  263. return session.exercises.length;
  264. },
  265. getTotalVolume: () => {
  266. const { session } = get();
  267. if (!session) return 0;
  268. let totalVolume = 0;
  269. session.exercises.forEach((exercise) => {
  270. exercise.sets.forEach((set) => {
  271. // Vérifier si le set est complété et contient REPS et WEIGHT
  272. if (set.completed && set.types.includes("REPS") && set.types.includes("WEIGHT") && set.valuesInt) {
  273. const repsIndex = set.types.indexOf("REPS");
  274. const weightIndex = set.types.indexOf("WEIGHT");
  275. const reps = set.valuesInt[repsIndex] || 0;
  276. const weight = set.valuesInt[weightIndex] || 0;
  277. // Convertir les livres en kg si nécessaire
  278. const weightInKg =
  279. set.units && set.units[weightIndex] === "lbs"
  280. ? weight * 0.453592 // 1 lb = 0.453592 kg
  281. : weight;
  282. totalVolume += reps * weightInKg;
  283. }
  284. });
  285. });
  286. return Math.round(totalVolume);
  287. },
  288. getTotalVolumeInUnit: (unit: WeightUnit) => {
  289. const { session } = get();
  290. if (!session) return 0;
  291. let totalVolume = 0;
  292. session.exercises.forEach((exercise) => {
  293. exercise.sets.forEach((set) => {
  294. // Vérifier si le set est complété et contient REPS et WEIGHT
  295. if (set.completed && set.types.includes("REPS") && set.types.includes("WEIGHT") && set.valuesInt) {
  296. const repsIndex = set.types.indexOf("REPS");
  297. const weightIndex = set.types.indexOf("WEIGHT");
  298. const reps = set.valuesInt[repsIndex] || 0;
  299. const weight = set.valuesInt[weightIndex] || 0;
  300. // Déterminer l'unité de poids originale de la série
  301. const originalUnit: WeightUnit = set.units && set.units[weightIndex] === "lbs" ? "lbs" : "kg";
  302. // Convertir vers l'unité demandée
  303. const convertedWeight = convertWeight(weight, originalUnit, unit);
  304. totalVolume += reps * convertedWeight;
  305. }
  306. });
  307. });
  308. return Math.round(totalVolume * 10) / 10; // Arrondir à 1 décimale
  309. },
  310. formatElapsedTime: () => {
  311. const { elapsedTime } = get();
  312. const hours = Math.floor(elapsedTime / 3600);
  313. const minutes = Math.floor((elapsedTime % 3600) / 60);
  314. const secs = elapsedTime % 60;
  315. if (hours > 0) {
  316. return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
  317. }
  318. return `${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
  319. },
  320. loadSessionFromLocal: () => {
  321. const currentId = workoutSessionLocal.getCurrent();
  322. console.log("currentId:", currentId);
  323. if (currentId) {
  324. const session = workoutSessionLocal.getById(currentId);
  325. if (session && session.status === "active") {
  326. set({
  327. session,
  328. isWorkoutActive: true,
  329. currentExerciseIndex: session.currentExerciseIndex ?? 0,
  330. currentExercise: session.exercises[session.currentExerciseIndex ?? 0],
  331. elapsedTime: 0,
  332. isTimerRunning: false,
  333. });
  334. }
  335. }
  336. },
  337. }));