Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions mobile/api/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
export interface WorkoutContext {
getCurrentMicrocycle: (session: Session) => Promise<Microcycle | null>
getActivePlanSummary: (session: Session) => Promise<ActivePlanSummary | null>
getWorkout: (session: Session) => Promise<MicrocycleWorkout | null>
getWorkout: (session: Session) => Promise<(MicrocycleWorkout & { progressionMode: ProgressionMode }) | null>
getWorkoutStats: (session: Session) => Promise<WorkoutStats | null>
startWorkout: (session: Session) => Promise<void>
confirmMesocycle: (session: Session) => Promise<void>
Expand Down Expand Up @@ -91,7 +91,7 @@
session: Session,
workoutId: string,
workingExerciseId: string,
exerciseAssesment: ExerciseAssesment
exerciseAssesment: ExerciseAssesment | null
) => Promise<void>
undoExercise: (session: Session, workoutId: string, workingExerciseId: string) => Promise<void>
exerciseSetStateChanged: (
Expand All @@ -107,10 +107,16 @@
workingExerciseId: string,
weight: number
) => Promise<void>
exerciseChangeReps: (
session: Session,
workoutId: string,
workingExerciseId: string,
reps: number
) => Promise<void>
}

export interface MesoPlannerContext {
proposeSplit: (session: Session, volumePerMuscleGroup: VolumePerMuscleGroup, trainingDays: number) => Promise<any>

Check warning on line 119 in mobile/api/contract.ts

View workflow job for this annotation

GitHub Actions / typecheck-and-lint

Unexpected any. Specify a different type
proposeVolume: (
session: Session,
muscleGroupPreference: MuscleGroupPreference,
Expand Down
7 changes: 6 additions & 1 deletion mobile/api/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const workout = trpcInstance.router({
)
}),
exerciseFinished: trpcProcedureAuthProcedure
.input(z.object({ workoutId: z.string(), workingExerciseId: z.string(), exerciseAssesment }))
.input(z.object({ workoutId: z.string(), workingExerciseId: z.string(), exerciseAssesment: exerciseAssesment.nullable() }))
.mutation(async ({ ctx, input }) => {
return await ctx.workout.exerciseFinished(
ctx.session,
Expand Down Expand Up @@ -157,6 +157,11 @@ const workout = trpcInstance.router({
.mutation(async ({ ctx, input }) => {
return await ctx.workout.exerciseChangeWeight(ctx.session, input.workoutId, input.workingExerciseId, input.weight)
}),
exerciseChangeReps: trpcProcedureAuthProcedure
.input(z.object({ workoutId: z.string(), workingExerciseId: z.string(), reps: z.number() }))
.mutation(async ({ ctx, input }) => {
return await ctx.workout.exerciseChangeReps(ctx.session, input.workoutId, input.workingExerciseId, input.reps)
}),
changeMicrocycle: trpcProcedureAuthProcedure
.input(z.object({ template: microcycleWorkoutsTemplateWithExercisesSchema, progressionMode: z.nativeEnum(ProgressionMode) }))
.mutation(async ({ ctx, input }) => {
Expand Down
34 changes: 24 additions & 10 deletions mobile/db/mesocycle.dao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const mapWorkoutExerciseToDTO = (
if (row.assesment === ExerciseAssesmentScore.Hard && row.hardAssesmentTag) {
return { assesment: ExerciseAssesmentScore.Hard, assesmentTag: row.hardAssesmentTag } as const
}
if (row.assesment === null) return null
throw new Error('Exercise is missing assesment')
}

Expand Down Expand Up @@ -519,16 +520,18 @@ export const updateMesocycle = async (events: MesocycleEvent[]) => {
.where(eq(schema.workoutExercise.id, event.payload.exerciseId))
})
.with({ type: 'ExerciseFinished' }, async event => {
const assessmentData = match(event.payload.exerciseAssesment)
.with({ assesment: ExerciseAssesmentScore.Hard }, assessment => ({
assesment: assessment.assesment as string,
hardAssesmentTag: assessment.assesmentTag as string,
}))
.with({ assesment: ExerciseAssesmentScore.Ideal }, assessment => ({
assesment: assessment.assesment as string,
hardAssesmentTag: null,
}))
.exhaustive()
const assessmentData = event.payload.exerciseAssesment
? match(event.payload.exerciseAssesment)
.with({ assesment: ExerciseAssesmentScore.Hard }, assessment => ({
assesment: assessment.assesment as string,
hardAssesmentTag: assessment.assesmentTag as string,
}))
.with({ assesment: ExerciseAssesmentScore.Ideal }, assessment => ({
assesment: assessment.assesment as string,
hardAssesmentTag: null,
}))
.exhaustive()
: { assesment: null, hardAssesmentTag: null }

await db
.update(schema.workoutExercise)
Expand Down Expand Up @@ -602,6 +605,17 @@ export const updateMesocycle = async (events: MesocycleEvent[]) => {
.set({ testingWeight: event.payload.weight })
.where(eq(schema.workoutExercise.id, event.payload.workoutExerciseId))
})
.with({ type: 'ExerciseRepsChanged' }, async event => {
await db
.update(schema.workoutExerciseSet)
.set({ reps: event.payload.reps })
.where(
and(
eq(schema.workoutExerciseSet.workoutExerciseId, event.payload.workoutExerciseId),
eq(schema.workoutExerciseSet.state, WorkingSetState.pending)
)
)
})
.with({ type: 'ExerciseRepsChangedDueToWeightChange' }, async event => {
await db
.update(schema.workoutExerciseSet)
Expand Down
6 changes: 3 additions & 3 deletions mobile/db/user.dao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,12 +309,12 @@ export const getPastTrainingResults = async () => {

const workoutExercises = exercisesByWorkout.get(workout.id) ?? []
const finishedExercises = workoutExercises.filter(e => e.state === WorkoutExerciseState.finished)
const assessedExercises = finishedExercises.filter(e => e.assesment !== null)

const exercisesFeelingAvg =
finishedExercises.reduce((acc, exercise) => {
if (!exercise.assesment) throw new Error('Illegal State')
assessedExercises.reduce((acc, exercise) => {
return acc + assesmentTable[exercise.assesment as ExerciseAssesmentScore]
}, 0) / finishedExercises.length
}, 0) / assessedExercises.length

const successRate =
finishedExercises.reduce((acc, exercise) => {
Expand Down
133 changes: 97 additions & 36 deletions mobile/domain/aggregate/mesocycle.aggregate-root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export class MesocycleAggregateRoot {
return this.mesocycleDTO.isConfirmed
}

finishExercise(exerciseId: string, exerciseAssesment: ExerciseAssesment) {
finishExercise(exerciseId: string, exerciseAssesment: ExerciseAssesment | null) {
const activeWorkout = this.getActiveWorkout()
const exercise = activeWorkout.exercises.find(exercise => exercise.id === exerciseId)
if (!exercise) {
Expand Down Expand Up @@ -803,6 +803,21 @@ export class MesocycleAggregateRoot {
targetReps: originalExercise.targetReps,
}

if (this.mesocycleDTO.progressionMode === ProgressionMode.Custom) {
return {
...replacingExerciseBase,
state: WorkoutExerciseState.pending,
progressionType: ProgressionType.CustomUserProvided,
sets: Array.from({ length: originalExercise.targetSets }).map((_, index) => ({
id: v4(),
state: WorkingSetState.pending,
weight: null,
reps: null,
orderIndex: index,
})),
}
}

const historicalResult = replacingExercise.historicalResult
if (historicalResult === null) {
return {
Expand Down Expand Up @@ -934,34 +949,36 @@ export class MesocycleAggregateRoot {
})

if (exercise.state === WorkoutExerciseState.pending) {
if (!historicalResults) {
throw new Error('Historical result not found')
}
const newReps = calculateRepsFromLoadedExercise(
{
loadingSet: {
weight: historicalResults.loadedWeight,
reps: historicalResults.loadedReps,
if (this.mesocycleDTO.progressionMode !== ProgressionMode.Custom) {
if (!historicalResults) {
throw new Error('Historical result not found')
}
const newReps = calculateRepsFromLoadedExercise(
{
loadingSet: {
weight: historicalResults.loadedWeight,
reps: historicalResults.loadedReps,
},
targetWeight: snappedWeight,
},
targetWeight: snappedWeight,
},
this.getCurrentRpe()
)
this.getCurrentRpe()
)

if (newReps === null) {
throw new Error("Couldn't resolve new rep count.")
}
if (newReps === null) {
throw new Error("Couldn't resolve new rep count.")
}

if (newReps !== exercise.sets[0].reps) {
this.apply({
type: 'ExerciseRepsChangedDueToWeightChange',
payload: {
workoutExerciseId,
newReps,
workoutId: activeWorkout.id,
microcycleId: activeWorkout.microcycleId,
},
})
if (newReps !== exercise.sets[0].reps) {
this.apply({
type: 'ExerciseRepsChangedDueToWeightChange',
payload: {
workoutExerciseId,
newReps,
workoutId: activeWorkout.id,
microcycleId: activeWorkout.microcycleId,
},
})
}
}

this.apply({
Expand Down Expand Up @@ -1028,6 +1045,27 @@ export class MesocycleAggregateRoot {
})
}

changeReps(workoutExerciseId: string, reps: number) {
const activeWorkout = this.getActiveWorkout()
const exercise = activeWorkout.exercises.find(exercise => exercise.id === workoutExerciseId)
if (!exercise) {
throw new Error('Exercise not found in the workout')
}

if (exercise.state !== WorkoutExerciseState.pending) {
throw new Error('Cant change reps of non-pending exercise')
}

this.apply({
type: 'ExerciseUpdated',
payload: { workoutExerciseId, workoutId: activeWorkout.id, microcycleId: activeWorkout.microcycleId },
})
this.apply({
type: 'ExerciseRepsChanged',
payload: { workoutExerciseId, reps, workoutId: activeWorkout.id, microcycleId: activeWorkout.microcycleId },
})
}

setStateHasChanged(workoutExerciseId: string, setId: string, state: WorkingSetState) {
const activeWorkout = this.getActiveWorkout()

Expand Down Expand Up @@ -1463,16 +1501,18 @@ export class MesocycleAggregateRoot {
...workout,
exercises: workout.exercises.map(exercise => {
if (exercise.id === event.payload.exerciseId) {
const assessmentData = match(event.payload.exerciseAssesment)
.with({ assesment: ExerciseAssesmentScore.Hard }, assessment => ({
assesment: assessment.assesment,
hardAssesmentTag: assessment.assesmentTag,
}))
.with({ assesment: ExerciseAssesmentScore.Ideal }, assessment => ({
assesment: assessment.assesment,
hardAssesmentTag: null,
}))
.exhaustive()
const assessmentData = event.payload.exerciseAssesment
? match(event.payload.exerciseAssesment)
.with({ assesment: ExerciseAssesmentScore.Hard }, assessment => ({
assesment: assessment.assesment,
hardAssesmentTag: assessment.assesmentTag,
}))
.with({ assesment: ExerciseAssesmentScore.Ideal }, assessment => ({
assesment: assessment.assesment,
hardAssesmentTag: null,
}))
.exhaustive()
: { assesment: null, hardAssesmentTag: null }

return {
...exercise,
Expand Down Expand Up @@ -1521,6 +1561,27 @@ export class MesocycleAggregateRoot {
} as MicrocycleWorkout
})
})
.with({ type: 'ExerciseRepsChanged' }, event => {
updateMicrocycleWorkout(event.payload.microcycleId, event.payload.workoutId, workout => {
return {
...workout,
exercises: workout.exercises.map(exercise => {
if (exercise.id === event.payload.workoutExerciseId) {
return {
...exercise,
sets: exercise.sets.map(set => {
if (set.state === WorkingSetState.pending) {
return { ...set, reps: event.payload.reps }
}
return set
}),
}
}
return exercise
}),
} as MicrocycleWorkout
})
})
.with({ type: 'ExerciseRepsChangedDueToWeightChange' }, event => {
updateMicrocycleWorkout(event.payload.microcycleId, event.payload.workoutId, workout => {
return {
Expand Down
13 changes: 12 additions & 1 deletion mobile/domain/aggregate/mesocycle.events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export interface ExerciseFinished {
workoutId: string
exerciseId: string
when: string
exerciseAssesment: ExerciseAssesment
exerciseAssesment: ExerciseAssesment | null
}
}

Expand Down Expand Up @@ -150,6 +150,16 @@ export interface ExerciseWeightChangedCalibration {
}
}

export interface ExerciseRepsChanged {
type: 'ExerciseRepsChanged'
payload: {
workoutExerciseId: string
reps: number
workoutId: string
microcycleId: string
}
}

export interface ExerciseRepsChangedDueToWeightChange {
type: 'ExerciseRepsChangedDueToWeightChange'
payload: {
Expand Down Expand Up @@ -237,6 +247,7 @@ export type MesocycleEvent =
| ExerciseWeightChangedTesting
| ExerciseWeightChangedPending
| ExerciseWeightChangedCalibration
| ExerciseRepsChanged
| ExerciseRepsChangedDueToWeightChange
| WorkoutStarted
| ExerciseUpdated
Expand Down
1 change: 1 addition & 0 deletions mobile/domain/progression/get-progression-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const getProgressionType = (
}

if (
lastPastExercise.exerciseAssesment &&
lastPastExercise.exerciseAssesment.assesment === ExerciseAssesmentScore.Hard &&
lastPastExercise.exerciseAssesment.assesmentTag === HardAssesmentTag.TooHeavy
) {
Expand Down
2 changes: 1 addition & 1 deletion mobile/domain/working-exercise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export type ExerciseAssesment = z.infer<typeof exerciseAssesment>
export const finishedWorkingExerciseSchema = workingExerciseBaseSchema.extend({
state: z.literal(WorkoutExerciseState.finished),
sets: z.array(workingSetSchema),
exerciseAssesment,
exerciseAssesment: exerciseAssesment.nullable(),
})
export type FinishedWorkingExercise = z.infer<typeof finishedWorkingExerciseSchema>

Expand Down
16 changes: 15 additions & 1 deletion mobile/local-context/workout-context.local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const createLocalWorkoutContext = (): WorkoutContext => {
const mesocycle = new MesocycleAggregateRoot(mesocycleDto, getUserCoefficient(session))

if (mesocycle.hasActiveWorkout()) {
return mesocycle.getActiveWorkout()
return { ...mesocycle.getActiveWorkout(), progressionMode: mesocycleDto.progressionMode }
}
return null
}
Expand Down Expand Up @@ -234,6 +234,19 @@ export const createLocalWorkoutContext = (): WorkoutContext => {
await mesocycleDao.updateMesocycle(mesocycle.events)
}

const exerciseChangeReps: WorkoutContext['exerciseChangeReps'] = async (
session,
workoutId,
workoutExerciseId,
reps
) => {
const mesocycleId = await mesocycleDao.getMesocycleIdByWorkoutId(workoutId)
const mesocycleDto = await mesocycleDao.getMesocycleById(mesocycleId)
const mesocycle = new MesocycleAggregateRoot(mesocycleDto, getUserCoefficient(session))
mesocycle.changeReps(workoutExerciseId, reps)
await mesocycleDao.updateMesocycle(mesocycle.events)
}

const exerciseFinished: WorkoutContext['exerciseFinished'] = async (
session,
workoutId,
Expand Down Expand Up @@ -376,6 +389,7 @@ export const createLocalWorkoutContext = (): WorkoutContext => {
replaceExercise,
exerciseSetStateChanged,
exerciseChangeWeight,
exerciseChangeReps,
exerciseFinished,
undoExercise,
exerciseLoaded,
Expand Down
Loading
Loading