@dgpholdings/greatoak-shared 1.1.36 → 1.1.37
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/typeGuards/index.d.ts +2 -1
- package/dist/typeGuards/index.js +3 -1
- package/dist/types/TApiAuth.d.ts +2 -0
- package/dist/types/TApiExercise.d.ts +21 -1
- package/dist/types/TApiUser.d.ts +4 -1
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +4 -1
- package/dist/utils/scoring.utils.d.ts +14 -8
- package/dist/utils/scoring.utils.js +282 -129
- package/dist/utils/workoutSummary.util.d.ts +87 -0
- package/dist/utils/workoutSummary.util.js +311 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { TRecord, TRecordBodyWeight, TRecordDuration, TRecordWeight } from "../types/TApiExerciseRecord";
|
|
1
|
+
import { TRecord, TRecordBodyWeight, TRecordDistance, TRecordDuration, TRecordWeight } from "../types/TApiExerciseRecord";
|
|
2
2
|
export declare const isRecordWeightTypeGuard: (param: TRecord[]) => param is TRecordWeight[];
|
|
3
3
|
export declare const isRecordDurationTypeGuard: (param: TRecord[]) => param is TRecordDuration[];
|
|
4
4
|
export declare const isRecordBodyWeightTypeGuard: (param: TRecord[]) => param is TRecordBodyWeight[];
|
|
5
|
+
export declare const isRecordDistanceTypeGuard: (param: TRecord[]) => param is TRecordDistance[];
|
package/dist/typeGuards/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isRecordBodyWeightTypeGuard = exports.isRecordDurationTypeGuard = exports.isRecordWeightTypeGuard = void 0;
|
|
3
|
+
exports.isRecordDistanceTypeGuard = exports.isRecordBodyWeightTypeGuard = exports.isRecordDurationTypeGuard = exports.isRecordWeightTypeGuard = void 0;
|
|
4
4
|
const isRecordWeightTypeGuard = (param) => { var _a; return ((_a = param === null || param === void 0 ? void 0 : param[0]) === null || _a === void 0 ? void 0 : _a.type) === "weight-reps"; };
|
|
5
5
|
exports.isRecordWeightTypeGuard = isRecordWeightTypeGuard;
|
|
6
6
|
const isRecordDurationTypeGuard = (param) => { var _a; return ((_a = param === null || param === void 0 ? void 0 : param[0]) === null || _a === void 0 ? void 0 : _a.type) === "duration"; };
|
|
7
7
|
exports.isRecordDurationTypeGuard = isRecordDurationTypeGuard;
|
|
8
8
|
const isRecordBodyWeightTypeGuard = (param) => { var _a; return ((_a = param === null || param === void 0 ? void 0 : param[0]) === null || _a === void 0 ? void 0 : _a.type) === "reps-only"; };
|
|
9
9
|
exports.isRecordBodyWeightTypeGuard = isRecordBodyWeightTypeGuard;
|
|
10
|
+
const isRecordDistanceTypeGuard = (param) => param[0].type === "distance";
|
|
11
|
+
exports.isRecordDistanceTypeGuard = isRecordDistanceTypeGuard;
|
package/dist/types/TApiAuth.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ export type TUser = {
|
|
|
78
78
|
billingPlanType: "monthly" | "annually" | "free";
|
|
79
79
|
billingPlanVersion?: string;
|
|
80
80
|
billingPlanGeoCountryCode: string;
|
|
81
|
+
fitnessLevel?: number;
|
|
81
82
|
paymentMethod?: "credit_card" | "paypal" | "apple_pay" | "upi";
|
|
82
83
|
billingPlanNextDueDate?: string;
|
|
83
84
|
devices: {
|
|
@@ -91,6 +92,7 @@ export type TUser = {
|
|
|
91
92
|
dob?: Date;
|
|
92
93
|
weightKg?: number;
|
|
93
94
|
heightCm?: number;
|
|
95
|
+
bodyFatPercentage?: number;
|
|
94
96
|
};
|
|
95
97
|
export type TUserSignInData = Pick<TUser, "email" | "isVerified" | "appLanguage" | "billingPlanType" | "billingPlanVersion" | "paymentMethod" | "authMethod" | "dob" | "weightKg" | "gender"> & {
|
|
96
98
|
createdAt: Date;
|
|
@@ -39,7 +39,6 @@ export type TExercise = {
|
|
|
39
39
|
secondaryMuscles: TBodyPartKeys;
|
|
40
40
|
trainingTypes: TTrainingType[];
|
|
41
41
|
avgSetDurationInSecs?: number;
|
|
42
|
-
difficultyScoreMultiplier: number;
|
|
43
42
|
difficultyLevel: number;
|
|
44
43
|
hypertrophyLevel: number;
|
|
45
44
|
strengthGainLevel: number;
|
|
@@ -47,6 +46,27 @@ export type TExercise = {
|
|
|
47
46
|
calorieBurnLevel: number;
|
|
48
47
|
stabilityLevel: number;
|
|
49
48
|
enduranceLevel: number;
|
|
49
|
+
metabolicData: {
|
|
50
|
+
baseMET: number;
|
|
51
|
+
metRange: [number, number];
|
|
52
|
+
compoundMultiplier: number;
|
|
53
|
+
muscleGroupFactor: number;
|
|
54
|
+
intensityScaling: "linear" | "exponential" | "plateau";
|
|
55
|
+
epocFactor: number;
|
|
56
|
+
weightFactors?: {
|
|
57
|
+
lightWeight: number;
|
|
58
|
+
moderateWeight: number;
|
|
59
|
+
heavyWeight: number;
|
|
60
|
+
};
|
|
61
|
+
durationFactors?: {
|
|
62
|
+
shortDuration: number;
|
|
63
|
+
mediumDuration: number;
|
|
64
|
+
longDuration: number;
|
|
65
|
+
};
|
|
66
|
+
paceFactors?: {
|
|
67
|
+
[key: string]: number;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
50
70
|
youtubeVideoUrl: string[];
|
|
51
71
|
modelVideoUrl: string;
|
|
52
72
|
thumbnailUrl: string;
|
package/dist/types/TApiUser.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { TGender } from "./TApiAuth";
|
|
1
2
|
export type TUserMetric = {
|
|
2
3
|
dob: Date;
|
|
3
4
|
weightKg?: number;
|
|
5
|
+
heightCm?: number;
|
|
4
6
|
appLanguage: string;
|
|
5
7
|
emailAddress?: string;
|
|
6
|
-
gender:
|
|
8
|
+
gender: TGender;
|
|
9
|
+
fitnessLevel?: 1 | 2 | 3 | 4 | 5;
|
|
7
10
|
createdAt: Date;
|
|
8
11
|
updatedAt?: Date;
|
|
9
12
|
};
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -4,4 +4,7 @@ export { countryToCurrencyCode } from "./billing.utils";
|
|
|
4
4
|
export { computeScoreFromRecord } from "./scoring.utils";
|
|
5
5
|
export { isDefined, isDefinedNumber } from "./isDefined.utils";
|
|
6
6
|
export { refineRecordEntry } from "./record.utils";
|
|
7
|
+
export { generateWorkoutSummaryText, calculateWorkoutSummary, } from "./workoutSummary.util";
|
|
7
8
|
export type { TRefinedRecord, TRefinedBodyWeightRecord, TRefinedDurationRecord, TRefinedWeightRecord, } from "./record.utils";
|
|
9
|
+
export type { TUserProfile, TScoreBreakdown } from "./scoring.utils";
|
|
10
|
+
export type { TWorkoutSummary, TExerciseSummary, TSetSummary, TMuscleRecoveryMap, } from "./workoutSummary.util";
|
package/dist/utils/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.refineRecordEntry = exports.isDefinedNumber = exports.isDefined = exports.computeScoreFromRecord = exports.countryToCurrencyCode = exports.getDaysAndHoursDifference = exports.isUserAllowedToUpdate = exports.mmssToSecs = exports.toNumber = void 0;
|
|
3
|
+
exports.calculateWorkoutSummary = exports.generateWorkoutSummaryText = exports.refineRecordEntry = exports.isDefinedNumber = exports.isDefined = exports.computeScoreFromRecord = exports.countryToCurrencyCode = exports.getDaysAndHoursDifference = exports.isUserAllowedToUpdate = exports.mmssToSecs = exports.toNumber = void 0;
|
|
4
4
|
var number_util_1 = require("./number.util");
|
|
5
5
|
Object.defineProperty(exports, "toNumber", { enumerable: true, get: function () { return number_util_1.toNumber; } });
|
|
6
6
|
var time_util_1 = require("./time.util");
|
|
@@ -16,3 +16,6 @@ Object.defineProperty(exports, "isDefined", { enumerable: true, get: function ()
|
|
|
16
16
|
Object.defineProperty(exports, "isDefinedNumber", { enumerable: true, get: function () { return isDefined_utils_1.isDefinedNumber; } });
|
|
17
17
|
var record_utils_1 = require("./record.utils");
|
|
18
18
|
Object.defineProperty(exports, "refineRecordEntry", { enumerable: true, get: function () { return record_utils_1.refineRecordEntry; } });
|
|
19
|
+
var workoutSummary_util_1 = require("./workoutSummary.util");
|
|
20
|
+
Object.defineProperty(exports, "generateWorkoutSummaryText", { enumerable: true, get: function () { return workoutSummary_util_1.generateWorkoutSummaryText; } });
|
|
21
|
+
Object.defineProperty(exports, "calculateWorkoutSummary", { enumerable: true, get: function () { return workoutSummary_util_1.calculateWorkoutSummary; } });
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { TRefinedRecord } from "./record.utils";
|
|
2
|
-
|
|
2
|
+
import { TExercise, TGender, TUserMetric } from "../types";
|
|
3
|
+
export type TUserProfile = {
|
|
3
4
|
age?: number;
|
|
4
|
-
gender?:
|
|
5
|
+
gender?: TGender;
|
|
5
6
|
weightKg?: number;
|
|
7
|
+
heightCm?: number;
|
|
8
|
+
bodyFatPercentage?: number;
|
|
9
|
+
fitnessLevel?: TUserMetric["fitnessLevel"];
|
|
10
|
+
fitnessGoal?: "strength" | "hypertrophy" | "endurance" | "general";
|
|
6
11
|
};
|
|
7
|
-
type TScoreBreakdown = {
|
|
12
|
+
export type TScoreBreakdown = {
|
|
8
13
|
baseScore: number;
|
|
9
14
|
plus: {
|
|
10
15
|
[reason: string]: number;
|
|
@@ -13,18 +18,19 @@ type TScoreBreakdown = {
|
|
|
13
18
|
[reason: string]: number;
|
|
14
19
|
}[];
|
|
15
20
|
finalScore: number;
|
|
21
|
+
caloriesBurned: number;
|
|
22
|
+
metValue: number;
|
|
23
|
+
epocCalories: number;
|
|
16
24
|
};
|
|
17
25
|
type TParams = {
|
|
18
26
|
record: TRefinedRecord;
|
|
27
|
+
exercise: TExercise;
|
|
19
28
|
userProfile?: TUserProfile;
|
|
20
29
|
avgRestDurationSecs?: number;
|
|
21
|
-
exerciseDefaultDifficultyMultiplier?: number;
|
|
22
30
|
isTimeIntervalModeEnabled?: boolean;
|
|
23
|
-
sessionSetCount?: number;
|
|
24
31
|
};
|
|
25
32
|
/**
|
|
26
|
-
*
|
|
27
|
-
* Uses improved load calculations with type-specific normalization and MET values for cardio.
|
|
33
|
+
* Enhanced training stress score computation using metabolic data
|
|
28
34
|
*/
|
|
29
|
-
export declare const computeScoreFromRecord: ({
|
|
35
|
+
export declare const computeScoreFromRecord: ({ avgRestDurationSecs, record, exercise, userProfile, isTimeIntervalModeEnabled, }: TParams) => TScoreBreakdown;
|
|
30
36
|
export {};
|
|
@@ -3,151 +3,309 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.computeScoreFromRecord = void 0;
|
|
4
4
|
const time_util_1 = require("./time.util");
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* Uses improved load calculations with type-specific normalization and MET values for cardio.
|
|
6
|
+
* Calculate BMR using Mifflin-St Jeor Equation with body composition adjustment
|
|
8
7
|
*/
|
|
9
|
-
const
|
|
10
|
-
var _a, _b, _c
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
};
|
|
8
|
+
const calculateBMR = (userProfile) => {
|
|
9
|
+
var _a, _b, _c;
|
|
10
|
+
const weight = (_a = userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
|
|
11
|
+
const height = (_b = userProfile.heightCm) !== null && _b !== void 0 ? _b : 170;
|
|
12
|
+
const age = (_c = userProfile.age) !== null && _c !== void 0 ? _c : 30;
|
|
13
|
+
let bmr;
|
|
14
|
+
if (userProfile.gender === "female") {
|
|
15
|
+
bmr = 10 * weight + 6.25 * height - 5 * age - 161;
|
|
18
16
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
else {
|
|
18
|
+
bmr = 10 * weight + 6.25 * height - 5 * age + 5;
|
|
19
|
+
}
|
|
20
|
+
// Adjust for body composition if available
|
|
21
|
+
if (userProfile.bodyFatPercentage) {
|
|
22
|
+
const leanBodyMass = weight * (1 - userProfile.bodyFatPercentage / 100);
|
|
23
|
+
const lbmAdjustment = leanBodyMass * 0.56;
|
|
24
|
+
bmr += lbmAdjustment;
|
|
25
|
+
}
|
|
26
|
+
return Math.max(1200, bmr);
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Calculate precise MET value using exercise metabolic data
|
|
30
|
+
*/
|
|
31
|
+
const calculateMETValue = (record, exercise, effortFactor, userProfile) => {
|
|
32
|
+
var _a, _b, _c, _d, _e;
|
|
33
|
+
const { metabolicData } = exercise;
|
|
34
|
+
const userWeight = (_a = userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
|
|
35
|
+
const fitnessLevel = (_b = userProfile.fitnessLevel) !== null && _b !== void 0 ? _b : 3;
|
|
36
|
+
// Fitness level efficiency: trained individuals are more efficient
|
|
37
|
+
const efficiencyFactor = 1 - (fitnessLevel - 1) * 0.04; // 4% more efficient per level
|
|
38
|
+
let calculatedMET = metabolicData.baseMET;
|
|
39
|
+
// Apply compound movement multiplier
|
|
40
|
+
calculatedMET *= metabolicData.compoundMultiplier;
|
|
41
|
+
// Apply muscle group factor
|
|
42
|
+
calculatedMET *= metabolicData.muscleGroupFactor / 2; // Normalize to reasonable range
|
|
43
|
+
// Record type specific adjustments
|
|
25
44
|
switch (record.type) {
|
|
26
45
|
case "weight-reps": {
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
if (metabolicData.weightFactors) {
|
|
47
|
+
const weightRatio = record.kg / userWeight;
|
|
48
|
+
let weightMultiplier = metabolicData.weightFactors.moderateWeight;
|
|
49
|
+
if (weightRatio < 0.5) {
|
|
50
|
+
weightMultiplier = metabolicData.weightFactors.lightWeight;
|
|
51
|
+
}
|
|
52
|
+
else if (weightRatio > 1.0) {
|
|
53
|
+
weightMultiplier = metabolicData.weightFactors.heavyWeight;
|
|
54
|
+
}
|
|
55
|
+
calculatedMET *= weightMultiplier;
|
|
56
|
+
}
|
|
57
|
+
// Apply rep-based intensity scaling
|
|
58
|
+
const repAdjustment = getRepIntensityAdjustment(record.reps, metabolicData.intensityScaling);
|
|
59
|
+
calculatedMET *= repAdjustment;
|
|
29
60
|
break;
|
|
30
61
|
}
|
|
31
|
-
case "
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
case "reps-only": {
|
|
63
|
+
// High rep bodyweight exercises get metabolic bonus
|
|
64
|
+
const repAdjustment = getRepIntensityAdjustment(record.reps, metabolicData.intensityScaling);
|
|
65
|
+
calculatedMET *= repAdjustment;
|
|
66
|
+
// Bodyweight exercises scale with user weight efficiency
|
|
67
|
+
if (userWeight > 80) {
|
|
68
|
+
calculatedMET *= 1.1; // Heavier people work harder
|
|
38
69
|
}
|
|
39
|
-
else {
|
|
40
|
-
|
|
41
|
-
rawLoad = userWeight * (durationSecs / 60); // Per minute scaling
|
|
42
|
-
normalizedLoad = rawLoad * 1.2; // Duration exercises are metabolically demanding
|
|
70
|
+
else if (userWeight < 60) {
|
|
71
|
+
calculatedMET *= 0.95; // Lighter people have slight advantage
|
|
43
72
|
}
|
|
44
73
|
break;
|
|
45
74
|
}
|
|
46
|
-
case "
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
75
|
+
case "duration": {
|
|
76
|
+
const durationSecs = (0, time_util_1.mmssToSecs)((_c = record.durationSecs) !== null && _c !== void 0 ? _c : "00:00");
|
|
77
|
+
if (metabolicData.durationFactors) {
|
|
78
|
+
let durationMultiplier = metabolicData.durationFactors.mediumDuration;
|
|
79
|
+
if (durationSecs < 30) {
|
|
80
|
+
durationMultiplier = metabolicData.durationFactors.shortDuration;
|
|
81
|
+
}
|
|
82
|
+
else if (durationSecs > 120) {
|
|
83
|
+
durationMultiplier = metabolicData.durationFactors.longDuration;
|
|
84
|
+
}
|
|
85
|
+
calculatedMET *= durationMultiplier;
|
|
86
|
+
}
|
|
52
87
|
break;
|
|
53
88
|
}
|
|
54
89
|
case "distance": {
|
|
55
|
-
const distance = (
|
|
56
|
-
const durationSecs = (0, time_util_1.mmssToSecs)((
|
|
57
|
-
if (distance > 0 && durationSecs > 0) {
|
|
58
|
-
// Calculate speed and estimate MET value
|
|
90
|
+
const distance = (_d = record.distanceKm) !== null && _d !== void 0 ? _d : 0;
|
|
91
|
+
const durationSecs = (0, time_util_1.mmssToSecs)((_e = record.durationSecs) !== null && _e !== void 0 ? _e : "00:00");
|
|
92
|
+
if (distance > 0 && durationSecs > 0 && metabolicData.paceFactors) {
|
|
59
93
|
const speedKmh = (distance * 3600) / durationSecs;
|
|
60
|
-
//
|
|
61
|
-
let
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
rawLoad = caloriesBurned;
|
|
75
|
-
normalizedLoad = caloriesBurned * 8; // Scale cardio to match resistance training
|
|
94
|
+
// Find closest pace factor
|
|
95
|
+
let closestMET = metabolicData.baseMET;
|
|
96
|
+
let closestSpeed = Infinity;
|
|
97
|
+
const paceKeys = Object.keys(metabolicData.paceFactors);
|
|
98
|
+
for (let i = 0; i < paceKeys.length; i++) {
|
|
99
|
+
const paceStr = paceKeys[i];
|
|
100
|
+
const metVal = metabolicData.paceFactors[paceStr];
|
|
101
|
+
const pace = parseFloat(paceStr);
|
|
102
|
+
if (Math.abs(speedKmh - pace) < Math.abs(speedKmh - closestSpeed)) {
|
|
103
|
+
closestSpeed = pace;
|
|
104
|
+
closestMET = metVal;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
calculatedMET = closestMET;
|
|
76
108
|
}
|
|
77
109
|
break;
|
|
78
110
|
}
|
|
79
111
|
}
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
112
|
+
// Apply effort factor (RPE/RIR influence on MET)
|
|
113
|
+
const effortMultiplier = 0.7 + (effortFactor / 10) * 0.5; // 0.7 to 1.2x
|
|
114
|
+
calculatedMET *= effortMultiplier;
|
|
115
|
+
// Apply interval training bonus
|
|
116
|
+
if (record.workDurationSecs && record.restDurationSecs) {
|
|
117
|
+
const workToRestRatio = record.workDurationSecs / record.restDurationSecs;
|
|
118
|
+
const intervalBonus = 1 + Math.min(0.3, workToRestRatio * 0.08); // Up to 30% bonus
|
|
119
|
+
calculatedMET *= intervalBonus;
|
|
120
|
+
}
|
|
121
|
+
// Apply fitness level efficiency
|
|
122
|
+
calculatedMET *= efficiencyFactor;
|
|
123
|
+
// Ensure MET stays within exercise's defined range
|
|
124
|
+
const [minMET, maxMET] = metabolicData.metRange;
|
|
125
|
+
calculatedMET = Math.max(minMET, Math.min(maxMET, calculatedMET));
|
|
126
|
+
return parseFloat(calculatedMET.toFixed(1));
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Get rep-based intensity adjustment
|
|
130
|
+
*/
|
|
131
|
+
const getRepIntensityAdjustment = (reps, scalingType) => {
|
|
132
|
+
const baseReps = 8; // Reference point
|
|
133
|
+
const repDiff = reps - baseReps;
|
|
134
|
+
switch (scalingType) {
|
|
135
|
+
case "linear":
|
|
136
|
+
return Math.max(0.7, 1 + repDiff * 0.02); // 2% per rep difference
|
|
137
|
+
case "exponential":
|
|
138
|
+
return Math.max(0.7, Math.pow(1.04, repDiff)); // Exponential growth
|
|
139
|
+
case "plateau":
|
|
140
|
+
// Plateaus at high reps but grows quickly initially
|
|
141
|
+
if (reps > 20)
|
|
142
|
+
return 1.3;
|
|
143
|
+
return Math.max(0.7, 1 + repDiff * 0.04);
|
|
144
|
+
default:
|
|
145
|
+
return 1.0;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Calculate exercise duration in minutes
|
|
150
|
+
*/
|
|
151
|
+
const getExerciseDurationMinutes = (record, exercise) => {
|
|
152
|
+
var _a, _b;
|
|
153
|
+
let durationSecs = 0;
|
|
154
|
+
switch (record.type) {
|
|
155
|
+
case "weight-reps":
|
|
156
|
+
case "reps-only": {
|
|
157
|
+
if (exercise.avgSetDurationInSecs) {
|
|
158
|
+
durationSecs = exercise.avgSetDurationInSecs;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Estimate based on reps and exercise complexity
|
|
162
|
+
const reps = record.type === "weight-reps" ? record.reps : record.reps;
|
|
163
|
+
const baseTimePerRep = exercise.difficultyLevel > 7 ? 3.5 : 2.5;
|
|
164
|
+
durationSecs = reps * baseTimePerRep;
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case "duration": {
|
|
169
|
+
durationSecs = (0, time_util_1.mmssToSecs)((_a = record.durationSecs) !== null && _a !== void 0 ? _a : "00:00");
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "distance": {
|
|
173
|
+
durationSecs = (0, time_util_1.mmssToSecs)((_b = record.durationSecs) !== null && _b !== void 0 ? _b : "00:00");
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Add rest time if available (but not for distance exercises)
|
|
178
|
+
if (record.type !== "distance" && record.restDurationSecs) {
|
|
179
|
+
durationSecs += record.restDurationSecs;
|
|
180
|
+
}
|
|
181
|
+
return Math.max(0.5, durationSecs / 60);
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Calculate comprehensive calorie burn including EPOC
|
|
185
|
+
*/
|
|
186
|
+
const calculateCalorieBurn = (record, exercise, metValue, durationMinutes, userProfile) => {
|
|
187
|
+
var _a;
|
|
188
|
+
const userWeight = (_a = userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
|
|
189
|
+
const bmr = calculateBMR(userProfile);
|
|
190
|
+
const bmrPerMinute = bmr / (24 * 60);
|
|
191
|
+
// Exercise calories: MET × weight(kg) × duration(hours)
|
|
192
|
+
const exerciseCalories = metValue * userWeight * (durationMinutes / 60);
|
|
193
|
+
// EPOC (Excess Post-Exercise Oxygen Consumption) calories
|
|
194
|
+
const epocCalories = exerciseCalories * exercise.metabolicData.epocFactor;
|
|
195
|
+
// BMR calories during exercise time
|
|
196
|
+
const bmrCaloriesDuringExercise = bmrPerMinute * durationMinutes;
|
|
197
|
+
// Total calories = exercise + EPOC + BMR during exercise
|
|
198
|
+
const totalCalories = exerciseCalories + epocCalories + bmrCaloriesDuringExercise;
|
|
199
|
+
return {
|
|
200
|
+
exerciseCalories: parseFloat(exerciseCalories.toFixed(1)),
|
|
201
|
+
epocCalories: parseFloat(epocCalories.toFixed(1)),
|
|
202
|
+
totalCalories: parseFloat(totalCalories.toFixed(1)),
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Enhanced training stress score computation using metabolic data
|
|
207
|
+
*/
|
|
208
|
+
const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userProfile, isTimeIntervalModeEnabled = false, }) => {
|
|
209
|
+
var _a, _b;
|
|
210
|
+
if (!record.isDone) {
|
|
211
|
+
return {
|
|
212
|
+
baseScore: 0,
|
|
213
|
+
plus: [],
|
|
214
|
+
minus: [],
|
|
215
|
+
finalScore: 0,
|
|
216
|
+
caloriesBurned: 0,
|
|
217
|
+
metValue: 0,
|
|
218
|
+
epocCalories: 0,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const plus = [];
|
|
222
|
+
const minus = [];
|
|
223
|
+
// Calculate effort factor from RPE/RIR
|
|
224
|
+
let effortFactor = 5; // Default moderate effort
|
|
85
225
|
if (typeof record.rpe === "number") {
|
|
86
226
|
effortFactor = Math.max(1, Math.min(10, record.rpe));
|
|
87
227
|
}
|
|
88
228
|
else if (typeof record.rir === "number") {
|
|
89
229
|
effortFactor = Math.max(1, Math.min(10, 10 - record.rir));
|
|
90
230
|
}
|
|
91
|
-
|
|
92
|
-
|
|
231
|
+
// Calculate precise MET value using exercise metabolic data
|
|
232
|
+
const metValue = calculateMETValue(record, exercise, effortFactor, userProfile || {});
|
|
233
|
+
// Calculate exercise duration
|
|
234
|
+
const durationMinutes = getExerciseDurationMinutes(record, exercise);
|
|
235
|
+
// Calculate comprehensive calorie burn
|
|
236
|
+
const calorieData = calculateCalorieBurn(record, exercise, metValue, durationMinutes, userProfile || {});
|
|
237
|
+
// Base score from calorie burn (more accurate than arbitrary load calculations)
|
|
238
|
+
const calorieBasedLoad = calorieData.exerciseCalories + calorieData.epocCalories * 2; // EPOC counts double
|
|
239
|
+
const base = Math.log10(1 + Math.max(5, calorieBasedLoad));
|
|
240
|
+
let workingScore = base;
|
|
241
|
+
// Exercise difficulty bonus (from database)
|
|
242
|
+
const difficultyMultiplier = 0.8 + (exercise.difficultyLevel / 10) * 0.4; // 0.8 to 1.2x
|
|
243
|
+
workingScore *= difficultyMultiplier;
|
|
93
244
|
plus.push({
|
|
94
|
-
|
|
245
|
+
exerciseDifficulty: parseFloat((difficultyMultiplier - 1).toFixed(3)),
|
|
95
246
|
});
|
|
96
|
-
//
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
247
|
+
// Goal alignment bonus
|
|
248
|
+
if (userProfile === null || userProfile === void 0 ? void 0 : userProfile.fitnessGoal) {
|
|
249
|
+
let goalMultiplier = 1;
|
|
250
|
+
switch (userProfile.fitnessGoal) {
|
|
251
|
+
case "strength":
|
|
252
|
+
goalMultiplier = 1 + (exercise.strengthGainLevel / 10) * 0.12;
|
|
253
|
+
break;
|
|
254
|
+
case "hypertrophy":
|
|
255
|
+
goalMultiplier = 1 + (exercise.hypertrophyLevel / 10) * 0.12;
|
|
256
|
+
break;
|
|
257
|
+
case "endurance":
|
|
258
|
+
goalMultiplier = 1 + (exercise.enduranceLevel / 10) * 0.12;
|
|
259
|
+
break;
|
|
260
|
+
case "general":
|
|
261
|
+
const avgLevel = (exercise.strengthGainLevel +
|
|
262
|
+
exercise.hypertrophyLevel +
|
|
263
|
+
exercise.enduranceLevel +
|
|
264
|
+
exercise.flexibilityLevel) /
|
|
265
|
+
4;
|
|
266
|
+
goalMultiplier = 1 + (avgLevel / 10) * 0.08;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
if (goalMultiplier > 1) {
|
|
270
|
+
workingScore *= goalMultiplier;
|
|
271
|
+
plus.push({ goalAlignment: parseFloat((goalMultiplier - 1).toFixed(3)) });
|
|
272
|
+
}
|
|
101
273
|
}
|
|
102
|
-
//
|
|
274
|
+
// Stability and complexity bonus
|
|
275
|
+
const stabilityBonus = 1 + (exercise.stabilityLevel / 10) * 0.06;
|
|
276
|
+
workingScore *= stabilityBonus;
|
|
277
|
+
plus.push({ stabilityDemand: parseFloat((stabilityBonus - 1).toFixed(3)) });
|
|
278
|
+
// High calorie burn efficiency bonus
|
|
279
|
+
if (calorieData.exerciseCalories > 50) {
|
|
280
|
+
// High calorie burn exercises
|
|
281
|
+
const efficiencyBonus = 1.05;
|
|
282
|
+
workingScore *= efficiencyBonus;
|
|
283
|
+
plus.push({
|
|
284
|
+
highCalorieBurn: parseFloat((efficiencyBonus - 1).toFixed(3)),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
// Interval training bonus (already calculated in MET, but add scoring bonus)
|
|
103
288
|
if (isTimeIntervalModeEnabled) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const restRatio = record.restDurationSecs / record.workDurationSecs;
|
|
107
|
-
// Optimal rest ratios vary by exercise type
|
|
108
|
-
let optimalRestRatio = 2.0; // Default
|
|
109
|
-
switch (record.type) {
|
|
110
|
-
case "weight-reps":
|
|
111
|
-
optimalRestRatio = 3.0; // Strength needs more rest
|
|
112
|
-
break;
|
|
113
|
-
case "distance":
|
|
114
|
-
case "duration":
|
|
115
|
-
optimalRestRatio = 1.0; // Cardio can handle shorter rest
|
|
116
|
-
break;
|
|
117
|
-
case "reps-only":
|
|
118
|
-
optimalRestRatio = 1.5; // Bodyweight middle ground
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
// Bonus for staying close to optimal ratio (within 50% deviation)
|
|
122
|
-
const deviation = Math.abs(restRatio - optimalRestRatio) / optimalRestRatio;
|
|
123
|
-
if (deviation <= 0.5) {
|
|
124
|
-
const ratioBonus = 0.05 * (1 - deviation * 2); // Up to 5% bonus
|
|
125
|
-
intervalMultiplier += ratioBonus;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
workingScore *= intervalMultiplier;
|
|
289
|
+
const intervalScoreBonus = 1.08;
|
|
290
|
+
workingScore *= intervalScoreBonus;
|
|
129
291
|
plus.push({
|
|
130
|
-
intervalTraining: parseFloat((
|
|
292
|
+
intervalTraining: parseFloat((intervalScoreBonus - 1).toFixed(3)),
|
|
131
293
|
});
|
|
132
294
|
}
|
|
133
|
-
//
|
|
295
|
+
// User profile adjustments
|
|
134
296
|
let profileMultiplier = 1;
|
|
135
|
-
// Gender adjustment (women typically have less muscle mass)
|
|
136
297
|
if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.gender) === "female") {
|
|
137
|
-
profileMultiplier *= 1.12
|
|
298
|
+
profileMultiplier *= 1.08; // Reduced from 1.12 since metabolic calculations are more accurate
|
|
138
299
|
}
|
|
139
|
-
// Age adjustment (strength declines with age)
|
|
140
300
|
if (userProfile === null || userProfile === void 0 ? void 0 : userProfile.age) {
|
|
141
301
|
if (userProfile.age > 50)
|
|
142
|
-
profileMultiplier *= 1.
|
|
302
|
+
profileMultiplier *= 1.06;
|
|
143
303
|
if (userProfile.age > 65)
|
|
144
|
-
profileMultiplier *= 1.
|
|
304
|
+
profileMultiplier *= 1.1;
|
|
145
305
|
}
|
|
146
|
-
// Weight normalization (smaller people work harder relatively)
|
|
147
306
|
if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.weightKg) && userProfile.weightKg > 0) {
|
|
148
307
|
const weightAdjustment = 70 / userProfile.weightKg;
|
|
149
|
-
|
|
150
|
-
profileMultiplier *= Math.max(0.7, Math.min(1.4, weightAdjustment));
|
|
308
|
+
profileMultiplier *= Math.max(0.85, Math.min(1.25, weightAdjustment));
|
|
151
309
|
}
|
|
152
310
|
if (profileMultiplier !== 1) {
|
|
153
311
|
workingScore *= profileMultiplier;
|
|
@@ -155,38 +313,39 @@ const computeScoreFromRecord = ({ exerciseDefaultDifficultyMultiplier = 1, avgRe
|
|
|
155
313
|
profileAdjustment: parseFloat((profileMultiplier - 1).toFixed(3)),
|
|
156
314
|
});
|
|
157
315
|
}
|
|
158
|
-
//
|
|
316
|
+
// Enhanced rest efficiency penalty
|
|
159
317
|
if (typeof record.restDurationSecs === "number" &&
|
|
160
318
|
typeof avgRestDurationSecs === "number") {
|
|
161
319
|
const restUsed = record.restDurationSecs;
|
|
162
|
-
const baseGrace =
|
|
163
|
-
const effortGrace = effortFactor *
|
|
320
|
+
const baseGrace = 8 + exercise.difficultyLevel * 1.5; // More grace for complex exercises
|
|
321
|
+
const effortGrace = effortFactor * 2.5;
|
|
164
322
|
const totalGrace = baseGrace + effortGrace;
|
|
165
|
-
const maxPenaltyThreshold = totalGrace +
|
|
323
|
+
const maxPenaltyThreshold = totalGrace + 45;
|
|
166
324
|
const excessRest = restUsed - avgRestDurationSecs;
|
|
167
325
|
if (excessRest > totalGrace) {
|
|
168
326
|
const penaltyExcess = Math.min(excessRest - totalGrace, maxPenaltyThreshold - totalGrace);
|
|
169
327
|
const penaltyRatio = penaltyExcess / (maxPenaltyThreshold - totalGrace);
|
|
170
|
-
const restPenalty = penaltyRatio * 0.
|
|
328
|
+
const restPenalty = penaltyRatio * 0.12 * workingScore; // Reduced penalty
|
|
171
329
|
workingScore -= restPenalty;
|
|
172
330
|
minus.push({ excessRestPenalty: parseFloat(restPenalty.toFixed(3)) });
|
|
173
331
|
}
|
|
174
332
|
}
|
|
175
|
-
//
|
|
333
|
+
// Work duration adherence for interval training
|
|
176
334
|
if (isTimeIntervalModeEnabled && record.workDurationSecs) {
|
|
177
335
|
let actualDuration = 0;
|
|
178
336
|
if (record.type === "duration") {
|
|
179
|
-
actualDuration = (0, time_util_1.mmssToSecs)((
|
|
337
|
+
actualDuration = (0, time_util_1.mmssToSecs)((_a = record.durationSecs) !== null && _a !== void 0 ? _a : "00:00");
|
|
180
338
|
}
|
|
181
339
|
else if (record.type === "distance") {
|
|
182
|
-
actualDuration = (0, time_util_1.mmssToSecs)((
|
|
340
|
+
actualDuration = (0, time_util_1.mmssToSecs)((_b = record.durationSecs) !== null && _b !== void 0 ? _b : "00:00");
|
|
183
341
|
}
|
|
184
342
|
if (actualDuration > 0) {
|
|
185
|
-
const
|
|
186
|
-
const
|
|
343
|
+
const expectedDuration = exercise.avgSetDurationInSecs || record.workDurationSecs;
|
|
344
|
+
const durationDiff = Math.abs(actualDuration - expectedDuration);
|
|
345
|
+
const tolerance = expectedDuration * 0.25; // 25% tolerance
|
|
187
346
|
if (durationDiff > tolerance) {
|
|
188
|
-
const excessPercent = (durationDiff - tolerance) /
|
|
189
|
-
const timingPenalty = Math.min(0.
|
|
347
|
+
const excessPercent = (durationDiff - tolerance) / expectedDuration;
|
|
348
|
+
const timingPenalty = Math.min(0.08 * workingScore, excessPercent * 0.05 * workingScore);
|
|
190
349
|
workingScore -= timingPenalty;
|
|
191
350
|
minus.push({
|
|
192
351
|
timingDeviationPenalty: parseFloat(timingPenalty.toFixed(3)),
|
|
@@ -194,24 +353,18 @@ const computeScoreFromRecord = ({ exerciseDefaultDifficultyMultiplier = 1, avgRe
|
|
|
194
353
|
}
|
|
195
354
|
}
|
|
196
355
|
}
|
|
197
|
-
//
|
|
198
|
-
workingScore *= exerciseDefaultDifficultyMultiplier;
|
|
199
|
-
if (exerciseDefaultDifficultyMultiplier !== 1) {
|
|
200
|
-
plus.push({
|
|
201
|
-
exerciseDifficulty: parseFloat((exerciseDefaultDifficultyMultiplier - 1).toFixed(3)),
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
// Step 10: Final score normalization and capping
|
|
356
|
+
// Final score normalization
|
|
205
357
|
let finalScore = Math.max(0, workingScore);
|
|
206
|
-
// Scale to
|
|
207
|
-
finalScore
|
|
208
|
-
// Cap extreme scores to prevent runaway values
|
|
209
|
-
finalScore = Math.min(200, finalScore);
|
|
358
|
+
finalScore *= 12; // Scale to intuitive range (0-120+ for typical exercises)
|
|
359
|
+
finalScore = Math.min(200, finalScore); // Cap extreme scores
|
|
210
360
|
return {
|
|
211
361
|
baseScore: parseFloat(base.toFixed(2)),
|
|
212
362
|
plus,
|
|
213
363
|
minus,
|
|
214
364
|
finalScore: parseFloat(finalScore.toFixed(1)),
|
|
365
|
+
caloriesBurned: calorieData.totalCalories,
|
|
366
|
+
metValue,
|
|
367
|
+
epocCalories: calorieData.epocCalories,
|
|
215
368
|
};
|
|
216
369
|
};
|
|
217
370
|
exports.computeScoreFromRecord = computeScoreFromRecord;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { TRefinedRecord } from "./record.utils";
|
|
2
|
+
import { TExercise, TBodyPartKeys } from "../types";
|
|
3
|
+
import { TScoreBreakdown, TUserProfile } from "./scoring.utils";
|
|
4
|
+
/**
|
|
5
|
+
* EXERCISE SCORING SYSTEM DOCUMENTATION
|
|
6
|
+
* =====================================
|
|
7
|
+
*
|
|
8
|
+
* What the Score Represents:
|
|
9
|
+
* -------------------------
|
|
10
|
+
* The exercise score is a comprehensive measure of training stress that combines:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Energy Expenditure (40%)**: Actual calories burned during exercise + afterburn (EPOC)
|
|
13
|
+
* 2. **Exercise Complexity (25%)**: Difficulty level, stability demands, muscle groups involved
|
|
14
|
+
* 3. **Effort Level (20%)**: RPE/RIR indicating how hard you worked
|
|
15
|
+
* 4. **Goal Alignment (10%)**: How well exercise matches your fitness goals
|
|
16
|
+
* 5. **Personal Factors (5%)**: Age, gender, weight, fitness level adjustments
|
|
17
|
+
*
|
|
18
|
+
* Score Ranges:
|
|
19
|
+
* -------------
|
|
20
|
+
* - 0-20: Light effort (walking, easy stretching)
|
|
21
|
+
* - 21-40: Moderate effort (bodyweight exercises, light weights)
|
|
22
|
+
* - 41-60: Hard effort (challenging sets, compound movements)
|
|
23
|
+
* - 61-80: Very hard effort (heavy weights, high-intensity cardio)
|
|
24
|
+
* - 81-100: Extremely hard effort (max effort sets, intense HIIT)
|
|
25
|
+
* - 100+: Elite/competition level effort
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* ------
|
|
29
|
+
* - Each SET gets its own score
|
|
30
|
+
* - Higher scores = higher training stress
|
|
31
|
+
* - Use for tracking progress, planning recovery
|
|
32
|
+
* - Compare different workouts objectively
|
|
33
|
+
*/
|
|
34
|
+
export type TWorkoutSummary = {
|
|
35
|
+
totalScore: number;
|
|
36
|
+
averageScore: number;
|
|
37
|
+
totalCalories: number;
|
|
38
|
+
totalEpocCalories: number;
|
|
39
|
+
workoutDuration: number;
|
|
40
|
+
exerciseSummaries: TExerciseSummary[];
|
|
41
|
+
muscleRecovery: TMuscleRecoveryMap;
|
|
42
|
+
fatigueLevel: "light" | "moderate" | "hard" | "very_hard" | "extreme";
|
|
43
|
+
recommendedRestDays: number;
|
|
44
|
+
};
|
|
45
|
+
export type TExerciseSummary = {
|
|
46
|
+
exercise: TExercise;
|
|
47
|
+
sets: TSetSummary[];
|
|
48
|
+
totalScore: number;
|
|
49
|
+
averageScore: number;
|
|
50
|
+
totalCalories: number;
|
|
51
|
+
bestSet: {
|
|
52
|
+
setNumber: number;
|
|
53
|
+
score: number;
|
|
54
|
+
reason: string;
|
|
55
|
+
};
|
|
56
|
+
muscleActivation: {
|
|
57
|
+
primary: number;
|
|
58
|
+
secondary: number;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
export type TSetSummary = {
|
|
62
|
+
setNumber: number;
|
|
63
|
+
record: TRefinedRecord;
|
|
64
|
+
scoreBreakdown: TScoreBreakdown;
|
|
65
|
+
description: string;
|
|
66
|
+
};
|
|
67
|
+
export type TMuscleRecoveryMap = {
|
|
68
|
+
[muscle in TBodyPartKeys[number]]: {
|
|
69
|
+
fatigueLevel: number;
|
|
70
|
+
recoveryHours: number;
|
|
71
|
+
trainingStress: number;
|
|
72
|
+
exercises: string[];
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Calculate comprehensive workout summary
|
|
77
|
+
*/
|
|
78
|
+
export declare const calculateWorkoutSummary: (exercises: Array<{
|
|
79
|
+
exercise: TExercise;
|
|
80
|
+
records: TRefinedRecord[];
|
|
81
|
+
startTime?: Date;
|
|
82
|
+
endTime?: Date;
|
|
83
|
+
}>, userProfile?: TUserProfile) => TWorkoutSummary;
|
|
84
|
+
/**
|
|
85
|
+
* Generate user-friendly workout summary text
|
|
86
|
+
*/
|
|
87
|
+
export declare const generateWorkoutSummaryText: (summary: TWorkoutSummary) => string;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateWorkoutSummaryText = exports.calculateWorkoutSummary = void 0;
|
|
4
|
+
const scoring_utils_1 = require("./scoring.utils");
|
|
5
|
+
/**
|
|
6
|
+
* Calculate comprehensive workout summary
|
|
7
|
+
*/
|
|
8
|
+
const calculateWorkoutSummary = (exercises, userProfile) => {
|
|
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 = ((_a = exercises[0]) === null || _a === void 0 ? void 0 : _a.startTime) || new Date();
|
|
18
|
+
const workoutEnd = ((_b = exercises[exercises.length - 1]) === null || _b === void 0 ? void 0 : _b.endTime) || new Date();
|
|
19
|
+
const workoutDuration = Math.max(1, (workoutEnd.getTime() - workoutStart.getTime()) / (1000 * 60));
|
|
20
|
+
// Process each exercise
|
|
21
|
+
exercises.forEach((exerciseData) => {
|
|
22
|
+
const { exercise, records } = exerciseData;
|
|
23
|
+
const sets = [];
|
|
24
|
+
let exerciseScore = 0;
|
|
25
|
+
let exerciseCalories = 0;
|
|
26
|
+
let bestSetScore = 0;
|
|
27
|
+
let bestSetNumber = 1;
|
|
28
|
+
let bestSetReason = "highest score";
|
|
29
|
+
// Process each set/record
|
|
30
|
+
records.forEach((record, index) => {
|
|
31
|
+
if (!record.isDone)
|
|
32
|
+
return;
|
|
33
|
+
const scoreBreakdown = (0, scoring_utils_1.computeScoreFromRecord)({
|
|
34
|
+
record,
|
|
35
|
+
exercise,
|
|
36
|
+
userProfile,
|
|
37
|
+
isTimeIntervalModeEnabled: !!(record.workDurationSecs && record.restDurationSecs),
|
|
38
|
+
});
|
|
39
|
+
const setNumber = index + 1;
|
|
40
|
+
const setDescription = generateSetDescription(record, exercise, scoreBreakdown);
|
|
41
|
+
sets.push({
|
|
42
|
+
setNumber,
|
|
43
|
+
record,
|
|
44
|
+
scoreBreakdown,
|
|
45
|
+
description: setDescription,
|
|
46
|
+
});
|
|
47
|
+
exerciseScore += scoreBreakdown.finalScore;
|
|
48
|
+
exerciseCalories += scoreBreakdown.caloriesBurned;
|
|
49
|
+
totalSets++;
|
|
50
|
+
// Track best set
|
|
51
|
+
if (scoreBreakdown.finalScore > bestSetScore) {
|
|
52
|
+
bestSetScore = scoreBreakdown.finalScore;
|
|
53
|
+
bestSetNumber = setNumber;
|
|
54
|
+
bestSetReason = getBestSetReason(record, exercise);
|
|
55
|
+
}
|
|
56
|
+
// Accumulate muscle stress
|
|
57
|
+
exercise.primaryMuscles.forEach((muscle) => {
|
|
58
|
+
const currentStress = muscleStressMap.get(muscle) || 0;
|
|
59
|
+
muscleStressMap.set(muscle, currentStress + scoreBreakdown.finalScore * 1.0); // Primary muscles get full stress
|
|
60
|
+
});
|
|
61
|
+
exercise.secondaryMuscles.forEach((muscle) => {
|
|
62
|
+
const currentStress = muscleStressMap.get(muscle) || 0;
|
|
63
|
+
muscleStressMap.set(muscle, currentStress + scoreBreakdown.finalScore * 0.6); // Secondary muscles get 60% stress
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
if (sets.length > 0) {
|
|
67
|
+
exerciseSummaries.push({
|
|
68
|
+
exercise,
|
|
69
|
+
sets,
|
|
70
|
+
totalScore: exerciseScore,
|
|
71
|
+
averageScore: exerciseScore / sets.length,
|
|
72
|
+
totalCalories: exerciseCalories,
|
|
73
|
+
bestSet: {
|
|
74
|
+
setNumber: bestSetNumber,
|
|
75
|
+
score: bestSetScore,
|
|
76
|
+
reason: bestSetReason,
|
|
77
|
+
},
|
|
78
|
+
muscleActivation: calculateMuscleActivation(exercise, sets),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// Calculate totals
|
|
83
|
+
totalScore = exerciseSummaries.reduce((sum, ex) => sum + ex.totalScore, 0);
|
|
84
|
+
totalCalories = exerciseSummaries.reduce((sum, ex) => sum + ex.totalCalories, 0);
|
|
85
|
+
totalEpocCalories = exerciseSummaries.reduce((sum, ex) => sum +
|
|
86
|
+
ex.sets.reduce((setSum, set) => setSum + set.scoreBreakdown.epocCalories, 0), 0);
|
|
87
|
+
// Calculate muscle recovery
|
|
88
|
+
const muscleRecovery = calculateMuscleRecovery(muscleStressMap, exercises);
|
|
89
|
+
// Determine overall fatigue level
|
|
90
|
+
const averageScore = totalSets > 0 ? totalScore / totalSets : 0;
|
|
91
|
+
const fatigueLevel = getFatigueLevel(averageScore, totalScore, workoutDuration);
|
|
92
|
+
const recommendedRestDays = getRecommendedRestDays(fatigueLevel, muscleRecovery);
|
|
93
|
+
return {
|
|
94
|
+
totalScore: Math.round(totalScore),
|
|
95
|
+
averageScore: Math.round(averageScore),
|
|
96
|
+
totalCalories: Math.round(totalCalories),
|
|
97
|
+
totalEpocCalories: Math.round(totalEpocCalories),
|
|
98
|
+
workoutDuration: Math.round(workoutDuration),
|
|
99
|
+
exerciseSummaries,
|
|
100
|
+
muscleRecovery,
|
|
101
|
+
fatigueLevel,
|
|
102
|
+
recommendedRestDays,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
exports.calculateWorkoutSummary = calculateWorkoutSummary;
|
|
106
|
+
/**
|
|
107
|
+
* Generate human-readable set description
|
|
108
|
+
*/
|
|
109
|
+
const generateSetDescription = (record, exercise, scoreBreakdown) => {
|
|
110
|
+
const effort = record.rpe
|
|
111
|
+
? `RPE ${record.rpe}`
|
|
112
|
+
: record.rir
|
|
113
|
+
? `${record.rir} RIR`
|
|
114
|
+
: "";
|
|
115
|
+
const calories = `${Math.round(scoreBreakdown.caloriesBurned)} cal`;
|
|
116
|
+
switch (record.type) {
|
|
117
|
+
case "weight-reps":
|
|
118
|
+
return `${record.kg}kg × ${record.reps} reps ${effort} • ${calories}`;
|
|
119
|
+
case "reps-only":
|
|
120
|
+
const auxWeight = record.auxWeightKg ? ` (+${record.auxWeightKg}kg)` : "";
|
|
121
|
+
return `${record.reps} reps${auxWeight} ${effort} • ${calories}`;
|
|
122
|
+
case "duration":
|
|
123
|
+
const auxWeightDur = record.auxWeightKg
|
|
124
|
+
? ` (+${record.auxWeightKg}kg)`
|
|
125
|
+
: "";
|
|
126
|
+
return `${record.durationSecs}${auxWeightDur} ${effort} • ${calories}`;
|
|
127
|
+
case "distance":
|
|
128
|
+
let pace = "";
|
|
129
|
+
if (record.avgPaceSecsPerKm) {
|
|
130
|
+
const minutes = Math.floor(record.avgPaceSecsPerKm / 60);
|
|
131
|
+
const seconds = (record.avgPaceSecsPerKm % 60).toFixed(0);
|
|
132
|
+
const secondsFormatted = seconds.length === 1 ? "0" + seconds : seconds;
|
|
133
|
+
pace = ` (${minutes}:${secondsFormatted}/km)`;
|
|
134
|
+
}
|
|
135
|
+
return `${record.distanceKm}km in ${record.durationSecs}${pace} ${effort} • ${calories}`;
|
|
136
|
+
default:
|
|
137
|
+
return `${effort} • ${calories}`;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Determine reason for best set
|
|
142
|
+
*/
|
|
143
|
+
const getBestSetReason = (record, exercise) => {
|
|
144
|
+
switch (record.type) {
|
|
145
|
+
case "weight-reps":
|
|
146
|
+
return "heaviest weight";
|
|
147
|
+
case "reps-only":
|
|
148
|
+
return "most reps";
|
|
149
|
+
case "duration":
|
|
150
|
+
return "longest hold";
|
|
151
|
+
case "distance":
|
|
152
|
+
return "fastest pace";
|
|
153
|
+
default:
|
|
154
|
+
return "highest effort";
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Calculate muscle activation levels
|
|
159
|
+
*/
|
|
160
|
+
const calculateMuscleActivation = (exercise, sets) => {
|
|
161
|
+
if (sets.length === 0)
|
|
162
|
+
return { primary: 0, secondary: 0 };
|
|
163
|
+
const avgScore = sets.reduce((sum, set) => sum + set.scoreBreakdown.finalScore, 0) /
|
|
164
|
+
sets.length;
|
|
165
|
+
const maxPossibleScore = 100; // Reference point
|
|
166
|
+
// Activation based on exercise difficulty and average set score
|
|
167
|
+
const baseActivation = Math.min(100, (avgScore / maxPossibleScore) * 100);
|
|
168
|
+
const difficultyBonus = exercise.difficultyLevel * 2; // Up to 20% bonus
|
|
169
|
+
return {
|
|
170
|
+
primary: Math.min(100, Math.round(baseActivation + difficultyBonus)),
|
|
171
|
+
secondary: Math.min(100, Math.round((baseActivation + difficultyBonus) * 0.7)), // 70% of primary
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Calculate muscle recovery times
|
|
176
|
+
*/
|
|
177
|
+
const calculateMuscleRecovery = (muscleStressMap, exercises) => {
|
|
178
|
+
const recovery = {};
|
|
179
|
+
muscleStressMap.forEach((totalStress, muscle) => {
|
|
180
|
+
// Base recovery time based on muscle group
|
|
181
|
+
const baseRecovery = getMuscleBaseRecovery(muscle);
|
|
182
|
+
// Stress level (0-100)
|
|
183
|
+
const fatigueLevel = Math.min(100, (totalStress / 200) * 100); // 200 = very high stress threshold
|
|
184
|
+
// Recovery time increases with fatigue level
|
|
185
|
+
let recoveryHours = baseRecovery;
|
|
186
|
+
if (fatigueLevel > 80)
|
|
187
|
+
recoveryHours *= 1.5; // 50% longer for high fatigue
|
|
188
|
+
else if (fatigueLevel > 60)
|
|
189
|
+
recoveryHours *= 1.3; // 30% longer for moderate-high fatigue
|
|
190
|
+
else if (fatigueLevel > 40)
|
|
191
|
+
recoveryHours *= 1.1; // 10% longer for moderate fatigue
|
|
192
|
+
// Find exercises that worked this muscle - using indexOf instead of includes
|
|
193
|
+
const involvedExercises = exercises
|
|
194
|
+
.filter(({ exercise }) => exercise.primaryMuscles.indexOf(muscle) !==
|
|
195
|
+
-1 ||
|
|
196
|
+
exercise.secondaryMuscles.indexOf(muscle) !==
|
|
197
|
+
-1)
|
|
198
|
+
.map(({ exercise }) => exercise.name);
|
|
199
|
+
recovery[muscle] = {
|
|
200
|
+
fatigueLevel: Math.round(fatigueLevel),
|
|
201
|
+
recoveryHours: Math.round(recoveryHours),
|
|
202
|
+
trainingStress: Math.round(totalStress),
|
|
203
|
+
exercises: involvedExercises,
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
return recovery;
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Get base recovery time for different muscle groups
|
|
210
|
+
*/
|
|
211
|
+
const getMuscleBaseRecovery = (muscle) => {
|
|
212
|
+
// Recovery times in hours based on muscle size and recovery capacity
|
|
213
|
+
const recoveryTimes = {
|
|
214
|
+
"pectoralis-major": 48,
|
|
215
|
+
"pectoralis-minor": 48,
|
|
216
|
+
"latissimus-dorsi": 48,
|
|
217
|
+
trapezius: 36,
|
|
218
|
+
rhomboids: 36,
|
|
219
|
+
"deltoids-anterior": 36,
|
|
220
|
+
"deltoids-middle": 36,
|
|
221
|
+
"bicep-short-inner": 24,
|
|
222
|
+
"bicep-long-outer": 24,
|
|
223
|
+
"tricep-brachii-long": 24,
|
|
224
|
+
"tricep-brachii-lateral": 24,
|
|
225
|
+
quadriceps: 48,
|
|
226
|
+
hamstrings: 48,
|
|
227
|
+
"maximus-lower": 48,
|
|
228
|
+
"medius-upper": 36,
|
|
229
|
+
"calf-inner": 24,
|
|
230
|
+
"calf-outer": 24,
|
|
231
|
+
"abs-upper": 24,
|
|
232
|
+
"abs-lower": 24,
|
|
233
|
+
obliques: 24,
|
|
234
|
+
"lower-back": 48,
|
|
235
|
+
"fore-arm-inner": 18,
|
|
236
|
+
"fore-arm-outer": 18,
|
|
237
|
+
};
|
|
238
|
+
return recoveryTimes[muscle] || 36; // Default 36 hours
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Determine overall fatigue level
|
|
242
|
+
*/
|
|
243
|
+
const getFatigueLevel = (averageScore, totalScore, workoutDuration) => {
|
|
244
|
+
const intensity = averageScore;
|
|
245
|
+
const volume = totalScore / 100; // Normalize volume
|
|
246
|
+
const density = totalScore / workoutDuration; // Score per minute
|
|
247
|
+
if (intensity > 80 || volume > 15 || density > 8)
|
|
248
|
+
return "extreme";
|
|
249
|
+
if (intensity > 65 || volume > 12 || density > 6)
|
|
250
|
+
return "very_hard";
|
|
251
|
+
if (intensity > 50 || volume > 8 || density > 4)
|
|
252
|
+
return "hard";
|
|
253
|
+
if (intensity > 35 || volume > 5 || density > 2)
|
|
254
|
+
return "moderate";
|
|
255
|
+
return "light";
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Get recommended rest days based on fatigue
|
|
259
|
+
*/
|
|
260
|
+
const getRecommendedRestDays = (fatigueLevel, muscleRecovery) => {
|
|
261
|
+
// Base rest days by fatigue level
|
|
262
|
+
const baseDays = {
|
|
263
|
+
light: 0,
|
|
264
|
+
moderate: 1,
|
|
265
|
+
hard: 1,
|
|
266
|
+
very_hard: 2,
|
|
267
|
+
extreme: 3,
|
|
268
|
+
};
|
|
269
|
+
const restDays = baseDays[fatigueLevel];
|
|
270
|
+
// Consider muscle recovery times - using manual iteration instead of Object.values
|
|
271
|
+
const recoveryHours = [];
|
|
272
|
+
for (const muscle in muscleRecovery) {
|
|
273
|
+
if (muscleRecovery.hasOwnProperty(muscle)) {
|
|
274
|
+
recoveryHours.push(muscleRecovery[muscle].recoveryHours);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const maxRecoveryHours = recoveryHours.length > 0 ? Math.max.apply(Math, recoveryHours) : 0;
|
|
278
|
+
const muscleBasedDays = Math.ceil(maxRecoveryHours / 24);
|
|
279
|
+
return Math.max(restDays, muscleBasedDays);
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Generate user-friendly workout summary text
|
|
283
|
+
*/
|
|
284
|
+
const generateWorkoutSummaryText = (summary) => {
|
|
285
|
+
const { totalScore, averageScore, totalCalories, totalEpocCalories, workoutDuration, fatigueLevel, recommendedRestDays, exerciseSummaries, } = summary;
|
|
286
|
+
const intensityMap = {
|
|
287
|
+
light: "Light workout",
|
|
288
|
+
moderate: "Moderate workout",
|
|
289
|
+
hard: "Hard workout",
|
|
290
|
+
very_hard: "Very hard workout",
|
|
291
|
+
extreme: "Extreme workout",
|
|
292
|
+
};
|
|
293
|
+
const intensityText = intensityMap[fatigueLevel];
|
|
294
|
+
const restText = recommendedRestDays === 0
|
|
295
|
+
? "You can train again tomorrow"
|
|
296
|
+
: `Recommended rest: ${recommendedRestDays} day${recommendedRestDays > 1 ? "s" : ""}`;
|
|
297
|
+
return `
|
|
298
|
+
🏋️ **${intensityText}** (Score: ${totalScore})
|
|
299
|
+
⏱️ Duration: ${workoutDuration} minutes
|
|
300
|
+
🔥 Calories: ${totalCalories} + ${totalEpocCalories} afterburn
|
|
301
|
+
📊 Average set intensity: ${averageScore}/100
|
|
302
|
+
|
|
303
|
+
💪 **Exercises completed:**
|
|
304
|
+
${exerciseSummaries
|
|
305
|
+
.map((ex) => `• ${ex.exercise.name}: ${ex.sets.length} sets (${Math.round(ex.totalScore)} pts)`)
|
|
306
|
+
.join("\n")}
|
|
307
|
+
|
|
308
|
+
🛌 **Recovery:** ${restText}
|
|
309
|
+
`.trim();
|
|
310
|
+
};
|
|
311
|
+
exports.generateWorkoutSummaryText = generateWorkoutSummaryText;
|