@dgpholdings/greatoak-shared 1.1.35 → 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/TApiExerciseRecord.d.ts +3 -2
- 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/record.utils.d.ts +9 -2
- package/dist/utils/record.utils.js +14 -3
- package/dist/utils/scoring.utils.d.ts +16 -12
- package/dist/utils/scoring.utils.js +339 -73
- 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;
|
|
@@ -2,7 +2,8 @@ export type TRecord = {
|
|
|
2
2
|
isDone: boolean;
|
|
3
3
|
rpe?: string;
|
|
4
4
|
setNote?: string;
|
|
5
|
-
|
|
5
|
+
workDurationSecs?: number;
|
|
6
|
+
restDurationSecs?: number;
|
|
6
7
|
} & ({
|
|
7
8
|
type: "weight-reps";
|
|
8
9
|
kg: string;
|
|
@@ -41,7 +42,7 @@ export type TRecordConfig = {
|
|
|
41
42
|
enableRpe?: boolean;
|
|
42
43
|
enableSetNote?: boolean;
|
|
43
44
|
enableExerciseNote?: boolean;
|
|
44
|
-
|
|
45
|
+
enableTimeIntervalMode?: boolean;
|
|
45
46
|
};
|
|
46
47
|
export type TApiExerciseRecordUpdateReq = {
|
|
47
48
|
templateId: 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; } });
|
|
@@ -3,7 +3,8 @@ type RefinedBase = {
|
|
|
3
3
|
isDone: boolean;
|
|
4
4
|
rpe?: number;
|
|
5
5
|
rir?: number;
|
|
6
|
-
|
|
6
|
+
workDurationSecs?: number;
|
|
7
|
+
restDurationSecs?: number;
|
|
7
8
|
setNote?: string;
|
|
8
9
|
};
|
|
9
10
|
export type TRefinedWeightRecord = RefinedBase & {
|
|
@@ -21,6 +22,12 @@ export type TRefinedBodyWeightRecord = RefinedBase & {
|
|
|
21
22
|
reps: number;
|
|
22
23
|
auxWeightKg?: number;
|
|
23
24
|
};
|
|
24
|
-
export type
|
|
25
|
+
export type TRefinedDistanceRecord = RefinedBase & {
|
|
26
|
+
type: "distance";
|
|
27
|
+
distanceKm: number;
|
|
28
|
+
durationSecs: string;
|
|
29
|
+
avgPaceSecsPerKm?: number;
|
|
30
|
+
};
|
|
31
|
+
export type TRefinedRecord = TRefinedWeightRecord | TRefinedDurationRecord | TRefinedBodyWeightRecord | TRefinedDistanceRecord;
|
|
25
32
|
export declare const refineRecordEntry: (entry: TRecord) => TRefinedRecord;
|
|
26
33
|
export {};
|
|
@@ -4,9 +4,11 @@ exports.refineRecordEntry = void 0;
|
|
|
4
4
|
const isDefined_utils_1 = require("./isDefined.utils");
|
|
5
5
|
const number_util_1 = require("./number.util");
|
|
6
6
|
const refineRecordEntry = (entry) => {
|
|
7
|
-
var _a, _b, _c;
|
|
8
|
-
const base = Object.assign(Object.assign(Object.assign({ isDone: entry.isDone }, ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rpe)) && { rpe: (0, number_util_1.toNumber)(entry.rpe) })), ((0, isDefined_utils_1.isDefinedNumber)(
|
|
9
|
-
|
|
7
|
+
var _a, _b, _c, _d, _e;
|
|
8
|
+
const base = Object.assign(Object.assign(Object.assign(Object.assign({ isDone: entry.isDone }, ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rpe)) && { rpe: (0, number_util_1.toNumber)(entry.rpe) })), ((0, isDefined_utils_1.isDefinedNumber)(entry.workDurationSecs) && {
|
|
9
|
+
workDurationSecs: entry.workDurationSecs,
|
|
10
|
+
})), ((0, isDefined_utils_1.isDefinedNumber)(entry.restDurationSecs) && {
|
|
11
|
+
restDurationSecs: entry.restDurationSecs,
|
|
10
12
|
})), ((0, isDefined_utils_1.isDefined)(entry.setNote) && { setNote: entry.setNote }));
|
|
11
13
|
if (entry.type === "weight-reps") {
|
|
12
14
|
return Object.assign(Object.assign({}, base), { type: "weight-reps", kg: (_a = (0, number_util_1.toNumber)(entry.kg)) !== null && _a !== void 0 ? _a : 0, rir: (0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rir))
|
|
@@ -25,6 +27,15 @@ const refineRecordEntry = (entry) => {
|
|
|
25
27
|
auxWeightKg: (0, number_util_1.toNumber)(entry.auxWeightKg),
|
|
26
28
|
}));
|
|
27
29
|
}
|
|
30
|
+
if (entry.type === "distance") {
|
|
31
|
+
const distanceKm = (_d = (0, number_util_1.toNumber)(entry.distanceKm)) !== null && _d !== void 0 ? _d : 0;
|
|
32
|
+
const durationSecs = (_e = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _e !== void 0 ? _e : 0;
|
|
33
|
+
// Calculate pace if both distance and duration are provided
|
|
34
|
+
const avgPaceSecsPerKm = distanceKm > 0 && durationSecs > 0
|
|
35
|
+
? durationSecs / distanceKm
|
|
36
|
+
: undefined;
|
|
37
|
+
return Object.assign(Object.assign(Object.assign({}, base), { type: "distance", distanceKm, durationSecs: entry.durationSecs }), ((0, isDefined_utils_1.isDefinedNumber)(avgPaceSecsPerKm) && { avgPaceSecsPerKm }));
|
|
38
|
+
}
|
|
28
39
|
throw new Error(`Unknown record type: ${entry.type}`);
|
|
29
40
|
};
|
|
30
41
|
exports.refineRecordEntry = refineRecordEntry;
|
|
@@ -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,20 +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
|
-
|
|
21
|
-
|
|
29
|
+
avgRestDurationSecs?: number;
|
|
30
|
+
isTimeIntervalModeEnabled?: boolean;
|
|
22
31
|
};
|
|
23
32
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* @param record - A refined workout set record
|
|
27
|
-
* @param userProfile - Optional user demographics (age, gender, weight)
|
|
28
|
-
* @param avgRestInBetweenDurationSecs - Average rest between sets in session
|
|
29
|
-
* @returns An object with baseScore, applied boosts, penalties, and final score
|
|
33
|
+
* Enhanced training stress score computation using metabolic data
|
|
30
34
|
*/
|
|
31
|
-
export declare const computeScoreFromRecord: ({
|
|
35
|
+
export declare const computeScoreFromRecord: ({ avgRestDurationSecs, record, exercise, userProfile, isTimeIntervalModeEnabled, }: TParams) => TScoreBreakdown;
|
|
32
36
|
export {};
|
|
@@ -3,102 +3,368 @@ 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
|
-
*
|
|
8
|
-
* @param record - A refined workout set record
|
|
9
|
-
* @param userProfile - Optional user demographics (age, gender, weight)
|
|
10
|
-
* @param avgRestInBetweenDurationSecs - Average rest between sets in session
|
|
11
|
-
* @returns An object with baseScore, applied boosts, penalties, and final score
|
|
6
|
+
* Calculate BMR using Mifflin-St Jeor Equation with body composition adjustment
|
|
12
7
|
*/
|
|
13
|
-
const
|
|
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;
|
|
16
|
+
}
|
|
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) => {
|
|
14
32
|
var _a, _b, _c, _d, _e;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
44
|
+
switch (record.type) {
|
|
45
|
+
case "weight-reps": {
|
|
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;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
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
|
|
69
|
+
}
|
|
70
|
+
else if (userWeight < 60) {
|
|
71
|
+
calculatedMET *= 0.95; // Lighter people have slight advantage
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
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
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "distance": {
|
|
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) {
|
|
93
|
+
const speedKmh = (distance * 3600) / durationSecs;
|
|
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;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
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;
|
|
22
146
|
}
|
|
23
|
-
|
|
24
|
-
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Calculate exercise duration in minutes
|
|
150
|
+
*/
|
|
151
|
+
const getExerciseDurationMinutes = (record, exercise) => {
|
|
152
|
+
var _a, _b;
|
|
153
|
+
let durationSecs = 0;
|
|
25
154
|
switch (record.type) {
|
|
26
155
|
case "weight-reps":
|
|
27
|
-
|
|
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
|
+
}
|
|
28
166
|
break;
|
|
167
|
+
}
|
|
29
168
|
case "duration": {
|
|
30
|
-
|
|
31
|
-
const auxWeight = (_b = record.auxWeightKg) !== null && _b !== void 0 ? _b : 0;
|
|
32
|
-
load = auxWeight > 0 ? auxWeight * durationSecs : durationSecs;
|
|
169
|
+
durationSecs = (0, time_util_1.mmssToSecs)((_a = record.durationSecs) !== null && _a !== void 0 ? _a : "00:00");
|
|
33
170
|
break;
|
|
34
171
|
}
|
|
35
|
-
case "
|
|
36
|
-
|
|
37
|
-
const auxWeight = (_d = record.auxWeightKg) !== null && _d !== void 0 ? _d : 0;
|
|
38
|
-
const baseWeight = auxWeight > 0 ? auxWeight : (_e = userProfile === null || userProfile === void 0 ? void 0 : userProfile.weightKg) !== null && _e !== void 0 ? _e : 0;
|
|
39
|
-
load = baseWeight * reps;
|
|
172
|
+
case "distance": {
|
|
173
|
+
durationSecs = (0, time_util_1.mmssToSecs)((_b = record.durationSecs) !== null && _b !== void 0 ? _b : "00:00");
|
|
40
174
|
break;
|
|
41
175
|
}
|
|
42
176
|
}
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
effortFactor = record.rpe;
|
|
177
|
+
// Add rest time if available (but not for distance exercises)
|
|
178
|
+
if (record.type !== "distance" && record.restDurationSecs) {
|
|
179
|
+
durationSecs += record.restDurationSecs;
|
|
47
180
|
}
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
};
|
|
50
220
|
}
|
|
51
|
-
// Step 3: Base score using log scale
|
|
52
|
-
const base = Math.log10(1 + Math.max(0, load));
|
|
53
221
|
const plus = [];
|
|
54
222
|
const minus = [];
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const effortBoost = 1 + boundedEffortFactor * 0.05;
|
|
60
|
-
rawScore *= effortBoost;
|
|
61
|
-
plus.push({ effortBoost: parseFloat((effortBoost - 1).toFixed(10)) });
|
|
62
|
-
// Step 5: User Profile Scaling
|
|
63
|
-
let adjustment = 1;
|
|
64
|
-
if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.gender) === "female")
|
|
65
|
-
adjustment *= 1.1;
|
|
66
|
-
if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.age) && userProfile.age > 50)
|
|
67
|
-
adjustment *= 1.15;
|
|
68
|
-
if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.weightKg) && userProfile.weightKg > 0) {
|
|
69
|
-
const normalizedWeight = 70;
|
|
70
|
-
adjustment *= normalizedWeight / userProfile.weightKg;
|
|
71
|
-
}
|
|
72
|
-
if (adjustment !== 1) {
|
|
73
|
-
rawScore *= adjustment;
|
|
74
|
-
plus.push({ profileAdjustment: parseFloat((adjustment - 1).toFixed(10)) });
|
|
75
|
-
}
|
|
76
|
-
// Step 6: Rest Penalty
|
|
77
|
-
if (typeof record.restInBetweenDurationSecs === "number" &&
|
|
78
|
-
typeof avgRestInBetweenDurationSecs === "number") {
|
|
79
|
-
const restUsed = record.restInBetweenDurationSecs;
|
|
80
|
-
const grace = 5 + effortFactor * 2;
|
|
81
|
-
const cap = grace + 25;
|
|
82
|
-
const restOver = restUsed - avgRestInBetweenDurationSecs;
|
|
83
|
-
if (restOver > grace) {
|
|
84
|
-
const cappedExcess = Math.min(restOver, cap);
|
|
85
|
-
const penaltyRatio = (cappedExcess - grace) / (cap - grace); // 0–1
|
|
86
|
-
const restPenalty = parseFloat((penaltyRatio * 0.2 * rawScore).toFixed(10));
|
|
87
|
-
rawScore -= restPenalty;
|
|
88
|
-
minus.push({ restPenalty });
|
|
89
|
-
}
|
|
223
|
+
// Calculate effort factor from RPE/RIR
|
|
224
|
+
let effortFactor = 5; // Default moderate effort
|
|
225
|
+
if (typeof record.rpe === "number") {
|
|
226
|
+
effortFactor = Math.max(1, Math.min(10, record.rpe));
|
|
90
227
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
228
|
+
else if (typeof record.rir === "number") {
|
|
229
|
+
effortFactor = Math.max(1, Math.min(10, 10 - record.rir));
|
|
230
|
+
}
|
|
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;
|
|
94
244
|
plus.push({
|
|
95
|
-
|
|
245
|
+
exerciseDifficulty: parseFloat((difficultyMultiplier - 1).toFixed(3)),
|
|
96
246
|
});
|
|
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
|
+
}
|
|
273
|
+
}
|
|
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)
|
|
288
|
+
if (isTimeIntervalModeEnabled) {
|
|
289
|
+
const intervalScoreBonus = 1.08;
|
|
290
|
+
workingScore *= intervalScoreBonus;
|
|
291
|
+
plus.push({
|
|
292
|
+
intervalTraining: parseFloat((intervalScoreBonus - 1).toFixed(3)),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// User profile adjustments
|
|
296
|
+
let profileMultiplier = 1;
|
|
297
|
+
if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.gender) === "female") {
|
|
298
|
+
profileMultiplier *= 1.08; // Reduced from 1.12 since metabolic calculations are more accurate
|
|
299
|
+
}
|
|
300
|
+
if (userProfile === null || userProfile === void 0 ? void 0 : userProfile.age) {
|
|
301
|
+
if (userProfile.age > 50)
|
|
302
|
+
profileMultiplier *= 1.06;
|
|
303
|
+
if (userProfile.age > 65)
|
|
304
|
+
profileMultiplier *= 1.1;
|
|
305
|
+
}
|
|
306
|
+
if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.weightKg) && userProfile.weightKg > 0) {
|
|
307
|
+
const weightAdjustment = 70 / userProfile.weightKg;
|
|
308
|
+
profileMultiplier *= Math.max(0.85, Math.min(1.25, weightAdjustment));
|
|
309
|
+
}
|
|
310
|
+
if (profileMultiplier !== 1) {
|
|
311
|
+
workingScore *= profileMultiplier;
|
|
312
|
+
plus.push({
|
|
313
|
+
profileAdjustment: parseFloat((profileMultiplier - 1).toFixed(3)),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
// Enhanced rest efficiency penalty
|
|
317
|
+
if (typeof record.restDurationSecs === "number" &&
|
|
318
|
+
typeof avgRestDurationSecs === "number") {
|
|
319
|
+
const restUsed = record.restDurationSecs;
|
|
320
|
+
const baseGrace = 8 + exercise.difficultyLevel * 1.5; // More grace for complex exercises
|
|
321
|
+
const effortGrace = effortFactor * 2.5;
|
|
322
|
+
const totalGrace = baseGrace + effortGrace;
|
|
323
|
+
const maxPenaltyThreshold = totalGrace + 45;
|
|
324
|
+
const excessRest = restUsed - avgRestDurationSecs;
|
|
325
|
+
if (excessRest > totalGrace) {
|
|
326
|
+
const penaltyExcess = Math.min(excessRest - totalGrace, maxPenaltyThreshold - totalGrace);
|
|
327
|
+
const penaltyRatio = penaltyExcess / (maxPenaltyThreshold - totalGrace);
|
|
328
|
+
const restPenalty = penaltyRatio * 0.12 * workingScore; // Reduced penalty
|
|
329
|
+
workingScore -= restPenalty;
|
|
330
|
+
minus.push({ excessRestPenalty: parseFloat(restPenalty.toFixed(3)) });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Work duration adherence for interval training
|
|
334
|
+
if (isTimeIntervalModeEnabled && record.workDurationSecs) {
|
|
335
|
+
let actualDuration = 0;
|
|
336
|
+
if (record.type === "duration") {
|
|
337
|
+
actualDuration = (0, time_util_1.mmssToSecs)((_a = record.durationSecs) !== null && _a !== void 0 ? _a : "00:00");
|
|
338
|
+
}
|
|
339
|
+
else if (record.type === "distance") {
|
|
340
|
+
actualDuration = (0, time_util_1.mmssToSecs)((_b = record.durationSecs) !== null && _b !== void 0 ? _b : "00:00");
|
|
341
|
+
}
|
|
342
|
+
if (actualDuration > 0) {
|
|
343
|
+
const expectedDuration = exercise.avgSetDurationInSecs || record.workDurationSecs;
|
|
344
|
+
const durationDiff = Math.abs(actualDuration - expectedDuration);
|
|
345
|
+
const tolerance = expectedDuration * 0.25; // 25% tolerance
|
|
346
|
+
if (durationDiff > tolerance) {
|
|
347
|
+
const excessPercent = (durationDiff - tolerance) / expectedDuration;
|
|
348
|
+
const timingPenalty = Math.min(0.08 * workingScore, excessPercent * 0.05 * workingScore);
|
|
349
|
+
workingScore -= timingPenalty;
|
|
350
|
+
minus.push({
|
|
351
|
+
timingDeviationPenalty: parseFloat(timingPenalty.toFixed(3)),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Final score normalization
|
|
357
|
+
let finalScore = Math.max(0, workingScore);
|
|
358
|
+
finalScore *= 12; // Scale to intuitive range (0-120+ for typical exercises)
|
|
359
|
+
finalScore = Math.min(200, finalScore); // Cap extreme scores
|
|
97
360
|
return {
|
|
98
361
|
baseScore: parseFloat(base.toFixed(2)),
|
|
99
362
|
plus,
|
|
100
363
|
minus,
|
|
101
|
-
finalScore,
|
|
364
|
+
finalScore: parseFloat(finalScore.toFixed(1)),
|
|
365
|
+
caloriesBurned: calorieData.totalCalories,
|
|
366
|
+
metValue,
|
|
367
|
+
epocCalories: calorieData.epocCalories,
|
|
102
368
|
};
|
|
103
369
|
};
|
|
104
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;
|