@dgpholdings/greatoak-shared 1.1.60 → 1.1.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/TApiRecord.d.ts +50 -32
- package/dist/types/TApiTemplateData.d.ts +1 -1
- package/dist/types/TApiUserData.d.ts +16 -16
- package/dist/types/commonTypes.d.ts +3 -3
- package/dist/utils/index.d.ts +1 -6
- package/dist/utils/index.js +2 -7
- package/dist/utils/record.utils.d.ts +33 -28
- package/dist/utils/record.utils.js +142 -41
- package/dist/utils/scoring.utils.d.ts +12 -36
- package/dist/utils/scoring.utils.js +243 -475
- package/dist/utils/workoutSummary.util.js +362 -294
- package/package.json +1 -1
|
@@ -6,342 +6,405 @@ const scoring_utils_1 = require("./scoring.utils");
|
|
|
6
6
|
* Calculate comprehensive workout summary
|
|
7
7
|
*/
|
|
8
8
|
const calculateWorkoutSummary = (exercises, userProfile) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
exercises.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
9
|
+
var _a, _b;
|
|
10
|
+
let totalScore = 0;
|
|
11
|
+
let totalCalories = 0;
|
|
12
|
+
let totalEpocCalories = 0;
|
|
13
|
+
let totalSets = 0;
|
|
14
|
+
const exerciseSummaries = [];
|
|
15
|
+
const muscleStressMap = new Map();
|
|
16
|
+
// Calculate workout duration
|
|
17
|
+
const workoutStart =
|
|
18
|
+
((_a = exercises[0]) === null || _a === void 0 ? void 0 : _a.startTime) ||
|
|
19
|
+
new Date();
|
|
20
|
+
const workoutEnd =
|
|
21
|
+
((_b = exercises[exercises.length - 1]) === null || _b === void 0
|
|
22
|
+
? void 0
|
|
23
|
+
: _b.endTime) || new Date();
|
|
24
|
+
const workoutDuration = Math.max(
|
|
25
|
+
1,
|
|
26
|
+
(workoutEnd.getTime() - workoutStart.getTime()) / (1000 * 60)
|
|
27
|
+
);
|
|
28
|
+
// Process each exercise
|
|
29
|
+
exercises.forEach((exerciseData) => {
|
|
30
|
+
const { exercise, records } = exerciseData;
|
|
31
|
+
const sets = [];
|
|
32
|
+
let exerciseScore = 0;
|
|
33
|
+
let exerciseCalories = 0;
|
|
34
|
+
let bestSetScore = 0;
|
|
35
|
+
let bestSetNumber = 1;
|
|
36
|
+
let bestSetReason = "highest score";
|
|
37
|
+
// Process each set/record
|
|
38
|
+
records.forEach((record, index) => {
|
|
39
|
+
if (!record.isDone) return;
|
|
40
|
+
const scoreBreakdown = (0, scoring_utils_1.computeScoreFromRecord)({
|
|
41
|
+
record,
|
|
42
|
+
exercise,
|
|
43
|
+
userProfile,
|
|
44
|
+
isTimeIntervalModeEnabled: !!(
|
|
45
|
+
record.workDurationSecs && record.restDurationSecs
|
|
46
|
+
),
|
|
47
|
+
});
|
|
48
|
+
const setNumber = index + 1;
|
|
49
|
+
const setDescription = generateSetDescription(
|
|
50
|
+
record,
|
|
51
|
+
exercise,
|
|
52
|
+
scoreBreakdown
|
|
53
|
+
);
|
|
54
|
+
sets.push({
|
|
55
|
+
setNumber,
|
|
56
|
+
record,
|
|
57
|
+
scoreBreakdown,
|
|
58
|
+
description: setDescription,
|
|
59
|
+
});
|
|
60
|
+
exerciseScore += scoreBreakdown.finalScore;
|
|
61
|
+
exerciseCalories += scoreBreakdown.caloriesBurned;
|
|
62
|
+
totalSets++;
|
|
63
|
+
// Track best set
|
|
64
|
+
if (scoreBreakdown.finalScore > bestSetScore) {
|
|
65
|
+
bestSetScore = scoreBreakdown.finalScore;
|
|
66
|
+
bestSetNumber = setNumber;
|
|
67
|
+
bestSetReason = getBestSetReason(record, exercise);
|
|
68
|
+
}
|
|
69
|
+
// Accumulate muscle stress
|
|
70
|
+
exercise.primaryMuscles.forEach((muscle) => {
|
|
71
|
+
const currentStress = muscleStressMap.get(muscle) || 0;
|
|
72
|
+
muscleStressMap.set(
|
|
73
|
+
muscle,
|
|
74
|
+
currentStress + scoreBreakdown.finalScore * 1.0
|
|
75
|
+
); // Primary muscles get full stress
|
|
76
|
+
});
|
|
77
|
+
exercise.secondaryMuscles.forEach((muscle) => {
|
|
78
|
+
const currentStress = muscleStressMap.get(muscle) || 0;
|
|
79
|
+
muscleStressMap.set(
|
|
80
|
+
muscle,
|
|
81
|
+
currentStress + scoreBreakdown.finalScore * 0.6
|
|
82
|
+
); // Secondary muscles get 60% stress
|
|
83
|
+
});
|
|
81
84
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
85
|
+
if (sets.length > 0) {
|
|
86
|
+
exerciseSummaries.push({
|
|
87
|
+
exercise,
|
|
88
|
+
sets,
|
|
89
|
+
totalScore: exerciseScore,
|
|
90
|
+
averageScore: exerciseScore / sets.length,
|
|
91
|
+
totalCalories: exerciseCalories,
|
|
92
|
+
bestSet: {
|
|
93
|
+
setNumber: bestSetNumber,
|
|
94
|
+
score: bestSetScore,
|
|
95
|
+
reason: bestSetReason,
|
|
96
|
+
},
|
|
97
|
+
muscleActivation: calculateMuscleActivation(exercise, sets),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
// Calculate totals
|
|
102
|
+
totalScore = exerciseSummaries.reduce((sum, ex) => sum + ex.totalScore, 0);
|
|
103
|
+
totalCalories = exerciseSummaries.reduce(
|
|
104
|
+
(sum, ex) => sum + ex.totalCalories,
|
|
105
|
+
0
|
|
106
|
+
);
|
|
107
|
+
totalEpocCalories = exerciseSummaries.reduce(
|
|
108
|
+
(sum, ex) =>
|
|
109
|
+
sum +
|
|
110
|
+
ex.sets.reduce(
|
|
111
|
+
(setSum, set) => setSum + set.scoreBreakdown.epocCalories,
|
|
112
|
+
0
|
|
113
|
+
),
|
|
114
|
+
0
|
|
115
|
+
);
|
|
116
|
+
// Calculate muscle recovery
|
|
117
|
+
const muscleRecovery = calculateMuscleRecovery(muscleStressMap, exercises);
|
|
118
|
+
// Determine overall fatigue level
|
|
119
|
+
const averageScore = totalSets > 0 ? totalScore / totalSets : 0;
|
|
120
|
+
const fatigueLevel = getFatigueLevel(
|
|
121
|
+
averageScore,
|
|
122
|
+
totalScore,
|
|
123
|
+
workoutDuration,
|
|
124
|
+
userProfile
|
|
125
|
+
);
|
|
126
|
+
const recommendedRestDays = getRecommendedRestDays(
|
|
127
|
+
fatigueLevel,
|
|
128
|
+
muscleRecovery
|
|
129
|
+
);
|
|
130
|
+
return {
|
|
131
|
+
totalScore: Math.round(totalScore),
|
|
132
|
+
averageScore: Math.round(averageScore),
|
|
133
|
+
totalCalories: Math.round(totalCalories),
|
|
134
|
+
totalEpocCalories: Math.round(totalEpocCalories),
|
|
135
|
+
workoutDuration: Math.round(workoutDuration),
|
|
136
|
+
exerciseSummaries,
|
|
137
|
+
muscleRecovery,
|
|
138
|
+
fatigueLevel,
|
|
139
|
+
recommendedRestDays,
|
|
140
|
+
};
|
|
104
141
|
};
|
|
105
142
|
exports.calculateWorkoutSummary = calculateWorkoutSummary;
|
|
106
143
|
/**
|
|
107
144
|
* Generate human-readable set description
|
|
108
145
|
*/
|
|
109
146
|
const generateSetDescription = (record, exercise, scoreBreakdown) => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
147
|
+
var _a;
|
|
148
|
+
const effort = record.rpe
|
|
149
|
+
? `RPE ${record.rpe}`
|
|
150
|
+
: "rir" in record && record.rir
|
|
151
|
+
? `${record.rir} RIR`
|
|
152
|
+
: "";
|
|
153
|
+
const calories = `${Math.round(scoreBreakdown.caloriesBurned)} cal`;
|
|
154
|
+
switch (record.type) {
|
|
155
|
+
case "weight-reps":
|
|
156
|
+
return `${record.kg}kg × ${record.reps} reps ${effort} • ${calories}`;
|
|
157
|
+
case "reps-only":
|
|
158
|
+
const auxWeight = record.auxWeightKg ? ` (+${record.auxWeightKg}kg)` : "";
|
|
159
|
+
return `${record.reps} reps${auxWeight} ${effort} • ${calories}`;
|
|
160
|
+
case "duration":
|
|
161
|
+
const auxWeightDur = record.auxWeightKg
|
|
162
|
+
? ` (+${record.auxWeightKg}kg)`
|
|
163
|
+
: "";
|
|
164
|
+
const minutes = Math.floor(record.durationMmSs / 60);
|
|
165
|
+
const seconds = record.durationMmSs % 60;
|
|
166
|
+
const timeStr =
|
|
167
|
+
minutes > 0
|
|
168
|
+
? `${minutes}:${seconds < 10 ? `0${seconds}` : `${seconds}`}`
|
|
169
|
+
: `${seconds}s`;
|
|
170
|
+
return `${timeStr}${auxWeightDur} ${effort} • ${calories}`;
|
|
171
|
+
case "cardio-machine":
|
|
172
|
+
const rpe = (_a = record.rpe) !== null && _a !== void 0 ? _a : 1;
|
|
173
|
+
const resistance = rpe > 0 ? ` R${rpe}` : "";
|
|
174
|
+
const minutes_cm = Math.floor(record.durationMmSs / 60);
|
|
175
|
+
const seconds_cm = record.durationMmSs % 60;
|
|
176
|
+
const timeStr_cm =
|
|
177
|
+
minutes_cm > 0
|
|
178
|
+
? `${minutes_cm}:${
|
|
179
|
+
seconds_cm < 10 ? `0${seconds_cm}` : `${seconds_cm}`
|
|
180
|
+
}`
|
|
181
|
+
: `${seconds_cm}s`;
|
|
182
|
+
return `${record.speed} speed${resistance} × ${timeStr_cm} ${effort} • ${calories}`;
|
|
183
|
+
case "cardio-free":
|
|
184
|
+
const minutes_cf = Math.floor(record.durationMmSs / 60);
|
|
185
|
+
const seconds_cf = record.durationMmSs % 60;
|
|
186
|
+
const timeStr_cf =
|
|
187
|
+
minutes_cf > 0
|
|
188
|
+
? `${minutes_cf}:${
|
|
189
|
+
seconds_cf < 10 ? `0${seconds_cf}` : `${seconds_cf}`
|
|
190
|
+
}`
|
|
191
|
+
: `${seconds_cf}s`;
|
|
192
|
+
let pace = "";
|
|
193
|
+
if ("avgPaceSecsPerKm" in record && record.avgPaceSecsPerKm) {
|
|
194
|
+
const paceMinutes = Math.floor(record.avgPaceSecsPerKm / 60);
|
|
195
|
+
const paceSeconds = Math.round(record.avgPaceSecsPerKm % 60);
|
|
196
|
+
const paceSecondsFormatted =
|
|
197
|
+
paceSeconds < 10 ? `0${paceSeconds}` : `${paceSeconds}`;
|
|
198
|
+
pace = ` (${paceMinutes}:${paceSecondsFormatted}/km)`;
|
|
199
|
+
}
|
|
200
|
+
return `${record.distance}km in ${timeStr_cf}${pace} ${effort} • ${calories}`;
|
|
201
|
+
default:
|
|
202
|
+
return `${effort} • ${calories}`;
|
|
203
|
+
}
|
|
159
204
|
};
|
|
160
205
|
/**
|
|
161
206
|
* Determine reason for best set
|
|
162
207
|
*/
|
|
163
208
|
const getBestSetReason = (record, exercise) => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
209
|
+
switch (record.type) {
|
|
210
|
+
case "weight-reps":
|
|
211
|
+
return "heaviest weight";
|
|
212
|
+
case "reps-only":
|
|
213
|
+
return "most reps";
|
|
214
|
+
case "duration":
|
|
215
|
+
return "longest hold";
|
|
216
|
+
case "cardio-machine":
|
|
217
|
+
return "highest intensity";
|
|
218
|
+
case "cardio-free":
|
|
219
|
+
return "fastest pace";
|
|
220
|
+
default:
|
|
221
|
+
return "highest effort";
|
|
222
|
+
}
|
|
178
223
|
};
|
|
179
224
|
/**
|
|
180
225
|
* Calculate muscle activation levels
|
|
181
226
|
*/
|
|
182
227
|
const calculateMuscleActivation = (exercise, sets) => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
228
|
+
if (sets.length === 0) return { primary: 0, secondary: 0 };
|
|
229
|
+
const avgScore =
|
|
230
|
+
sets.reduce((sum, set) => sum + set.scoreBreakdown.finalScore, 0) /
|
|
231
|
+
sets.length;
|
|
232
|
+
const maxPossibleScore = 100; // Reference point
|
|
233
|
+
// Activation based on exercise difficulty and average set score
|
|
234
|
+
const baseActivation = Math.min(100, (avgScore / maxPossibleScore) * 100);
|
|
235
|
+
const difficultyBonus = exercise.difficultyLevel * 2; // Up to 20% bonus
|
|
236
|
+
return {
|
|
237
|
+
primary: Math.min(100, Math.round(baseActivation + difficultyBonus)),
|
|
238
|
+
secondary: Math.min(
|
|
239
|
+
100,
|
|
240
|
+
Math.round((baseActivation + difficultyBonus) * 0.7)
|
|
241
|
+
), // 70% of primary
|
|
242
|
+
};
|
|
195
243
|
};
|
|
196
244
|
/**
|
|
197
245
|
* Calculate muscle recovery times
|
|
198
246
|
*/
|
|
199
247
|
const calculateMuscleRecovery = (muscleStressMap, exercises) => {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
involvedExercises.push(exercise.name);
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
recovery[muscle] = {
|
|
239
|
-
fatigueLevel: Math.round(fatigueLevel),
|
|
240
|
-
recoveryHours: Math.round(recoveryHours),
|
|
241
|
-
trainingStress: Math.round(totalStress),
|
|
242
|
-
exercises: involvedExercises,
|
|
243
|
-
};
|
|
248
|
+
const recovery = {};
|
|
249
|
+
muscleStressMap.forEach((totalStress, muscle) => {
|
|
250
|
+
// Base recovery time based on muscle group
|
|
251
|
+
const baseRecovery = getMuscleBaseRecovery(muscle);
|
|
252
|
+
// Stress level (0-100)
|
|
253
|
+
const fatigueLevel = Math.min(100, (totalStress / 200) * 100); // 200 = very high stress threshold
|
|
254
|
+
// Recovery time increases with fatigue level
|
|
255
|
+
let recoveryHours = baseRecovery;
|
|
256
|
+
if (fatigueLevel > 80) recoveryHours *= 1.5; // 50% longer for high fatigue
|
|
257
|
+
else if (fatigueLevel > 60)
|
|
258
|
+
recoveryHours *= 1.3; // 30% longer for moderate-high fatigue
|
|
259
|
+
else if (fatigueLevel > 40) recoveryHours *= 1.1; // 10% longer for moderate fatigue
|
|
260
|
+
// Find exercises that worked this muscle - using manual search instead of includes
|
|
261
|
+
const involvedExercises = [];
|
|
262
|
+
exercises.forEach(({ exercise }) => {
|
|
263
|
+
let found = false;
|
|
264
|
+
// Check primary muscles
|
|
265
|
+
for (let i = 0; i < exercise.primaryMuscles.length; i++) {
|
|
266
|
+
if (exercise.primaryMuscles[i] === muscle) {
|
|
267
|
+
found = true;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Check secondary muscles if not found in primary
|
|
272
|
+
if (!found) {
|
|
273
|
+
for (let i = 0; i < exercise.secondaryMuscles.length; i++) {
|
|
274
|
+
if (exercise.secondaryMuscles[i] === muscle) {
|
|
275
|
+
found = true;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (found) {
|
|
281
|
+
involvedExercises.push(exercise.name);
|
|
282
|
+
}
|
|
244
283
|
});
|
|
245
|
-
|
|
284
|
+
recovery[muscle] = {
|
|
285
|
+
fatigueLevel: Math.round(fatigueLevel),
|
|
286
|
+
recoveryHours: Math.round(recoveryHours),
|
|
287
|
+
trainingStress: Math.round(totalStress),
|
|
288
|
+
exercises: involvedExercises,
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
return recovery;
|
|
246
292
|
};
|
|
247
293
|
/**
|
|
248
294
|
* Get base recovery time for different muscle groups
|
|
249
295
|
*/
|
|
250
296
|
const getMuscleBaseRecovery = (muscle) => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
297
|
+
// Recovery times in hours based on muscle size and recovery capacity
|
|
298
|
+
const recoveryTimes = {
|
|
299
|
+
"pectoralis-major": 48,
|
|
300
|
+
"pectoralis-minor": 48,
|
|
301
|
+
"latissimus-dorsi": 48,
|
|
302
|
+
trapezius: 36,
|
|
303
|
+
rhomboids: 36,
|
|
304
|
+
"deltoids-anterior": 36,
|
|
305
|
+
"deltoids-middle": 36,
|
|
306
|
+
"bicep-short-inner": 24,
|
|
307
|
+
"bicep-long-outer": 24,
|
|
308
|
+
"tricep-brachii-long": 24,
|
|
309
|
+
"tricep-brachii-lateral": 24,
|
|
310
|
+
quadriceps: 48,
|
|
311
|
+
hamstrings: 48,
|
|
312
|
+
"maximus-lower": 48,
|
|
313
|
+
"medius-upper": 36,
|
|
314
|
+
"calf-inner": 24,
|
|
315
|
+
"calf-outer": 24,
|
|
316
|
+
"abs-upper": 24,
|
|
317
|
+
"abs-lower": 24,
|
|
318
|
+
obliques: 24,
|
|
319
|
+
"lower-back": 48,
|
|
320
|
+
"fore-arm-inner": 18,
|
|
321
|
+
"fore-arm-outer": 18,
|
|
322
|
+
};
|
|
323
|
+
return recoveryTimes[muscle] || 36; // Default 36 hours
|
|
278
324
|
};
|
|
279
325
|
/**
|
|
280
326
|
* Determine overall fatigue level
|
|
281
327
|
*/
|
|
282
|
-
const getFatigueLevel = (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
328
|
+
const getFatigueLevel = (
|
|
329
|
+
averageScore,
|
|
330
|
+
totalScore,
|
|
331
|
+
workoutDuration,
|
|
332
|
+
userProfile
|
|
333
|
+
) => {
|
|
334
|
+
const fitnessAdjustment = (
|
|
335
|
+
userProfile === null || userProfile === void 0
|
|
336
|
+
? void 0
|
|
337
|
+
: userProfile.fitnessLevel
|
|
338
|
+
)
|
|
339
|
+
? (userProfile.fitnessLevel - 3) * 0.1 // ±20% based on fitness level
|
|
340
|
+
: 0;
|
|
341
|
+
const intensity = averageScore * (1 - fitnessAdjustment);
|
|
342
|
+
const volume = totalScore / 100; // Normalize volume
|
|
343
|
+
const density = totalScore / workoutDuration; // Score per minute
|
|
344
|
+
if (intensity > 80 || volume > 15 || density > 8) return "extreme";
|
|
345
|
+
if (intensity > 65 || volume > 12 || density > 6) return "very_hard";
|
|
346
|
+
if (intensity > 50 || volume > 8 || density > 4) return "hard";
|
|
347
|
+
if (intensity > 35 || volume > 5 || density > 2) return "moderate";
|
|
348
|
+
return "light";
|
|
298
349
|
};
|
|
299
350
|
/**
|
|
300
351
|
* Get recommended rest days based on fatigue
|
|
301
352
|
*/
|
|
302
353
|
const getRecommendedRestDays = (fatigueLevel, muscleRecovery) => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
354
|
+
// Base rest days by fatigue level
|
|
355
|
+
const baseDays = {
|
|
356
|
+
light: 0,
|
|
357
|
+
moderate: 1,
|
|
358
|
+
hard: 1,
|
|
359
|
+
very_hard: 2,
|
|
360
|
+
extreme: 3,
|
|
361
|
+
};
|
|
362
|
+
const restDays = baseDays[fatigueLevel];
|
|
363
|
+
// Consider muscle recovery times - using manual iteration
|
|
364
|
+
const recoveryHours = [];
|
|
365
|
+
for (const muscle in muscleRecovery) {
|
|
366
|
+
if (muscleRecovery.hasOwnProperty(muscle)) {
|
|
367
|
+
recoveryHours.push(muscleRecovery[muscle].recoveryHours);
|
|
318
368
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
369
|
+
}
|
|
370
|
+
let maxRecoveryHours = 0;
|
|
371
|
+
for (let i = 0; i < recoveryHours.length; i++) {
|
|
372
|
+
if (recoveryHours[i] > maxRecoveryHours) {
|
|
373
|
+
maxRecoveryHours = recoveryHours[i];
|
|
324
374
|
}
|
|
325
|
-
|
|
326
|
-
|
|
375
|
+
}
|
|
376
|
+
const muscleBasedDays = Math.ceil(maxRecoveryHours / 24);
|
|
377
|
+
return Math.max(restDays, muscleBasedDays);
|
|
327
378
|
};
|
|
328
379
|
/**
|
|
329
380
|
* Generate user-friendly workout summary text
|
|
330
381
|
*/
|
|
331
382
|
const generateWorkoutSummaryText = (summary) => {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
383
|
+
const {
|
|
384
|
+
totalScore,
|
|
385
|
+
averageScore,
|
|
386
|
+
totalCalories,
|
|
387
|
+
totalEpocCalories,
|
|
388
|
+
workoutDuration,
|
|
389
|
+
fatigueLevel,
|
|
390
|
+
recommendedRestDays,
|
|
391
|
+
exerciseSummaries,
|
|
392
|
+
} = summary;
|
|
393
|
+
const intensityMap = {
|
|
394
|
+
light: "Light workout",
|
|
395
|
+
moderate: "Moderate workout",
|
|
396
|
+
hard: "Hard workout",
|
|
397
|
+
very_hard: "Very hard workout",
|
|
398
|
+
extreme: "Extreme workout",
|
|
399
|
+
};
|
|
400
|
+
const intensityText = intensityMap[fatigueLevel];
|
|
401
|
+
const restText =
|
|
402
|
+
recommendedRestDays === 0
|
|
403
|
+
? "You can train again tomorrow"
|
|
404
|
+
: `Recommended rest: ${recommendedRestDays} day${
|
|
405
|
+
recommendedRestDays > 1 ? "s" : ""
|
|
406
|
+
}`;
|
|
407
|
+
return `
|
|
345
408
|
🏋️ **${intensityText}** (Score: ${totalScore})
|
|
346
409
|
⏱️ Duration: ${workoutDuration} minutes
|
|
347
410
|
🔥 Calories: ${totalCalories} + ${totalEpocCalories} afterburn
|
|
@@ -349,8 +412,13 @@ const generateWorkoutSummaryText = (summary) => {
|
|
|
349
412
|
|
|
350
413
|
💪 **Exercises completed:**
|
|
351
414
|
${exerciseSummaries
|
|
352
|
-
|
|
353
|
-
|
|
415
|
+
.map(
|
|
416
|
+
(ex) =>
|
|
417
|
+
`• ${ex.exercise.name}: ${ex.sets.length} sets (${Math.round(
|
|
418
|
+
ex.totalScore
|
|
419
|
+
)} pts)`
|
|
420
|
+
)
|
|
421
|
+
.join("\n")}
|
|
354
422
|
|
|
355
423
|
🛌 **Recovery:** ${restText}
|
|
356
424
|
`.trim();
|