diff --git a/apps/api/src/modules/training/application/use-cases/complex.use-cases.ts b/apps/api/src/modules/training/application/use-cases/complex.use-cases.ts index b6404e13..254d125a 100644 --- a/apps/api/src/modules/training/application/use-cases/complex.use-cases.ts +++ b/apps/api/src/modules/training/application/use-cases/complex.use-cases.ts @@ -107,7 +107,6 @@ export class ComplexUseCase implements IComplexUseCases { // 5. Create the complex const complex = new Complex(); complex.complexCategory = complexCategory; - complex.description = data.description || ''; // 6. Assign the creator user const user = await this.userUseCases.getOne(userId); @@ -124,7 +123,6 @@ export class ComplexUseCase implements IComplexUseCases { const exerciseComplex = new ExerciseComplex(); exerciseComplex.order = exerciseData.order; - exerciseComplex.reps = exerciseData.reps; exerciseComplex.exercise = exercise; exerciseComplex.complex = complex; @@ -160,10 +158,6 @@ export class ComplexUseCase implements IComplexUseCases { } // 4. Update the complex properties - if (data.description !== undefined) { - complexToUpdate.description = data.description; - } - if (data.complexCategory) { const complexCategory = await this.complexCategoryRepository.getOne(data.complexCategory, coachFilterConditions); if (!complexCategory) { @@ -194,7 +188,6 @@ export class ComplexUseCase implements IComplexUseCases { exerciseComplex.exercise = exercise; exerciseComplex.complex = complexToUpdate; exerciseComplex.order = exerciseData.order; - exerciseComplex.reps = exerciseData.reps; complexToUpdate.exercises.add(exerciseComplex); } diff --git a/apps/api/src/modules/training/application/use-cases/exercise.use-cases.ts b/apps/api/src/modules/training/application/use-cases/exercise.use-cases.ts index ad67aa4a..442cdf31 100644 --- a/apps/api/src/modules/training/application/use-cases/exercise.use-cases.ts +++ b/apps/api/src/modules/training/application/use-cases/exercise.use-cases.ts @@ -126,9 +126,6 @@ export class ExerciseUseCase implements IExerciseUseCases { //5. Create exercise const exercise = new Exercise(); exercise.name = data.name; - if (data.description) { - exercise.description = data.description; - } exercise.exerciseCategory = exerciseCategory; if (data.englishName) { exercise.englishName = data.englishName; @@ -177,9 +174,6 @@ export class ExerciseUseCase implements IExerciseUseCases { if (data.name) { exerciseToUpdate.name = data.name; } - if (data.description !== undefined) { - exerciseToUpdate.description = data.description; - } if (data.englishName !== undefined) { exerciseToUpdate.englishName = data.englishName; } diff --git a/apps/api/src/modules/training/application/use-cases/workout.use-cases.ts b/apps/api/src/modules/training/application/use-cases/workout.use-cases.ts index 3ad9a348..72b330c3 100644 --- a/apps/api/src/modules/training/application/use-cases/workout.use-cases.ts +++ b/apps/api/src/modules/training/application/use-cases/workout.use-cases.ts @@ -145,7 +145,6 @@ export class WorkoutUseCases implements IWorkoutUseCases { //5. Create workout const workoutToCreate = new Workout(); - workoutToCreate.title = workout.title; workoutToCreate.description = workout.description || ''; workoutToCreate.category = category; @@ -158,26 +157,23 @@ export class WorkoutUseCases implements IWorkoutUseCases { const workoutElement = new WorkoutElement(); workoutElement.type = element.type; workoutElement.order = element.order; - workoutElement.sets = element.sets; - workoutElement.reps = element.reps; - workoutElement.rest = element.rest; - workoutElement.duration = element.duration; - workoutElement.startWeight_percent = element.startWeight_percent; - workoutElement.endWeight_percent = element.endWeight_percent; + workoutElement.blocks = element.blocks; + workoutElement.tempo = element.tempo; + workoutElement.commentary = element.commentary; if (element.type === WORKOUT_ELEMENT_TYPES.EXERCISE) { - const exercise = await this.exerciseRepository.getOne(element.id, coachFilterConditions); + const exercise = await this.exerciseRepository.getOne(element.exerciseId, coachFilterConditions); if (!exercise) { throw new ExerciseNotFoundException( - `Exercise with ID ${element.id} not found or access denied` + `Exercise with ID ${element.exerciseId} not found or access denied` ); } workoutElement.exercise = exercise; } else { - const complex = await this.complexRepository.getOne(element.id, coachFilterConditions); + const complex = await this.complexRepository.getOne(element.complexId, coachFilterConditions); if (!complex) { throw new ComplexNotFoundException( - `Complex with ID ${element.id} not found or access denied` + `Complex with ID ${element.complexId} not found or access denied` ); } workoutElement.complex = complex; @@ -253,10 +249,6 @@ export class WorkoutUseCases implements IWorkoutUseCases { } //4. Update workout - if (workout.title) { - workoutToUpdate.title = workout.title; - } - if (workout.description !== undefined) { workoutToUpdate.description = workout.description; } @@ -288,27 +280,24 @@ export class WorkoutUseCases implements IWorkoutUseCases { const workoutElement = new WorkoutElement(); workoutElement.type = element.type; workoutElement.order = element.order; - workoutElement.sets = element.sets; - workoutElement.reps = element.reps; - workoutElement.rest = element.rest; - workoutElement.duration = element.duration; - workoutElement.startWeight_percent = element.startWeight_percent; - workoutElement.endWeight_percent = element.endWeight_percent; + workoutElement.blocks = element.blocks; + workoutElement.tempo = element.tempo; + workoutElement.commentary = element.commentary; workoutElement.workout = workoutToUpdate; if (element.type === WORKOUT_ELEMENT_TYPES.EXERCISE) { - const exercise = await this.exerciseRepository.getOne(element.id, coachFilterConditions); + const exercise = await this.exerciseRepository.getOne(element.exerciseId, coachFilterConditions); if (!exercise) { throw new ExerciseNotFoundException( - `Exercise with ID ${element.id} not found or access denied` + `Exercise with ID ${element.exerciseId} not found or access denied` ); } workoutElement.exercise = exercise; } else { - const complex = await this.complexRepository.getOne(element.id, coachFilterConditions); + const complex = await this.complexRepository.getOne(element.complexId, coachFilterConditions); if (!complex) { throw new ComplexNotFoundException( - `Complex with ID ${element.id} not found or access denied` + `Complex with ID ${element.complexId} not found or access denied` ); } workoutElement.complex = complex; diff --git a/apps/api/src/modules/training/domain/complex.entity.ts b/apps/api/src/modules/training/domain/complex.entity.ts index 99ad1179..292c5c6f 100644 --- a/apps/api/src/modules/training/domain/complex.entity.ts +++ b/apps/api/src/modules/training/domain/complex.entity.ts @@ -24,9 +24,6 @@ export class Complex { ) exercises = new Collection(this); - @Property({ nullable: true }) - description?: string; - @Property({ onCreate: () => new Date() }) createdAt: Date = new Date(); diff --git a/apps/api/src/modules/training/domain/exercise-complex.entity.ts b/apps/api/src/modules/training/domain/exercise-complex.entity.ts index 89d59a29..1af0fa5c 100644 --- a/apps/api/src/modules/training/domain/exercise-complex.entity.ts +++ b/apps/api/src/modules/training/domain/exercise-complex.entity.ts @@ -13,9 +13,6 @@ export class ExerciseComplex { @Property() order!: number; - @Property({ default: 1 }) - reps!: number; - @Property({ onCreate: () => new Date() }) createdAt: Date = new Date(); diff --git a/apps/api/src/modules/training/domain/exercise.entity.ts b/apps/api/src/modules/training/domain/exercise.entity.ts index 5dff0a5c..22b78a48 100644 --- a/apps/api/src/modules/training/domain/exercise.entity.ts +++ b/apps/api/src/modules/training/domain/exercise.entity.ts @@ -20,9 +20,6 @@ export class Exercise { @Property() name!: string; - @Property({ nullable: true }) - description?: string; - @Property({ nullable: true }) englishName?: string; diff --git a/apps/api/src/modules/training/domain/workout-element.entity.ts b/apps/api/src/modules/training/domain/workout-element.entity.ts index 434da880..5fcb53b1 100644 --- a/apps/api/src/modules/training/domain/workout-element.entity.ts +++ b/apps/api/src/modules/training/domain/workout-element.entity.ts @@ -9,6 +9,7 @@ import { import { Complex } from './complex.entity'; import { Exercise } from './exercise.entity'; import { Workout } from './workout.entity'; +import { BlockConfigDto } from '@dropit/schemas'; export const WORKOUT_ELEMENT_TYPES = { EXERCISE: 'exercise', @@ -42,29 +43,17 @@ export class WorkoutElement { @ManyToOne(() => Complex, { nullable: true, deleteRule: 'cascade' }) complex?: Complex; + @Property({ type: 'jsonb' }) + blocks: BlockConfigDto[] = []; + @Property() order!: number; - @Property({ default: 1 }) - sets!: number; - - @Property({ default: 1 }) - reps!: number; - - @Property({ nullable: true }) - rest?: number; - - @Property({ nullable: true }) - duration?: number; - - @Property({ nullable: true }) - description?: string; - @Property({ nullable: true }) - startWeight_percent?: number; + tempo?: string; @Property({ nullable: true }) - endWeight_percent?: number; + commentary?: string; @Property({ onCreate: () => new Date() }) createdAt: Date = new Date(); diff --git a/apps/api/src/modules/training/domain/workout.entity.ts b/apps/api/src/modules/training/domain/workout.entity.ts index c552fda5..1ac98b1f 100644 --- a/apps/api/src/modules/training/domain/workout.entity.ts +++ b/apps/api/src/modules/training/domain/workout.entity.ts @@ -16,9 +16,6 @@ export class Workout { @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string; - @Property() - title!: string; - @Property() description!: string; @@ -28,7 +25,6 @@ export class Workout { @ManyToOne(() => User, { nullable: true, deleteRule: 'cascade'}) createdBy!: User | null; - @OneToMany( () => WorkoutElement, (workoutElement) => workoutElement.workout diff --git a/apps/api/src/modules/training/interface/mappers/complex.mapper.ts b/apps/api/src/modules/training/interface/mappers/complex.mapper.ts index 7ea90c7d..bcd0f1a5 100644 --- a/apps/api/src/modules/training/interface/mappers/complex.mapper.ts +++ b/apps/api/src/modules/training/interface/mappers/complex.mapper.ts @@ -18,15 +18,12 @@ export const ComplexMapper = { id: exercise.exerciseCategory.id, name: exercise.exerciseCategory.name, }, - description: exercise.description, video: exercise.video?.id, englishName: exercise.englishName, shortName: exercise.shortName, order: exerciseComplex.order, - reps: exerciseComplex.reps, }; }), - description: complex.description, }; }, diff --git a/apps/api/src/modules/training/interface/mappers/exercise.mapper.ts b/apps/api/src/modules/training/interface/mappers/exercise.mapper.ts index 5d816a33..a6869a2b 100644 --- a/apps/api/src/modules/training/interface/mappers/exercise.mapper.ts +++ b/apps/api/src/modules/training/interface/mappers/exercise.mapper.ts @@ -11,7 +11,6 @@ export const ExerciseMapper = { name: exercise.exerciseCategory.name, }, video: exercise.video?.id, - description: exercise.description, englishName: exercise.englishName, shortName: exercise.shortName, }; diff --git a/apps/api/src/modules/training/interface/mappers/workout.mapper.ts b/apps/api/src/modules/training/interface/mappers/workout.mapper.ts index cb45f7d8..1622d35d 100644 --- a/apps/api/src/modules/training/interface/mappers/workout.mapper.ts +++ b/apps/api/src/modules/training/interface/mappers/workout.mapper.ts @@ -20,10 +20,9 @@ export const WorkoutMapper = { const baseElement = { id: element.id, order: element.order, - reps: element.reps, - sets: element.sets, - rest: element.rest, - startWeight_percent: element.startWeight_percent, + tempo: element.tempo, + commentary: element.commentary, + blocks: element.blocks, }; if (isExerciseElement(element)) { @@ -33,7 +32,6 @@ export const WorkoutMapper = { exercise: { id: element.exercise.id, name: element.exercise.name, - description: element.exercise.description, exerciseCategory: { id: element.exercise.exerciseCategory.id, name: element.exercise.exerciseCategory.name, @@ -51,7 +49,6 @@ export const WorkoutMapper = { type: 'complex' as const, complex: { id: element.complex.id, - description: element.complex.description, complexCategory: { id: element.complex.complexCategory.id, name: element.complex.complexCategory.name, @@ -59,7 +56,6 @@ export const WorkoutMapper = { exercises: element.complex.exercises.getItems().map((ex: ExerciseComplex) => ({ id: ex.exercise.id, name: ex.exercise.name, - description: ex.exercise.description, exerciseCategory: { id: ex.exercise.exerciseCategory.id, name: ex.exercise.exerciseCategory.name, @@ -68,7 +64,6 @@ export const WorkoutMapper = { englishName: ex.exercise.englishName, shortName: ex.exercise.shortName, order: ex.order, - reps: ex.reps, })), }, }; @@ -80,7 +75,6 @@ export const WorkoutMapper = { return { id: workout.id, - title: workout.title, workoutCategory: workout.category.name, description: workout.description, elements, diff --git a/apps/api/src/seeders/complex.seeder.ts b/apps/api/src/seeders/complex.seeder.ts index ee02c634..55ff2881 100644 --- a/apps/api/src/seeders/complex.seeder.ts +++ b/apps/api/src/seeders/complex.seeder.ts @@ -16,7 +16,7 @@ export async function seedComplexes( description: "Exercices focalisés sur la technique de l'arraché", }, { - name: 'Épaulé', + name: 'Épaulé-Jeté', description: "Exercices focalisés sur la technique de l'épaulé-jeté", }, { @@ -35,124 +35,76 @@ export async function seedComplexes( console.log('Complex category created:', categoryToCreate); } - const ARRACHE_CATEGORY_INDEX = 0; - const EPAULE_CATEGORY_INDEX = 1; - const RENFORCEMENT_CATEGORY_INDEX = 2; - + // Complexes basés sur les vrais entraînements const complexesToCreate = [ { - category: complexCategories[ARRACHE_CATEGORY_INDEX].name, - description: "Focus sur la technique de l'arraché", + category: 'Arraché', + exercises: [ + { name: 'Passage', order: 0 }, + { name: 'Chute', order: 1 }, + { name: 'Flexion d\'Arraché', order: 2 } + ] + }, + { + category: 'Arraché', exercises: [ - { - name: 'Arraché Debout', - reps: 3, - }, - { - name: 'Tirage Nuque', - reps: 5, - }, - { - name: 'Squat Clavicule', - reps: 2, - }, - ], + { name: 'Arraché Flexion', order: 0 }, + { name: 'Flexion d\'Arraché', order: 1 } + ] }, { - category: complexCategories[EPAULE_CATEGORY_INDEX].name, - description: "EMOM", + category: 'Épaulé-Jeté', exercises: [ - { - name: 'Épaulé Debout', - reps: 3, - }, - { - name: 'Jeté Fente', - reps: 2, - }, - { - name: 'Squat Clavicule', - reps: 3, - }, - { - name: 'Développé Militaire', - reps: 8, - }, - ], + { name: 'Passage Epaulé', order: 0 }, + { name: 'Squat Devant', order: 1 } + ] }, { - category: complexCategories[RENFORCEMENT_CATEGORY_INDEX].name, - description: 'On le fait en TABATA', + category: 'Épaulé-Jeté', exercises: [ - { - name: 'Squat Nuque', - reps: 6, - }, - { - name: 'Développé Militaire', - reps: 8, - }, - { - name: 'Soulevé de Terre', - reps: 4, - }, - ], + { name: 'Epaulé Flexion', order: 0 }, + { name: 'Jeté Fente', order: 1 } + ] }, { - category: complexCategories[ARRACHE_CATEGORY_INDEX].name, - description: "Focus sur la technique de l'arraché, EMOM", + category: 'Épaulé-Jeté', exercises: [ - { - name: 'Arraché Debout', - reps: 2, - }, - { - name: 'Tirage Nuque', - reps: 4, - }, - { - name: 'Squat Nuque', - reps: 5, - }, - ], + { name: 'Epaulé Debout', order: 0 }, + { name: 'Squat (drop)', order: 1 }, + { name: 'Epaulé Flexion', order: 2 }, + { name: 'Jeté Fente', order: 3 } + ] }, { - category: complexCategories[EPAULE_CATEGORY_INDEX].name, - description: "On se concentre sur la technique", + category: 'Arraché', exercises: [ - { - name: 'Épaulé Debout', - reps: 2, - }, - { - name: 'Squat Clavicule', - reps: 3, - }, - { - name: 'Développé Militaire', - reps: 5, - }, - ], + { name: 'Tirage Lourd d\'Arraché', order: 0 }, + { name: 'Arraché Flexion', order: 1 } + ] }, + { + category: 'Épaulé-Jeté', + exercises: [ + { name: 'Epaulé Flexion', order: 0 }, + { name: 'Squat Nuque', order: 1 }, + { name: 'Jeté Fente', order: 2 } + ] + } ]; const complexesCreated: Complex[] = []; for (const complexData of complexesToCreate) { const complex = new Complex(); - complex.description = complexData.description; complex.complexCategory = complexCategoriesMap[complexData.category]; complex.createdBy = null; await em.persistAndFlush(complex); - for (let i = 0; i < complexData.exercises.length; i++) { - const exerciseData = complexData.exercises[i]; - + for (const exerciseData of complexData.exercises) { const exerciseComplex = new ExerciseComplex(); exerciseComplex.complex = complex; exerciseComplex.exercise = exercisesMap[exerciseData.name]; - exerciseComplex.order = i; - exerciseComplex.reps = exerciseData.reps; + exerciseComplex.order = exerciseData.order; await em.persistAndFlush(exerciseComplex); } diff --git a/apps/api/src/seeders/exercise.seeder.ts b/apps/api/src/seeders/exercise.seeder.ts index a308aded..6ebd7153 100644 --- a/apps/api/src/seeders/exercise.seeder.ts +++ b/apps/api/src/seeders/exercise.seeder.ts @@ -50,84 +50,42 @@ export async function seedExercises( const exercisesMap: Record = {}; const exercises = [ - { - name: 'Squat Clavicule', - category: 'Technique', - englishName: 'Front Squat', - shortName: 'Squat Clav', - }, - { - name: 'Épaulé Debout', - category: 'Technique', - englishName: 'Power Clean', - shortName: 'PC', - }, - { - name: 'Arraché Debout', - category: 'Technique', - englishName: 'Power Snatch', - shortName: 'PS', - }, - { - name: 'Jeté Fente', - category: 'Technique', - englishName: 'Split Jerk', - shortName: 'SJ', - }, - { - name: 'Arraché', - category: 'Technique', - englishName: 'snatch', - shortName: 'SN', - }, - { - name: 'Squat Nuque', - category: 'Technique', - englishName: 'Back Squat', - shortName: 'BS', - }, - { - name: 'Tirage Nuque', - category: 'Technique', - englishName: 'Snatch Pull', - shortName: 'SP', - }, - { - name: 'Développé Militaire', - category: 'Renforcement', - englishName: 'Military Press', - shortName: 'MP', - }, - { - name: 'Soulevé de Terre', - category: 'Renforcement', - englishName: 'Deadlift', - shortName: 'DL', - }, - { - name: 'Tirage Menton', - category: 'Renforcement', - englishName: 'Upright Row', - shortName: 'UR', - }, - { - name: 'Développé Couché', - category: 'Renforcement', - englishName: 'Bench Press', - shortName: 'BP', - }, - { - name: 'Épaulé-Jeté', - category: 'Technique', - englishName: 'cleanAndJerk', - shortName: 'C&J', - }, - { - name: 'Tirage Planche', - category: 'Renforcement', - englishName: 'Bent Over Row', - shortName: 'BOR', - }, + // Arraché (Snatch) family + { name: 'Arraché Flexion', category: 'Technique', englishName: 'Snatch Pull', shortName: 'Arr Flex' }, + { name: 'Flexion d\'Arraché', category: 'Technique', englishName: 'Snatch Pull Hang', shortName: 'Flex Arr' }, + { name: 'Arraché', category: 'Technique', englishName: 'Snatch', shortName: 'Arr' }, + { name: 'Passage', category: 'Technique', englishName: 'Muscle Snatch', shortName: 'Pass' }, + { name: 'Chute', category: 'Technique', englishName: 'Drop Snatch', shortName: 'Chute' }, + { name: 'Arraché Debout', category: 'Technique', englishName: 'Power Snatch', shortName: 'Arr Deb' }, + + // Epaulé (Clean) family + { name: 'Epaulé Flexion', category: 'Technique', englishName: 'Clean Pull', shortName: 'Ep Flex' }, + { name: 'Epaulé Debout', category: 'Technique', englishName: 'Power Clean', shortName: 'Ep Deb' }, + { name: 'Epaulé', category: 'Technique', englishName: 'Clean', shortName: 'Ep' }, + { name: 'Passage Epaulé', category: 'Technique', englishName: 'Muscle Clean', shortName: 'Pass Ep' }, + { name: 'Épaulé-Jeté', category: 'Technique', englishName: 'Clean & Jerk', shortName: 'Ep-Jeté' }, + + // Jeté (Jerk) family + { name: 'Jeté Fente', category: 'Technique', englishName: 'Split Jerk', shortName: 'Jeté Fente' }, + { name: 'Jeté Nuque', category: 'Technique', englishName: 'Jerk Behind Neck', shortName: 'Jeté Nuque' }, + + // Squat family + { name: 'Squat Nuque', category: 'Technique', englishName: 'Back Squat', shortName: 'Sq Nuque' }, + { name: 'Squat Devant', category: 'Technique', englishName: 'Front Squat', shortName: 'Sq Devant' }, + { name: 'Squat (drop)', category: 'Technique', englishName: 'Drop Squat', shortName: 'Sq drop' }, + { name: 'Squat Clavicule', category: 'Technique', englishName: 'Front Squat', shortName: 'Sq Clav' }, + + // Tirages + { name: 'Tirage Lourd d\'Arraché', category: 'Technique', englishName: 'Snatch High Pull', shortName: 'TLA' }, + { name: 'Tirage Lourd d\'Epaulé', category: 'Technique', englishName: 'Clean High Pull', shortName: 'TLE' }, + { name: 'Tirage Nuque', category: 'Technique', englishName: 'Snatch Pull', shortName: 'Tir Nuque' }, + { name: 'Tirage Planche', category: 'Renforcement', englishName: 'Bent Over Row', shortName: 'Tir Plan' }, + { name: 'Tirage Menton', category: 'Renforcement', englishName: 'Upright Row', shortName: 'Tir Ment' }, + + // Développés et renforcement + { name: 'Développé Militaire', category: 'Renforcement', englishName: 'Military Press', shortName: 'Dév Mil' }, + { name: 'Développé Couché', category: 'Renforcement', englishName: 'Bench Press', shortName: 'Dév Cou' }, + { name: 'Soulevé de Terre', category: 'Renforcement', englishName: 'Deadlift', shortName: 'SDT' }, ]; for (const exercise of exercises) { diff --git a/apps/api/src/seeders/training-session.seeder.ts b/apps/api/src/seeders/training-session.seeder.ts index 779ac3ba..3db418b7 100644 --- a/apps/api/src/seeders/training-session.seeder.ts +++ b/apps/api/src/seeders/training-session.seeder.ts @@ -17,7 +17,7 @@ export async function seedTrainingSessions(em: EntityManager): Promise { } const firstWorkout = workouts[0]; - console.log('Using workout:', firstWorkout.title); + console.log('Using workout:', firstWorkout.id); // 2. Récupérer l'organisation const organizations = await em.find(Organization, {}, { limit: 1 }); diff --git a/apps/api/src/seeders/workout.seeder.ts b/apps/api/src/seeders/workout.seeder.ts index 144e315b..f755d1a4 100644 --- a/apps/api/src/seeders/workout.seeder.ts +++ b/apps/api/src/seeders/workout.seeder.ts @@ -45,175 +45,444 @@ export async function seedWorkouts(em: EntityManager): Promise { console.log('Workout category created:', categoryToCreate); } - // Creation of workouts - const workoutsToCreate = [ - { - title: 'Entraînement Technique Lourd', - category: 'Saison', - description: 'Focus sur la technique avec charges lourdes', - elements: [ - { - type: WORKOUT_ELEMENT_TYPES.COMPLEX, - complexIndex: 0, - order: 0, - sets: 4, - reps: 1, - rest: 180, - startWeight_percent: 80, - endWeight_percent: 92, - description: 'Monter progressivement la charge à chaque série', - }, - { - type: WORKOUT_ELEMENT_TYPES.EXERCISE, - id: 'Squat Nuque', - order: 1, - sets: 5, - reps: 3, - rest: 180, - startWeight_percent: 85, - description: '2 secondes de descente, pause en bas et on remonte fort', - }, - { - type: WORKOUT_ELEMENT_TYPES.COMPLEX, - complexIndex: 1, // Deuxième complex créé (Complex Épaulé-Jeté) - order: 2, - sets: 3, - reps: 1, - rest: 180, - startWeight_percent: 75, - description: 'Focus sur la vitesse de passage sous la barre', - }, - { - type: WORKOUT_ELEMENT_TYPES.EXERCISE, - id: 'Développé Militaire', - order: 3, - sets: 3, - reps: 8, - rest: 120, - startWeight_percent: 65, - description: 'Maintenir le tronc gainé, coudes légèrement en avant', - }, - ], + // Basé sur la séance du Lundi 17 Novembre + const workout1 = new Workout(); + workout1.description = 'Séance technique avec variations d\'intensité'; + workout1.category = workoutCategoriesMap.Saison; + workout1.createdBy = null; + + await em.persistAndFlush(workout1); + + // Element 1: Complex Passage + Chute + Flexion d'Arraché @ BAV + const element1 = new WorkoutElement(); + element1.type = WORKOUT_ELEMENT_TYPES.COMPLEX; + element1.complex = complexes[0]; // Passage + Chute + Flexion d'Arraché + element1.order = 0; + element1.commentary = '@ BAV (Barre à Vide)'; + element1.blocks = [ + { + order: 1, + numberOfSets: 2, + intensity: { + percentageOfMax: 30, + referenceExerciseId: exercisesMap.Passage.id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap.Passage.id, reps: 2, order: 1 }, + { exerciseId: exercisesMap.Chute.id, reps: 2, order: 2 }, + { exerciseId: exercisesMap['Flexion d\'Arraché'].id, reps: 2, order: 3 } + ] + } + ]; + element1.workout = workout1; + await em.persistAndFlush(element1); + + // Element 2: Complex Arraché Flexion + Flexion d'Arraché avec progression + const element2 = new WorkoutElement(); + element2.type = WORKOUT_ELEMENT_TYPES.COMPLEX; + element2.complex = complexes[1]; // Arraché Flexion + Flexion d'Arraché + element2.order = 1; + element2.commentary = 'Rest 1min30'; + element2.blocks = [ + { + order: 1, + numberOfSets: 2, + rest: 90, + intensity: { + percentageOfMax: 60, + referenceExerciseId: exercisesMap['Arraché Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Arraché Flexion'].id, reps: 2, order: 1 }, + { exerciseId: exercisesMap['Flexion d\'Arraché'].id, reps: 2, order: 2 } + ] }, { - title: 'Décharge Technique', - category: 'Décharge', - description: 'Maintien technique à intensité modérée', - elements: [ - { - type: WORKOUT_ELEMENT_TYPES.COMPLEX, - complexIndex: 3, - order: 0, - sets: 3, - reps: 2, - rest: 120, - startWeight_percent: 65, - }, - { - type: WORKOUT_ELEMENT_TYPES.EXERCISE, - id: 'Squat Clavicule', - order: 1, - sets: 3, - reps: 5, - rest: 120, - startWeight_percent: 70, - }, - { - type: WORKOUT_ELEMENT_TYPES.COMPLEX, - complexIndex: 4, - order: 2, - sets: 3, - reps: 2, - rest: 120, - startWeight_percent: 65, - }, - { - type: WORKOUT_ELEMENT_TYPES.EXERCISE, - id: 'Tirage Planche', - order: 3, - sets: 3, - reps: 10, - rest: 90, - startWeight_percent: 60, - }, - ], + order: 2, + numberOfSets: 2, + rest: 90, + intensity: { + percentageOfMax: 70, + referenceExerciseId: exercisesMap['Arraché Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Arraché Flexion'].id, reps: 1, order: 1 }, + { exerciseId: exercisesMap['Flexion d\'Arraché'].id, reps: 1, order: 2 } + ] + } + ]; + element2.workout = workout1; + await em.persistAndFlush(element2); + + // Element 3: Arraché Flexion simple + const element3 = new WorkoutElement(); + element3.type = WORKOUT_ELEMENT_TYPES.EXERCISE; + element3.exercise = exercisesMap['Arraché Flexion']; + element3.order = 2; + element3.commentary = 'Monté en gamme simple'; + element3.blocks = [ + { + order: 1, + numberOfSets: 3, + rest: 120, + intensity: { + percentageOfMax: 85, + referenceExerciseId: exercisesMap['Arraché Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Arraché Flexion'].id, reps: 1, order: 1 } + ] + } + ]; + element3.workout = workout1; + await em.persistAndFlush(element3); + + // Element 4: Squat Nuque + const element4 = new WorkoutElement(); + element4.type = WORKOUT_ELEMENT_TYPES.EXERCISE; + element4.exercise = exercisesMap['Squat Nuque']; + element4.order = 3; + element4.blocks = [ + { + order: 1, + numberOfSets: 5, + rest: 180, + intensity: { + percentageOfMax: 76, + referenceExerciseId: exercisesMap['Squat Nuque'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Squat Nuque'].id, reps: 4, order: 1 } + ] + } + ]; + element4.workout = workout1; + await em.persistAndFlush(element4); + + console.log('Workout 1 created:', workout1); + + // Workout 2 - Basé sur Mercredi 19 Novembre + const workout2 = new Workout(); + workout2.description = 'Focus montée en charge progressive'; + workout2.category = workoutCategoriesMap.Saison; + workout2.createdBy = null; + + await em.persistAndFlush(workout2); + + // Element 1: Epaulé Flexion + Jeté Fente avec 5 blocs de progression + const workout2Element1 = new WorkoutElement(); + workout2Element1.type = WORKOUT_ELEMENT_TYPES.COMPLEX; + workout2Element1.complex = complexes[3]; // Epaulé Flexion + Jeté Fente + workout2Element1.order = 0; + workout2Element1.blocks = [ + { + order: 1, + numberOfSets: 1, + rest: 180, + intensity: { + percentageOfMax: 75, + referenceExerciseId: exercisesMap['Epaulé Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Epaulé Flexion'].id, reps: 1, order: 1 }, + { exerciseId: exercisesMap['Jeté Fente'].id, reps: 1, order: 2 } + ] }, { - title: 'Préparation Physique', - category: 'Fond', - description: 'Développement des qualités physiques', - elements: [ - { - type: WORKOUT_ELEMENT_TYPES.COMPLEX, - complexIndex: 2, - order: 0, - sets: 4, - reps: 1, - rest: 60, - startWeight_percent: 60, - }, - { - type: WORKOUT_ELEMENT_TYPES.EXERCISE, - id: 'Développé Couché', - order: 1, - sets: 4, - reps: 8, - rest: 90, - startWeight_percent: 70, - }, - { - type: WORKOUT_ELEMENT_TYPES.EXERCISE, - id: 'Tirage Planche', - order: 2, - sets: 4, - reps: 10, - rest: 90, - startWeight_percent: 65, - }, - ], + order: 2, + numberOfSets: 1, + rest: 180, + intensity: { + percentageOfMax: 80, + referenceExerciseId: exercisesMap['Epaulé Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Epaulé Flexion'].id, reps: 1, order: 1 }, + { exerciseId: exercisesMap['Jeté Fente'].id, reps: 1, order: 2 } + ] }, + { + order: 3, + numberOfSets: 1, + rest: 180, + intensity: { + percentageOfMax: 85, + referenceExerciseId: exercisesMap['Epaulé Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Epaulé Flexion'].id, reps: 1, order: 1 }, + { exerciseId: exercisesMap['Jeté Fente'].id, reps: 1, order: 2 } + ] + }, + { + order: 4, + numberOfSets: 1, + rest: 180, + intensity: { + percentageOfMax: 90, + referenceExerciseId: exercisesMap['Epaulé Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Epaulé Flexion'].id, reps: 1, order: 1 }, + { exerciseId: exercisesMap['Jeté Fente'].id, reps: 1, order: 2 } + ] + }, + { + order: 5, + numberOfSets: 1, + rest: 180, + intensity: { + percentageOfMax: 93, + referenceExerciseId: exercisesMap['Epaulé Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Epaulé Flexion'].id, reps: 1, order: 1 }, + { exerciseId: exercisesMap['Jeté Fente'].id, reps: 1, order: 2 } + ] + } ]; + workout2Element1.workout = workout2; + await em.persistAndFlush(workout2Element1); - for (const workoutData of workoutsToCreate) { - const workout = new Workout(); - workout.title = workoutData.title; - workout.description = workoutData.description; - workout.category = workoutCategoriesMap[workoutData.category]; - workout.createdBy = null; - - await em.persistAndFlush(workout); - - for (const element of workoutData.elements) { - const workoutElement = new WorkoutElement(); - workoutElement.type = element.type; - workoutElement.order = element.order; - workoutElement.workout = workout; - workoutElement.sets = element.sets; - workoutElement.reps = element.reps; - workoutElement.startWeight_percent = element.startWeight_percent; - if ('description' in element) { - workoutElement.description = element.description; - } - - if (element.type === WORKOUT_ELEMENT_TYPES.EXERCISE) { - workoutElement.exercise = exercisesMap[element.id]; - if (!workoutElement.exercise) { - console.warn(`Exercise ${element.id} not found, skipping element`); - continue; - } - } else { - // Utiliser l'index du complex - const complex = complexes[element.complexIndex]; - if (!complex) { - console.warn(`Complex at index ${element.complexIndex} not found, skipping element`); - continue; - } - workoutElement.complex = complex; - } - - await em.persistAndFlush(workoutElement); + // Element 2: Squat Nuque simple + const workout2Element2 = new WorkoutElement(); + workout2Element2.type = WORKOUT_ELEMENT_TYPES.EXERCISE; + workout2Element2.exercise = exercisesMap['Squat Nuque']; + workout2Element2.order = 1; + workout2Element2.commentary = 'Recherche de vitesse au redressement'; + workout2Element2.blocks = [ + { + order: 1, + numberOfSets: 4, + rest: 180, + intensity: { + percentageOfMax: 73, + referenceExerciseId: exercisesMap['Squat Nuque'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Squat Nuque'].id, reps: 5, order: 1 } + ] } + ]; + workout2Element2.workout = workout2; + await em.persistAndFlush(workout2Element2); - console.log('Workout created:', workout.title); - } + console.log('Workout 2 created:', workout2); + + // Workout 3 - Simple pour la décharge + const workout3 = new Workout(); + workout3.description = 'Séance technique avec charges légères'; + workout3.category = workoutCategoriesMap.Décharge; + workout3.createdBy = null; + + await em.persistAndFlush(workout3); + + // Element 1: Complex Passage Epaulé + Squat Devant + const workout3Element1 = new WorkoutElement(); + workout3Element1.type = WORKOUT_ELEMENT_TYPES.COMPLEX; + workout3Element1.complex = complexes[2]; // Passage Epaulé + Squat Devant + workout3Element1.order = 0; + workout3Element1.blocks = [ + { + order: 1, + numberOfSets: 3, + rest: 120, + intensity: { + percentageOfMax: 50, + referenceExerciseId: exercisesMap['Passage Epaulé'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Passage Epaulé'].id, reps: 2, order: 1 }, + { exerciseId: exercisesMap['Squat Devant'].id, reps: 2, order: 2 } + ] + } + ]; + workout3Element1.workout = workout3; + await em.persistAndFlush(workout3Element1); + + console.log('Workout 3 created:', workout3); + + // Workout 4 - Exemple 2 : Arraché Flexion avec montée progressive + const workout4 = new Workout(); + workout4.description = 'Montée progressive en intensité - Arraché Flexion'; + workout4.category = workoutCategoriesMap.Saison; + workout4.createdBy = null; + + await em.persistAndFlush(workout4); + + // Element 1: Arraché Flexion avec montée progressive (78%, 82%, 85%, 90%) + const workout4Element1 = new WorkoutElement(); + workout4Element1.type = WORKOUT_ELEMENT_TYPES.EXERCISE; + workout4Element1.exercise = exercisesMap['Arraché Flexion']; + workout4Element1.order = 0; + workout4Element1.commentary = 'Doublé Jusqu\'a 75%'; + workout4Element1.blocks = [ + { + order: 1, + numberOfSets: 1, + rest: 120, + intensity: { + percentageOfMax: 78, + referenceExerciseId: exercisesMap['Arraché Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Arraché Flexion'].id, reps: 1, order: 1 } + ] + }, + { + order: 2, + numberOfSets: 1, + rest: 120, + intensity: { + percentageOfMax: 82, + referenceExerciseId: exercisesMap['Arraché Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Arraché Flexion'].id, reps: 1, order: 1 } + ] + }, + { + order: 3, + numberOfSets: 1, + rest: 120, + intensity: { + percentageOfMax: 85, + referenceExerciseId: exercisesMap['Arraché Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Arraché Flexion'].id, reps: 1, order: 1 } + ] + }, + { + order: 4, + numberOfSets: 1, + rest: 120, + intensity: { + percentageOfMax: 90, + referenceExerciseId: exercisesMap['Arraché Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Arraché Flexion'].id, reps: 1, order: 1 } + ] + } + ]; + workout4Element1.workout = workout4; + await em.persistAndFlush(workout4Element1); + + console.log('Workout 4 created:', workout4); + + // Workout 5 - Exemple 4 : Tirage Lourd d'Arraché + Arraché Flexion + const workout5 = new Workout(); + workout5.description = 'Variations de volume - Tirage Lourd d\'Arraché'; + workout5.category = workoutCategoriesMap.Saison; + workout5.createdBy = null; + + await em.persistAndFlush(workout5); + + // Element 1: Complex Tirage Lourd d'Arraché + Arraché Flexion + const workout5Element1 = new WorkoutElement(); + workout5Element1.type = WORKOUT_ELEMENT_TYPES.COMPLEX; + workout5Element1.complex = complexes[5]; // Tirage Lourd d'Arraché + Arraché Flexion + workout5Element1.order = 0; + workout5Element1.blocks = [ + { + order: 1, + numberOfSets: 4, + rest: 120, + intensity: { + percentageOfMax: 60, + referenceExerciseId: exercisesMap['Arraché Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Tirage Lourd d\'Arraché'].id, reps: 3, order: 1 }, + { exerciseId: exercisesMap['Arraché Flexion'].id, reps: 1, order: 2 } + ] + }, + { + order: 2, + numberOfSets: 2, + rest: 120, + intensity: { + percentageOfMax: 70, + referenceExerciseId: exercisesMap['Arraché Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Tirage Lourd d\'Arraché'].id, reps: 2, order: 1 }, + { exerciseId: exercisesMap['Arraché Flexion'].id, reps: 1, order: 2 } + ] + }, + { + order: 3, + numberOfSets: 1, + rest: 120, + intensity: { + percentageOfMax: 80, + referenceExerciseId: exercisesMap['Arraché Flexion'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Tirage Lourd d\'Arraché'].id, reps: 1, order: 1 }, + { exerciseId: exercisesMap['Arraché Flexion'].id, reps: 1, order: 2 } + ] + } + ]; + workout5Element1.workout = workout5; + await em.persistAndFlush(workout5Element1); + + console.log('Workout 5 created:', workout5); + + // Workout 6 - Exemple 6 : Complex large avec multiples exercices + const workout6 = new Workout(); + workout6.description = 'Complex complet Epaulé Debout + Squat (drop) + Epaulé Flexion + Jeté Fente'; + workout6.category = workoutCategoriesMap.Saison; + workout6.createdBy = null; + + await em.persistAndFlush(workout6); + + // Element 1: Complex Epaulé Debout + Squat (drop) + Epaulé Flexion + Jeté Fente + const workout6Element1 = new WorkoutElement(); + workout6Element1.type = WORKOUT_ELEMENT_TYPES.COMPLEX; + workout6Element1.complex = complexes[4]; // Epaulé Debout + Squat (drop) + Epaulé Flexion + Jeté Fente + workout6Element1.order = 0; + workout6Element1.blocks = [ + { + order: 1, + numberOfSets: 4, + rest: 240, + intensity: { + percentageOfMax: 80, + referenceExerciseId: exercisesMap['Epaulé Debout'].id, + type: 'percentage' as const + }, + exercises: [ + { exerciseId: exercisesMap['Epaulé Debout'].id, reps: 1, order: 1 }, + { exerciseId: exercisesMap['Squat (drop)'].id, reps: 1, order: 2 }, + { exerciseId: exercisesMap['Epaulé Flexion'].id, reps: 1, order: 3 }, + { exerciseId: exercisesMap['Jeté Fente'].id, reps: 1, order: 4 } + ] + } + ]; + workout6Element1.workout = workout6; + await em.persistAndFlush(workout6Element1); + + console.log('Workout 6 created:', workout6); + console.log('All workouts seeded successfully!'); } diff --git a/apps/api/src/test/complex.integration.ts b/apps/api/src/test/complex.integration.ts index f68f3c3b..6b247631 100644 --- a/apps/api/src/test/complex.integration.ts +++ b/apps/api/src/test/complex.integration.ts @@ -72,19 +72,16 @@ export async function runComplexTests(orm: MikroORM): Promise { try { exercise1 = await exerciseUseCase.create({ name: 'Squat', - description: 'Basic squat exercise', exerciseCategory: exerciseCategory.id, }, testData.organization.id, testData.adminUser.id); exercise2 = await exerciseUseCase.create({ name: 'Deadlift', - description: 'Basic deadlift exercise', exerciseCategory: exerciseCategory.id, }, testData.organization.id, testData.adminUser.id); exercise3 = await exerciseUseCase.create({ name: 'Bench Press', - description: 'Basic bench press exercise', exerciseCategory: exerciseCategory.id, }, testData.organization.id, testData.adminUser.id); } catch (error: unknown) { @@ -105,20 +102,16 @@ export async function runComplexTests(orm: MikroORM): Promise { { exerciseId: exercise1.id, order: 1, - reps: 10, }, { exerciseId: exercise2.id, order: 2, - reps: 10, }, { exerciseId: exercise3.id, order: 3, - reps: 10, }, ], - description: 'Pour monter en gamme tranquillement', }, testData.organization.id, testData.adminUser.id); } catch (error: unknown) { throw new Error(`Failed to create complex: ${(error as Error).message}`); @@ -136,22 +129,19 @@ export async function runComplexTests(orm: MikroORM): Promise { try { exercise4 = await exerciseUseCase.create({ name: 'Push-up', - description: 'Basic push-up', exerciseCategory: exerciseCategory.id, }, testData.organization.id, testData.adminUser.id); exercise5 = await exerciseUseCase.create({ name: 'Pull-up', - description: 'Basic pull-up', exerciseCategory: exerciseCategory.id, }, testData.organization.id, testData.adminUser.id); complex2 = await complexUseCase.create({ - description: 'Complexe push-pull', complexCategory: complexCategory.id, exercises: [ - { exerciseId: exercise4.id, order: 1, reps: 10 }, - { exerciseId: exercise5.id, order: 2, reps: 8 }, + { exerciseId: exercise4.id, order: 1 }, + { exerciseId: exercise5.id, order: 2 }, ], }, testData.organization.id, testData.adminUser.id); } catch (error: unknown) { @@ -188,7 +178,16 @@ export async function runComplexTests(orm: MikroORM): Promise { updatedComplex = await complexUseCase.update( complex1.id, { - description: 'Description modifiée', + exercises: [ + { + exerciseId: exercise1.id, + order: 1, + }, + { + exerciseId: exercise2.id, + order: 2, + }, + ], }, testData.organization.id, testData.adminUser.id @@ -196,7 +195,7 @@ export async function runComplexTests(orm: MikroORM): Promise { } catch (error: unknown) { throw new Error(`Failed to update complex: ${(error as Error).message}`); } - expect(updatedComplex.description).toBe('Description modifiée'); + expect(updatedComplex.exercises).toHaveLength(2); // Test 7: Supprimer un complex via use case console.log('🧪 Testing complex deletion via use case...'); diff --git a/apps/api/src/test/exercise.integration.ts b/apps/api/src/test/exercise.integration.ts index b44e493e..23ca10ab 100644 --- a/apps/api/src/test/exercise.integration.ts +++ b/apps/api/src/test/exercise.integration.ts @@ -53,7 +53,6 @@ export async function runExerciseTests(orm: MikroORM): Promise { try { exercise1 = await exerciseUseCase.create({ name: 'Squat', - description: 'Basic squat exercise', exerciseCategory: exerciseCategory.id, }, testData.organization.id, testData.adminUser.id); } catch (error: unknown) { @@ -68,7 +67,6 @@ export async function runExerciseTests(orm: MikroORM): Promise { try { exercise2 = await exerciseUseCase.create({ name: 'Push-up', - description: 'Basic push-up', exerciseCategory: exerciseCategory.id, }, testData.organization.id, testData.adminUser.id); } catch (error: unknown) { @@ -81,7 +79,6 @@ export async function runExerciseTests(orm: MikroORM): Promise { try { exercise3 = await exerciseUseCase.create({ name: 'Squat Clavicule', - description: 'Squat avec barre en position clavicule', exerciseCategory: exerciseCategory.id, englishName: 'Front Squat', shortName: 'FS', @@ -123,7 +120,6 @@ export async function runExerciseTests(orm: MikroORM): Promise { exercise1.id, { name: 'Squat Modifié', - description: 'Description modifiée', }, testData.organization.id, testData.adminUser.id @@ -132,7 +128,6 @@ export async function runExerciseTests(orm: MikroORM): Promise { throw new Error(`Failed to update exercise: ${(error as Error).message}`); } expect(updatedExercise.name).toBe('Squat Modifié'); - expect(updatedExercise.description).toBe('Description modifiée'); // Test 5: Rechercher des exercices via use case console.log('🧪 Testing exercise search via use case...'); diff --git a/apps/api/src/test/validation.integration.spec.ts b/apps/api/src/test/validation.integration.spec.ts index 5f6b5997..392cff5e 100644 --- a/apps/api/src/test/validation.integration.spec.ts +++ b/apps/api/src/test/validation.integration.spec.ts @@ -14,28 +14,9 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { describe('createWorkoutSchema - used by POST /api/workout', () => { - it('should reject when title is missing (required field)', () => { - const invalidPayload = { - workoutCategory: 'strength', - elements: [], - // title manquant - devrait échouer - }; - - const result = createWorkoutSchema.safeParse(invalidPayload); - - expect(result.success).toBe(false); - if (!result.success) { - const titleError = result.error.issues.find( - issue => issue.path.includes('title') - ); - expect(titleError).toBeDefined(); - expect(titleError?.code).toBe('invalid_type'); - } - }); it('should reject when workoutCategory is missing (required field)', () => { const invalidPayload = { - title: 'Test Workout', elements: [], // workoutCategory manquant }; @@ -51,17 +32,27 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { } }); - it('should reject when sets is negative (min validation)', () => { + it('should reject when numberOfSets is negative (min validation)', () => { const invalidPayload = { - title: 'Test Workout', workoutCategory: 'strength', elements: [ { type: 'exercise', - id: 'some-id', + exerciseId: 'some-id', order: 0, - sets: -1, // Invalide ! Doit être >= 1 - reps: 10, + blocks: [ + { + order: 1, + numberOfSets: -1, // Invalide ! Doit être >= 1 + exercises: [ + { + exerciseId: 'some-id', + reps: 10, + order: 1, + }, + ], + }, + ], }, ], }; @@ -71,7 +62,7 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { expect(result.success).toBe(false); if (!result.success) { const setsError = result.error.issues.find( - issue => issue.path.includes('sets') + issue => issue.path.includes('numberOfSets') ); expect(setsError).toBeDefined(); expect(setsError?.code).toBe('too_small'); @@ -80,15 +71,25 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { it('should reject when reps is negative (min validation)', () => { const invalidPayload = { - title: 'Test Workout', workoutCategory: 'strength', elements: [ { type: 'exercise', - id: 'some-id', + exerciseId: 'some-id', order: 0, - sets: 3, - reps: -5, // Invalide ! Doit être >= 1 + blocks: [ + { + order: 1, + numberOfSets: 3, + exercises: [ + { + exerciseId: 'some-id', + reps: -5, // Invalide ! Doit être >= 1 + order: 1, + }, + ], + }, + ], }, ], }; @@ -107,15 +108,25 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { it('should reject when order is negative (min validation)', () => { const invalidPayload = { - title: 'Test Workout', workoutCategory: 'strength', elements: [ { type: 'exercise', - id: 'some-id', + exerciseId: 'some-id', order: -1, // Invalide ! Doit être >= 0 - sets: 3, - reps: 10, + blocks: [ + { + order: 1, + numberOfSets: 3, + exercises: [ + { + exerciseId: 'some-id', + reps: 10, + order: 1, + }, + ], + }, + ], }, ], }; @@ -134,15 +145,25 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { it('should reject when element type is invalid (discriminated union)', () => { const invalidPayload = { - title: 'Test Workout', workoutCategory: 'strength', elements: [ { type: 'invalid-type', // Doit être 'exercise' ou 'complex' - id: 'some-id', + exerciseId: 'some-id', order: 0, - sets: 3, - reps: 10, + blocks: [ + { + order: 1, + numberOfSets: 3, + exercises: [ + { + exerciseId: 'some-id', + reps: 10, + order: 1, + }, + ], + }, + ], }, ], }; @@ -156,17 +177,27 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { } }); - it('should reject when element id is missing (required field)', () => { + it('should reject when element exerciseId is missing (required field)', () => { const invalidPayload = { - title: 'Test Workout', workoutCategory: 'strength', elements: [ { type: 'exercise', - // id manquant + // exerciseId manquant order: 0, - sets: 3, - reps: 10, + blocks: [ + { + order: 1, + numberOfSets: 3, + exercises: [ + { + exerciseId: 'some-id', + reps: 10, + order: 1, + }, + ], + }, + ], }, ], }; @@ -176,7 +207,7 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { expect(result.success).toBe(false); if (!result.success) { const idError = result.error.issues.find( - issue => issue.path.includes('id') + issue => issue.path.includes('exerciseId') ); expect(idError).toBeDefined(); } @@ -184,18 +215,31 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { it('should accept valid workout data', () => { const validPayload = { - title: 'Test Workout', workoutCategory: 'strength', description: 'A valid workout', elements: [ { type: 'exercise', - id: 'some-exercise-id', + exerciseId: '550e8400-e29b-41d4-a716-446655440000', order: 0, - sets: 3, - reps: 10, - rest: 90, - startWeight_percent: 75, + blocks: [ + { + order: 1, + numberOfSets: 3, + rest: 90, + intensity: { + percentageOfMax: 75, + type: 'percentage', + }, + exercises: [ + { + exerciseId: '550e8400-e29b-41d4-a716-446655440000', + reps: 10, + order: 1, + }, + ], + }, + ], }, ], }; @@ -204,25 +248,42 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { expect(result.success).toBe(true); if (result.success) { - expect(result.data.title).toBe('Test Workout'); expect(result.data.elements.length).toBe(1); - expect(result.data.elements[0].sets).toBe(3); + expect(result.data.elements[0].blocks[0].numberOfSets).toBe(3); } }); it('should accept valid workout with complex element', () => { const validPayload = { - title: 'Test Workout', workoutCategory: 'strength', elements: [ { type: 'complex', - id: 'some-complex-id', + complexId: '550e8400-e29b-41d4-a716-446655440001', order: 0, - sets: 3, - reps: 1, - rest: 120, - startWeight_percent: 80, + blocks: [ + { + order: 1, + numberOfSets: 3, + rest: 120, + intensity: { + percentageOfMax: 80, + type: 'percentage', + }, + exercises: [ + { + exerciseId: '550e8400-e29b-41d4-a716-446655440000', + reps: 5, + order: 1, + }, + { + exerciseId: '550e8400-e29b-41d4-a716-446655440002', + reps: 5, + order: 2, + }, + ], + }, + ], }, ], }; @@ -237,7 +298,6 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { it('should accept optional fields as undefined', () => { const validPayload = { - title: 'Minimal Workout', workoutCategory: 'cardio', elements: [], // description est optionnel @@ -253,26 +313,37 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { it('should accept partial data (all fields optional)', () => { const validPayload = { - title: 'Updated Title', + description: 'Updated Description', }; const result = updateWorkoutSchema.safeParse(validPayload); expect(result.success).toBe(true); if (result.success) { - expect(result.data.title).toBe('Updated Title'); + expect(result.data.description).toBe('Updated Description'); } }); - it('should reject when sets is negative in elements', () => { + it('should reject when numberOfSets is negative in elements', () => { const invalidPayload = { elements: [ { type: 'exercise', - id: 'some-id', + exerciseId: 'some-id', order: 0, - sets: -1, // Invalide - reps: 10, + blocks: [ + { + order: 1, + numberOfSets: -1, // Invalide + exercises: [ + { + exerciseId: 'some-id', + reps: 10, + order: 1, + }, + ], + }, + ], }, ], }; @@ -282,7 +353,7 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { expect(result.success).toBe(false); if (!result.success) { const setsError = result.error.issues.find( - issue => issue.path.includes('sets') + issue => issue.path.includes('numberOfSets') ); expect(setsError).toBeDefined(); } @@ -302,10 +373,25 @@ describe('Zod Validation - Automatic validation with ts-rest', () => { elements: [ { type: 'exercise', - id: 'ex-1', + exerciseId: '550e8400-e29b-41d4-a716-446655440000', order: 0, - sets: 5, - reps: 5, + blocks: [ + { + order: 1, + numberOfSets: 5, + intensity: { + percentageOfMax: 80, + type: 'percentage', + }, + exercises: [ + { + exerciseId: '550e8400-e29b-41d4-a716-446655440000', + reps: 5, + order: 1, + }, + ], + }, + ], }, ], }; diff --git a/apps/api/src/test/workout.integration.ts b/apps/api/src/test/workout.integration.ts index cd2d14b5..04935ebf 100644 --- a/apps/api/src/test/workout.integration.ts +++ b/apps/api/src/test/workout.integration.ts @@ -89,13 +89,11 @@ export async function runWorkoutTests(orm: MikroORM): Promise { try { exercise1 = await exerciseUseCase.create({ name: 'Squat', - description: 'Basic squat exercise', exerciseCategory: exerciseCategory.id, }, testData.organization.id, testData.adminUser.id); exercise2 = await exerciseUseCase.create({ name: 'Deadlift', - description: 'Basic deadlift exercise', exerciseCategory: exerciseCategory.id, }, testData.organization.id, testData.adminUser.id); @@ -105,12 +103,10 @@ export async function runWorkoutTests(orm: MikroORM): Promise { { exerciseId: exercise1.id, order: 1, - reps: 10, }, { exerciseId: exercise2.id, order: 2, - reps: 10, }, ], }, testData.organization.id, testData.adminUser.id); @@ -127,27 +123,59 @@ export async function runWorkoutTests(orm: MikroORM): Promise { let workout1: Workout; try { workout1 = await workoutUseCase.createWorkout({ - title: 'Test Workout', workoutCategory: workoutCategory.id, description: 'Test workout description', elements: [ { type: WORKOUT_ELEMENT_TYPES.COMPLEX, - id: complex.id, + complexId: complex.id, order: 0, - reps: 1, - sets: 1, - rest: 120, - startWeight_percent: 75, + blocks: [ + { + order: 1, + numberOfSets: 1, + rest: 120, + intensity: { + percentageOfMax: 75, + type: 'percentage' as const, + }, + exercises: [ + { + exerciseId: exercise1.id, + reps: 10, + order: 1, + }, + { + exerciseId: exercise2.id, + reps: 10, + order: 2, + }, + ], + }, + ], }, { type: WORKOUT_ELEMENT_TYPES.EXERCISE, - id: exercise2.id, + exerciseId: exercise2.id, order: 1, - reps: 8, - sets: 3, - rest: 90, - startWeight_percent: 70, + blocks: [ + { + order: 1, + numberOfSets: 3, + rest: 90, + intensity: { + percentageOfMax: 70, + type: 'percentage' as const, + }, + exercises: [ + { + exerciseId: exercise2.id, + reps: 8, + order: 1, + }, + ], + }, + ], }, ], }, testData.organization.id, testData.adminUser.id); @@ -156,7 +184,6 @@ export async function runWorkoutTests(orm: MikroORM): Promise { } expect(workout1).toBeDefined(); expect(workout1.id).toBeDefined(); - expect(workout1.title).toBe('Test Workout'); expect(workout1.elements.length).toBe(2); // Vérification du complex @@ -166,32 +193,46 @@ export async function runWorkoutTests(orm: MikroORM): Promise { expect(complexElement.complex).toBeDefined(); expect(complexElement.complex?.exercises).toBeDefined(); expect(complexElement.complex?.exercises.length).toBeGreaterThan(0); + expect(complexElement.blocks).toBeDefined(); + expect(complexElement.blocks.length).toBe(1); + expect(complexElement.blocks[0].numberOfSets).toBe(1); // Vérification de l'exercice simple const exerciseElement = elements[1]; expect(exerciseElement.type).toBe('exercise'); expect(exerciseElement.exercise).toBeDefined(); - expect(exerciseElement.reps).toBe(8); - expect(exerciseElement.sets).toBe(3); - expect(exerciseElement.rest).toBe(90); - expect(exerciseElement.startWeight_percent).toBe(70); + expect(exerciseElement.blocks).toBeDefined(); + expect(exerciseElement.blocks.length).toBe(1); + expect(exerciseElement.blocks[0].exercises[0].reps).toBe(8); + expect(exerciseElement.blocks[0].numberOfSets).toBe(3); + expect(exerciseElement.blocks[0].rest).toBe(90); // Test 3: Créer un autre workout console.log('🧪 Testing second workout creation via use case...'); let workout2: Workout; try { workout2 = await workoutUseCase.createWorkout({ - title: 'Second Workout', workoutCategory: workoutCategory.id, description: 'Second workout description', elements: [ { type: WORKOUT_ELEMENT_TYPES.EXERCISE, - id: exercise1.id, + exerciseId: exercise1.id, order: 0, - reps: 5, - sets: 3, - rest: 60, + blocks: [ + { + order: 1, + numberOfSets: 3, + rest: 60, + exercises: [ + { + exerciseId: exercise1.id, + reps: 5, + order: 1, + }, + ], + }, + ], }, ], }, testData.organization.id, testData.adminUser.id); @@ -199,7 +240,6 @@ export async function runWorkoutTests(orm: MikroORM): Promise { throw new Error(`Failed to create workout2: ${(error as Error).message}`); } expect(workout2).toBeDefined(); - expect(workout2.title).toBe('Second Workout'); expect(workout2.elements.length).toBe(1); // Test 4: Récupérer tous les workouts via use case @@ -221,7 +261,6 @@ export async function runWorkoutTests(orm: MikroORM): Promise { throw new Error(`Failed to get single workout: ${(error as Error).message}`); } expect(singleWorkout.id).toBe(workout1.id); - expect(singleWorkout.title).toBe('Test Workout'); // Test 6: Mettre à jour un workout via use case console.log('🧪 Testing workout update via use case...'); @@ -230,7 +269,6 @@ export async function runWorkoutTests(orm: MikroORM): Promise { updatedWorkout = await workoutUseCase.updateWorkout( workout1.id, { - title: 'Test Workout Modifié', description: 'Description modifiée', }, testData.organization.id, @@ -239,7 +277,6 @@ export async function runWorkoutTests(orm: MikroORM): Promise { } catch (error: unknown) { throw new Error(`Failed to update workout: ${(error as Error).message}`); } - expect(updatedWorkout.title).toBe('Test Workout Modifié'); expect(updatedWorkout.description).toBe('Description modifiée'); // Test 7: Supprimer un workout via use case diff --git a/apps/mobile/src/components/TrainingDetailScreen.tsx b/apps/mobile/src/components/TrainingDetailScreen.tsx index cae6dbdc..c562ee3c 100644 --- a/apps/mobile/src/components/TrainingDetailScreen.tsx +++ b/apps/mobile/src/components/TrainingDetailScreen.tsx @@ -29,15 +29,23 @@ export default function TrainingDetailScreen({ const name = element.type === 'exercise' ? element.exercise.name : element.complex.exercises.map((e: { name: string }) => e.name).join(', '); - const sets = `${element.sets} sets`; - const reps = `${element.reps} reps` - const weight = element.startWeight_percent ? `${element.startWeight_percent}%` : '-'; - const recovery = element.rest ? `${element.rest}sec` : '90sec'; - const instructions = element.description || `Instructions pour ${name}.`; + + // Calculate totals from blocks + const totalSets = element.blocks.reduce((sum, block) => sum + block.numberOfSets, 0); + const firstBlock = element.blocks[0]; + const firstReps = firstBlock?.exercises[0]?.reps ?? 0; + const firstRest = firstBlock?.rest ?? 90; + const firstIntensity = firstBlock?.intensity?.percentageOfMax; + + const sets = `${totalSets} sets`; + const reps = `${firstReps} reps`; + const weight = firstIntensity ? `${firstIntensity}%` : '-'; + const recovery = `${firstRest}sec`; + const instructions = element.commentary || `Instructions pour ${name}.`; const videoUrl = element.type === 'exercise' ? element.exercise.video : undefined; - // Get default rest time in seconds (from element.rest or 90s default) - const defaultRestTime = element.rest || 90; + // Get default rest time in seconds (from first block rest or 90s default) + const defaultRestTime = firstRest; useEffect(() => { let interval: ReturnType; if (isTimerActive && timeLeft > 0) { diff --git a/apps/mobile/src/components/TrainingScreen.tsx b/apps/mobile/src/components/TrainingScreen.tsx index 65ace1f3..fe2c07ac 100644 --- a/apps/mobile/src/components/TrainingScreen.tsx +++ b/apps/mobile/src/components/TrainingScreen.tsx @@ -260,12 +260,19 @@ export default function TrainingScreen({ onBack }: TrainingScreenProps) { ? element.exercise.name : element.complex.exercises.map((e: { name: string }) => e.name).join(', '); + // Calculate from blocks + const totalSets = element.blocks.reduce((sum, block) => sum + block.numberOfSets, 0); + const firstBlock = element.blocks[0]; + const firstReps = firstBlock?.exercises[0]?.reps ?? 0; + const firstRest = firstBlock?.rest; + const firstIntensity = firstBlock?.intensity?.percentageOfMax; + return renderExerciseBlock(element, { id, name, - sets: `${element.sets} x ${element.reps}`, - weight: element.startWeight_percent ? `${element.startWeight_percent}%` : '-', - rest: element.rest ? `${element.rest}sec` : '-', + sets: `${totalSets} x ${firstReps}`, + weight: firstIntensity ? `${firstIntensity}%` : '-', + rest: firstRest ? `${firstRest}sec` : '-', }); })} diff --git a/apps/web/src/features/complex/complex-card.tsx b/apps/web/src/features/complex/complex-card.tsx index 8762641f..468e78e5 100644 --- a/apps/web/src/features/complex/complex-card.tsx +++ b/apps/web/src/features/complex/complex-card.tsx @@ -49,9 +49,6 @@ export function ComplexCard({ complex, onClick }: ComplexCardProps) { -
- {complex.description || 'Pas de description'} -
>({ resolver: zodResolver(formComplexSchema), defaultValues: { - description: '', complexCategory: '', exercises: [ { exerciseId: '', order: 0, - reps: 1, }, { exerciseId: '', order: 1, - reps: 1, }, ], }, @@ -191,7 +187,6 @@ export function ComplexCreationForm({ append({ exerciseId: '', order: fields.length, - reps: 1, }); }; @@ -231,20 +226,6 @@ export function ComplexCreationForm({ )} className="grid gap-4 py-4" > - ( - - Description - -