@dgpholdings/greatoak-shared 1.1.49 → 1.1.51
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 +3 -2
- package/dist/typeGuards/index.js +5 -3
- package/dist/types/TApiExercise.d.ts +0 -7
- package/dist/types/TApiUser.d.ts +2 -3
- package/dist/types/commonTypes.d.ts +11 -4
- package/dist/utils/record.utils.d.ts +15 -7
- package/dist/utils/record.utils.js +26 -18
- package/dist/utils/scoring.utils.d.ts +2 -2
- package/dist/utils/scoring.utils.js +79 -24
- package/dist/utils/workoutSummary.util.js +61 -19
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { TRecord, TRecordWeight, TRecordDuration, TRecordBodyWeight,
|
|
1
|
+
import { TRecord, TRecordWeight, TRecordDuration, TRecordBodyWeight, TRecordCardioFree, TRecordCardioMachine } from "../types";
|
|
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
|
|
5
|
+
export declare const isRecordCardioFreeTypeGuard: (param: TRecord[]) => param is TRecordCardioFree[];
|
|
6
|
+
export declare const isRecordCardioMachineTypeGuard: (param: TRecord[]) => param is TRecordCardioMachine[];
|
package/dist/typeGuards/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.isRecordCardioMachineTypeGuard = exports.isRecordCardioFreeTypeGuard = 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
|
|
11
|
-
exports.
|
|
10
|
+
const isRecordCardioFreeTypeGuard = (param) => param[0].type === "cardio-free";
|
|
11
|
+
exports.isRecordCardioFreeTypeGuard = isRecordCardioFreeTypeGuard;
|
|
12
|
+
const isRecordCardioMachineTypeGuard = (param) => param[0].type === "cardio-machine";
|
|
13
|
+
exports.isRecordCardioMachineTypeGuard = isRecordCardioMachineTypeGuard;
|
|
@@ -61,13 +61,6 @@ export type TTimingGuardrails = TBaseTimingGuardrails & ({
|
|
|
61
61
|
max: number;
|
|
62
62
|
typical: number;
|
|
63
63
|
};
|
|
64
|
-
} | {
|
|
65
|
-
type: "distance";
|
|
66
|
-
pacing: {
|
|
67
|
-
minPacePerUnit: number;
|
|
68
|
-
maxPacePerUnit: number;
|
|
69
|
-
typicalPacePerUnit: number;
|
|
70
|
-
};
|
|
71
64
|
});
|
|
72
65
|
export type TExercise = {
|
|
73
66
|
exerciseId: string;
|
package/dist/types/TApiUser.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export type TAuthType = "email" | "apple" | "anonymous";
|
|
2
2
|
export type TGender = "male" | "female" | "unmentioned";
|
|
3
3
|
export type TFitnessGoal = "strength" | "hypertrophy" | "endurance" | "general" | "fat_burn" | "flexibility";
|
|
4
|
-
export type TUserFitnessGoal = "strength" | "hypertrophy" | "endurance" | "general";
|
|
5
4
|
export type TUserMetric = {
|
|
6
5
|
dob: Date;
|
|
7
6
|
weightKg?: number;
|
|
@@ -39,7 +38,6 @@ export type TUser = {
|
|
|
39
38
|
billingPlanType: "monthly" | "annually" | "free";
|
|
40
39
|
billingPlanVersion?: string;
|
|
41
40
|
billingPlanGeoCountryCode: string;
|
|
42
|
-
fitnessLevel?: number;
|
|
43
41
|
paymentMethod?: "credit_card" | "paypal" | "apple_pay" | "upi";
|
|
44
42
|
billingPlanNextDueDate?: string;
|
|
45
43
|
devices: {
|
|
@@ -54,5 +52,6 @@ export type TUser = {
|
|
|
54
52
|
weightKg?: number;
|
|
55
53
|
heightCm?: number;
|
|
56
54
|
bodyFatPercentage?: number;
|
|
57
|
-
fitnessGoal:
|
|
55
|
+
fitnessGoal: TFitnessGoal;
|
|
56
|
+
fitnessLevel: number;
|
|
58
57
|
};
|
|
@@ -20,9 +20,13 @@ export type TRecord = {
|
|
|
20
20
|
auxWeightKg: string;
|
|
21
21
|
rir?: string;
|
|
22
22
|
} | {
|
|
23
|
-
type: "
|
|
23
|
+
type: "cardio-machine";
|
|
24
|
+
speed: string;
|
|
25
|
+
resistance: string;
|
|
26
|
+
durationSecs: string;
|
|
27
|
+
} | {
|
|
28
|
+
type: "cardio-free";
|
|
24
29
|
distanceKm: string;
|
|
25
|
-
auxWeightKg: string;
|
|
26
30
|
durationSecs: string;
|
|
27
31
|
});
|
|
28
32
|
export type TRecordDuration = Extract<TRecord, {
|
|
@@ -34,8 +38,11 @@ export type TRecordWeight = Extract<TRecord, {
|
|
|
34
38
|
export type TRecordBodyWeight = Extract<TRecord, {
|
|
35
39
|
type: "reps-only";
|
|
36
40
|
}>;
|
|
37
|
-
export type
|
|
38
|
-
type: "
|
|
41
|
+
export type TRecordCardioFree = Extract<TRecord, {
|
|
42
|
+
type: "cardio-free";
|
|
43
|
+
}>;
|
|
44
|
+
export type TRecordCardioMachine = Extract<TRecord, {
|
|
45
|
+
type: "cardio-machine";
|
|
39
46
|
}>;
|
|
40
47
|
export type TTemplateConfig = {
|
|
41
48
|
enableAuxWeight?: boolean;
|
|
@@ -2,7 +2,6 @@ import { TRecord } from "../types";
|
|
|
2
2
|
type RefinedBase = {
|
|
3
3
|
isDone: boolean;
|
|
4
4
|
rpe?: number;
|
|
5
|
-
rir?: number;
|
|
6
5
|
workDurationSecs?: number;
|
|
7
6
|
restDurationSecs?: number;
|
|
8
7
|
setNote?: string;
|
|
@@ -12,24 +11,33 @@ export type TRefinedWeightRecord = RefinedBase & {
|
|
|
12
11
|
type: "weight-reps";
|
|
13
12
|
kg: number;
|
|
14
13
|
reps: number;
|
|
14
|
+
rir?: number;
|
|
15
15
|
};
|
|
16
16
|
export type TRefinedDurationRecord = RefinedBase & {
|
|
17
17
|
type: "duration";
|
|
18
18
|
durationSecs: number;
|
|
19
|
-
auxWeightKg
|
|
19
|
+
auxWeightKg: number;
|
|
20
20
|
};
|
|
21
21
|
export type TRefinedBodyWeightRecord = RefinedBase & {
|
|
22
22
|
type: "reps-only";
|
|
23
23
|
reps: number;
|
|
24
|
-
auxWeightKg
|
|
24
|
+
auxWeightKg: number;
|
|
25
|
+
rir?: number;
|
|
26
|
+
};
|
|
27
|
+
export type TRefinedCardioMachineRecord = RefinedBase & {
|
|
28
|
+
type: "cardio-machine";
|
|
29
|
+
speed: number;
|
|
30
|
+
resistance: number;
|
|
31
|
+
durationSecs: number;
|
|
32
|
+
intensityScore?: number;
|
|
25
33
|
};
|
|
26
|
-
export type
|
|
27
|
-
type: "
|
|
34
|
+
export type TRefinedCardioFreeRecord = RefinedBase & {
|
|
35
|
+
type: "cardio-free";
|
|
28
36
|
distanceKm: number;
|
|
29
|
-
auxWeightKg?: number;
|
|
30
37
|
durationSecs: number;
|
|
31
38
|
avgPaceSecsPerKm?: number;
|
|
39
|
+
avgSpeedKmh?: number;
|
|
32
40
|
};
|
|
33
|
-
export type TRefinedRecord = TRefinedWeightRecord | TRefinedDurationRecord | TRefinedBodyWeightRecord |
|
|
41
|
+
export type TRefinedRecord = TRefinedWeightRecord | TRefinedDurationRecord | TRefinedBodyWeightRecord | TRefinedCardioMachineRecord | TRefinedCardioFreeRecord;
|
|
34
42
|
export declare const refineRecordEntry: (entry: TRecord) => TRefinedRecord;
|
|
35
43
|
export {};
|
|
@@ -4,40 +4,48 @@ 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, _d, _e, _f;
|
|
7
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
8
8
|
const base = Object.assign(Object.assign(Object.assign(Object.assign({ isDone: entry.isDone, isStrictMode: entry.isStrictMode }, ((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
9
|
workDurationSecs: entry.workDurationSecs,
|
|
10
10
|
})), ((0, isDefined_utils_1.isDefinedNumber)(entry.restDurationSecs) && {
|
|
11
11
|
restDurationSecs: entry.restDurationSecs,
|
|
12
12
|
})), ((0, isDefined_utils_1.isDefined)(entry.setNote) && { setNote: entry.setNote }));
|
|
13
13
|
if (entry.type === "weight-reps") {
|
|
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))
|
|
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, reps: (_b = (0, number_util_1.toNumber)(entry.reps)) !== null && _b !== void 0 ? _b : 0, rir: (0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rir))
|
|
15
15
|
? (0, number_util_1.toNumber)(entry.rir)
|
|
16
|
-
: undefined
|
|
16
|
+
: undefined });
|
|
17
17
|
}
|
|
18
18
|
if (entry.type === "duration") {
|
|
19
|
-
return Object.assign(Object.assign(
|
|
20
|
-
auxWeightKg: (0, number_util_1.toNumber)(entry.auxWeightKg),
|
|
21
|
-
}));
|
|
19
|
+
return Object.assign(Object.assign({}, base), { type: "duration", durationSecs: (_c = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _c !== void 0 ? _c : 0, auxWeightKg: (_d = (0, number_util_1.toNumber)(entry.auxWeightKg)) !== null && _d !== void 0 ? _d : 0 });
|
|
22
20
|
}
|
|
23
21
|
if (entry.type === "reps-only") {
|
|
24
|
-
return Object.assign(Object.assign(
|
|
22
|
+
return Object.assign(Object.assign({}, base), { type: "reps-only", reps: (_e = (0, number_util_1.toNumber)(entry.reps)) !== null && _e !== void 0 ? _e : 0, auxWeightKg: (_f = (0, number_util_1.toNumber)(entry.auxWeightKg)) !== null && _f !== void 0 ? _f : 0, rir: (0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rir))
|
|
25
23
|
? (0, number_util_1.toNumber)(entry.rir)
|
|
26
|
-
: undefined
|
|
27
|
-
auxWeightKg: (0, number_util_1.toNumber)(entry.auxWeightKg),
|
|
28
|
-
}));
|
|
24
|
+
: undefined });
|
|
29
25
|
}
|
|
30
|
-
if (entry.type === "
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
26
|
+
if (entry.type === "cardio-machine") {
|
|
27
|
+
const speed = (_g = (0, number_util_1.toNumber)(entry.speed)) !== null && _g !== void 0 ? _g : 0;
|
|
28
|
+
const resistance = (_h = (0, number_util_1.toNumber)(entry.resistance)) !== null && _h !== void 0 ? _h : 0;
|
|
29
|
+
const durationSecs = (_j = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _j !== void 0 ? _j : 0;
|
|
30
|
+
// Calculate intensity score for progress tracking
|
|
31
|
+
const intensityScore = speed > 0 && resistance > 0 ? speed * resistance : undefined;
|
|
32
|
+
return Object.assign(Object.assign(Object.assign({}, base), { type: "cardio-machine", speed,
|
|
33
|
+
resistance,
|
|
34
|
+
durationSecs }), ((0, isDefined_utils_1.isDefinedNumber)(intensityScore) && { intensityScore }));
|
|
35
|
+
}
|
|
36
|
+
if (entry.type === "cardio-free") {
|
|
37
|
+
const distanceKm = (_k = (0, number_util_1.toNumber)(entry.distanceKm)) !== null && _k !== void 0 ? _k : 0;
|
|
38
|
+
const durationSecs = (_l = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _l !== void 0 ? _l : 0;
|
|
39
|
+
// Calculate pace (seconds per km)
|
|
34
40
|
const avgPaceSecsPerKm = distanceKm > 0 && durationSecs > 0
|
|
35
41
|
? durationSecs / distanceKm
|
|
36
42
|
: undefined;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
// Calculate average speed (km/h)
|
|
44
|
+
const avgSpeedKmh = distanceKm > 0 && durationSecs > 0
|
|
45
|
+
? distanceKm / (durationSecs / 3600)
|
|
46
|
+
: undefined;
|
|
47
|
+
return Object.assign(Object.assign(Object.assign(Object.assign({}, base), { type: "cardio-free", distanceKm,
|
|
48
|
+
durationSecs }), ((0, isDefined_utils_1.isDefinedNumber)(avgPaceSecsPerKm) && { avgPaceSecsPerKm })), ((0, isDefined_utils_1.isDefinedNumber)(avgSpeedKmh) && { avgSpeedKmh }));
|
|
41
49
|
}
|
|
42
50
|
throw new Error(`Unknown record type: ${entry.type}`);
|
|
43
51
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TRefinedRecord } from "./record.utils";
|
|
2
|
-
import { TExercise, TGender,
|
|
2
|
+
import { TExercise, TGender, TFitnessGoal, TUserMetric } from "../types";
|
|
3
3
|
export type TUserProfile = {
|
|
4
4
|
age?: number;
|
|
5
5
|
gender?: TGender;
|
|
@@ -7,7 +7,7 @@ export type TUserProfile = {
|
|
|
7
7
|
heightCm?: number;
|
|
8
8
|
bodyFatPercentage?: number;
|
|
9
9
|
fitnessLevel?: TUserMetric["fitnessLevel"];
|
|
10
|
-
fitnessGoal?:
|
|
10
|
+
fitnessGoal?: TFitnessGoal;
|
|
11
11
|
};
|
|
12
12
|
export type TScoreBreakdown = {
|
|
13
13
|
baseScore: number;
|
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.computeScoreFromRecord = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* GIST: Advanced Exercise Scoring Algorithm
|
|
6
|
+
*
|
|
7
|
+
* This function calculates a comprehensive score for a single exercise set using metabolic science,
|
|
8
|
+
* user personalization, and exercise characteristics. The algorithm:
|
|
9
|
+
*
|
|
10
|
+
* 🔬 SCIENTIFIC FOUNDATION:
|
|
11
|
+
* - Uses MET (Metabolic Equivalent) values for accurate calorie calculations
|
|
12
|
+
* - Incorporates BMR, exercise calories, and EPOC (post-exercise oxygen consumption)
|
|
13
|
+
* - Validates performance using timing guardrails and realistic human limits
|
|
14
|
+
*
|
|
15
|
+
* 👤 PERSONALIZATION:
|
|
16
|
+
* - Adjusts for user age, gender, weight, fitness level, and body composition
|
|
17
|
+
* - Provides goal-specific bonuses (strength, hypertrophy, endurance, fat burn, etc.)
|
|
18
|
+
* - Accounts for fitness level efficiency (trained athletes are more metabolically efficient)
|
|
19
|
+
*
|
|
20
|
+
* 🏋️ EXERCISE INTELLIGENCE:
|
|
21
|
+
* - Handles 5 record types: weight-reps, reps-only, duration, cardio-machine, cardio-free
|
|
22
|
+
* - Scales difficulty based on exercise complexity, compound movements, and muscle engagement
|
|
23
|
+
* - Rewards progressive overload, proper timing, and interval training
|
|
24
|
+
*
|
|
25
|
+
* 📊 SCORING SYSTEM:
|
|
26
|
+
* - Base score derived from calorie burn (more accurate than arbitrary load calculations)
|
|
27
|
+
* - Bonus system for goal alignment, high intensity, precision timing, stability demands
|
|
28
|
+
* - Penalty system for excessive rest, timing deviations (strict mode only)
|
|
29
|
+
* - Final score normalized to 0-200 range for intuitive user feedback
|
|
30
|
+
*
|
|
31
|
+
* The result motivates users through science-based, personalized feedback that encourages
|
|
32
|
+
* proper form, progressive overload, and goal-aligned training.
|
|
33
|
+
*/
|
|
4
34
|
/**
|
|
5
35
|
* Calculate BMR using Mifflin-St Jeor Equation with body composition adjustment
|
|
6
36
|
*/
|
|
@@ -28,7 +58,7 @@ const calculateBMR = (userProfile) => {
|
|
|
28
58
|
* Calculate precise MET value using exercise metabolic data
|
|
29
59
|
*/
|
|
30
60
|
const calculateMETValue = (record, exercise, effortFactor, userProfile) => {
|
|
31
|
-
var _a, _b
|
|
61
|
+
var _a, _b;
|
|
32
62
|
const { metabolicData } = exercise;
|
|
33
63
|
const userWeight = (_a = userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
|
|
34
64
|
const fitnessLevel = (_b = userProfile.fitnessLevel) !== null && _b !== void 0 ? _b : 3;
|
|
@@ -85,11 +115,27 @@ const calculateMETValue = (record, exercise, effortFactor, userProfile) => {
|
|
|
85
115
|
}
|
|
86
116
|
break;
|
|
87
117
|
}
|
|
88
|
-
case "
|
|
89
|
-
const
|
|
118
|
+
case "cardio-machine": {
|
|
119
|
+
const speed = record.speed;
|
|
120
|
+
const resistance = record.resistance;
|
|
90
121
|
const durationSecs = record.durationSecs;
|
|
91
|
-
|
|
92
|
-
|
|
122
|
+
// Calculate intensity from speed and resistance combination
|
|
123
|
+
const intensityFactor = (speed * resistance) / 100; // Normalize intensity
|
|
124
|
+
// Apply intensity multiplier to base MET
|
|
125
|
+
const intensityMultiplier = 0.8 + intensityFactor * 0.4; // 0.8 to 1.2x range
|
|
126
|
+
calculatedMET *= Math.min(2.0, intensityMultiplier); // Cap at 2x
|
|
127
|
+
// Duration bonus for sustained cardio
|
|
128
|
+
if (durationSecs > 600) {
|
|
129
|
+
// 10+ minutes
|
|
130
|
+
calculatedMET *= 1.1; // 10% bonus for sustained effort
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "cardio-free": {
|
|
135
|
+
const distanceKm = record.distanceKm;
|
|
136
|
+
const durationSecs = record.durationSecs;
|
|
137
|
+
if (distanceKm > 0 && durationSecs > 0 && metabolicData.paceFactors) {
|
|
138
|
+
const speedKmh = (distanceKm * 3600) / durationSecs;
|
|
93
139
|
// Find closest pace factor
|
|
94
140
|
let closestMET = metabolicData.baseMET;
|
|
95
141
|
let closestSpeed = Infinity;
|
|
@@ -183,7 +229,8 @@ const getExerciseDurationMinutes = (record, exercise) => {
|
|
|
183
229
|
}
|
|
184
230
|
break;
|
|
185
231
|
}
|
|
186
|
-
case "
|
|
232
|
+
case "cardio-machine":
|
|
233
|
+
case "cardio-free": {
|
|
187
234
|
if (record.workDurationSecs && record.isStrictMode) {
|
|
188
235
|
durationSecs = record.workDurationSecs;
|
|
189
236
|
}
|
|
@@ -194,8 +241,10 @@ const getExerciseDurationMinutes = (record, exercise) => {
|
|
|
194
241
|
}
|
|
195
242
|
}
|
|
196
243
|
}
|
|
197
|
-
// Add rest time if available (but not for
|
|
198
|
-
if (record.type !== "
|
|
244
|
+
// Add rest time if available (but not for cardio exercises)
|
|
245
|
+
if (record.type !== "cardio-machine" &&
|
|
246
|
+
record.type !== "cardio-free" &&
|
|
247
|
+
record.restDurationSecs) {
|
|
199
248
|
durationSecs += record.restDurationSecs;
|
|
200
249
|
}
|
|
201
250
|
return Math.max(0.5, durationSecs / 60);
|
|
@@ -253,15 +302,11 @@ const calculateTimingAdherence = (record, exercise, workingScore) => {
|
|
|
253
302
|
}
|
|
254
303
|
break;
|
|
255
304
|
}
|
|
256
|
-
case "
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
record.distanceKm * timingGuardrails.pacing.typicalPacePerUnit;
|
|
262
|
-
tolerance = expectedDuration * 0.2; // 20% tolerance for distance
|
|
263
|
-
}
|
|
264
|
-
break;
|
|
305
|
+
case "cardio-machine":
|
|
306
|
+
case "cardio-free": {
|
|
307
|
+
// For cardio exercises, timing adherence is less critical
|
|
308
|
+
// Users can go longer or shorter based on their fitness level
|
|
309
|
+
return { bonus: 0, penalty: 0, reason: "cardio_flexible_timing" };
|
|
265
310
|
}
|
|
266
311
|
}
|
|
267
312
|
if (expectedDuration === 0) {
|
|
@@ -312,7 +357,7 @@ const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userPro
|
|
|
312
357
|
if (typeof record.rpe === "number") {
|
|
313
358
|
effortFactor = Math.max(1, Math.min(10, record.rpe));
|
|
314
359
|
}
|
|
315
|
-
else if (typeof record.rir === "number") {
|
|
360
|
+
else if ("rir" in record && typeof record.rir === "number") {
|
|
316
361
|
effortFactor = Math.max(1, Math.min(10, 10 - record.rir));
|
|
317
362
|
}
|
|
318
363
|
// Calculate precise MET value using exercise metabolic data
|
|
@@ -331,18 +376,28 @@ const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userPro
|
|
|
331
376
|
plus.push({
|
|
332
377
|
exerciseDifficulty: parseFloat((difficultyMultiplier - 1).toFixed(3)),
|
|
333
378
|
});
|
|
334
|
-
// Goal alignment bonus
|
|
379
|
+
// ENHANCED Goal alignment bonus
|
|
335
380
|
if (userProfile === null || userProfile === void 0 ? void 0 : userProfile.fitnessGoal) {
|
|
336
381
|
let goalMultiplier = 1;
|
|
337
|
-
|
|
382
|
+
const goal = userProfile.fitnessGoal;
|
|
383
|
+
switch (goal) {
|
|
338
384
|
case "strength":
|
|
339
|
-
goalMultiplier = 1 + (exercise.strengthGainLevel / 10) * 0.12
|
|
385
|
+
goalMultiplier = 1 + (exercise.strengthGainLevel / 10) * 0.15; // Increased from 0.12
|
|
340
386
|
break;
|
|
341
387
|
case "hypertrophy":
|
|
342
|
-
goalMultiplier = 1 + (exercise.hypertrophyLevel / 10) * 0.
|
|
388
|
+
goalMultiplier = 1 + (exercise.hypertrophyLevel / 10) * 0.15;
|
|
343
389
|
break;
|
|
344
390
|
case "endurance":
|
|
345
|
-
goalMultiplier = 1 + (exercise.enduranceLevel / 10) * 0.
|
|
391
|
+
goalMultiplier = 1 + (exercise.enduranceLevel / 10) * 0.15;
|
|
392
|
+
break;
|
|
393
|
+
case "fat_burn":
|
|
394
|
+
// For fat burn, prioritize calorie burn level and high MET exercises
|
|
395
|
+
const calorieBurnBonus = (exercise.calorieBurnLevel / 10) * 0.18; // Higher bonus for calorie burn
|
|
396
|
+
const metBonus = exercise.metabolicData.baseMET > 6 ? 0.08 : 0; // 8% bonus for high MET exercises
|
|
397
|
+
goalMultiplier = 1 + calorieBurnBonus + metBonus;
|
|
398
|
+
break;
|
|
399
|
+
case "flexibility":
|
|
400
|
+
goalMultiplier = 1 + (exercise.flexibilityLevel / 10) * 0.15;
|
|
346
401
|
break;
|
|
347
402
|
case "general":
|
|
348
403
|
const avgLevel = (exercise.strengthGainLevel +
|
|
@@ -350,7 +405,7 @@ const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userPro
|
|
|
350
405
|
exercise.enduranceLevel +
|
|
351
406
|
exercise.flexibilityLevel) /
|
|
352
407
|
4;
|
|
353
|
-
goalMultiplier = 1 + (avgLevel / 10) * 0.
|
|
408
|
+
goalMultiplier = 1 + (avgLevel / 10) * 0.1;
|
|
354
409
|
break;
|
|
355
410
|
}
|
|
356
411
|
if (goalMultiplier > 1) {
|
|
@@ -109,7 +109,7 @@ exports.calculateWorkoutSummary = calculateWorkoutSummary;
|
|
|
109
109
|
const generateSetDescription = (record, exercise, scoreBreakdown) => {
|
|
110
110
|
const effort = record.rpe
|
|
111
111
|
? `RPE ${record.rpe}`
|
|
112
|
-
: record.rir
|
|
112
|
+
: "rir" in record && record.rir
|
|
113
113
|
? `${record.rir} RIR`
|
|
114
114
|
: "";
|
|
115
115
|
const calories = `${Math.round(scoreBreakdown.caloriesBurned)} cal`;
|
|
@@ -123,16 +123,34 @@ const generateSetDescription = (record, exercise, scoreBreakdown) => {
|
|
|
123
123
|
const auxWeightDur = record.auxWeightKg
|
|
124
124
|
? ` (+${record.auxWeightKg}kg)`
|
|
125
125
|
: "";
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
const minutes = Math.floor(record.durationSecs / 60);
|
|
127
|
+
const seconds = record.durationSecs % 60;
|
|
128
|
+
const timeStr = minutes > 0
|
|
129
|
+
? `${minutes}:${seconds < 10 ? `0${seconds}` : `${seconds}`}`
|
|
130
|
+
: `${seconds}s`;
|
|
131
|
+
return `${timeStr}${auxWeightDur} ${effort} • ${calories}`;
|
|
132
|
+
case "cardio-machine":
|
|
133
|
+
const resistance = record.resistance > 0 ? ` R${record.resistance}` : "";
|
|
134
|
+
const minutes_cm = Math.floor(record.durationSecs / 60);
|
|
135
|
+
const seconds_cm = record.durationSecs % 60;
|
|
136
|
+
const timeStr_cm = minutes_cm > 0
|
|
137
|
+
? `${minutes_cm}:${seconds_cm < 10 ? `0${seconds_cm}` : `${seconds_cm}`}`
|
|
138
|
+
: `${seconds_cm}s`;
|
|
139
|
+
return `${record.speed} speed${resistance} × ${timeStr_cm} ${effort} • ${calories}`;
|
|
140
|
+
case "cardio-free":
|
|
141
|
+
const minutes_cf = Math.floor(record.durationSecs / 60);
|
|
142
|
+
const seconds_cf = record.durationSecs % 60;
|
|
143
|
+
const timeStr_cf = minutes_cf > 0
|
|
144
|
+
? `${minutes_cf}:${seconds_cf < 10 ? `0${seconds_cf}` : `${seconds_cf}`}`
|
|
145
|
+
: `${seconds_cf}s`;
|
|
128
146
|
let pace = "";
|
|
129
|
-
if (record.avgPaceSecsPerKm) {
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
pace = ` (${
|
|
147
|
+
if ("avgPaceSecsPerKm" in record && record.avgPaceSecsPerKm) {
|
|
148
|
+
const paceMinutes = Math.floor(record.avgPaceSecsPerKm / 60);
|
|
149
|
+
const paceSeconds = Math.round(record.avgPaceSecsPerKm % 60);
|
|
150
|
+
const paceSecondsFormatted = paceSeconds < 10 ? `0${paceSeconds}` : `${paceSeconds}`;
|
|
151
|
+
pace = ` (${paceMinutes}:${paceSecondsFormatted}/km)`;
|
|
134
152
|
}
|
|
135
|
-
return `${record.distanceKm}km in ${
|
|
153
|
+
return `${record.distanceKm}km in ${timeStr_cf}${pace} ${effort} • ${calories}`;
|
|
136
154
|
default:
|
|
137
155
|
return `${effort} • ${calories}`;
|
|
138
156
|
}
|
|
@@ -148,7 +166,9 @@ const getBestSetReason = (record, exercise) => {
|
|
|
148
166
|
return "most reps";
|
|
149
167
|
case "duration":
|
|
150
168
|
return "longest hold";
|
|
151
|
-
case "
|
|
169
|
+
case "cardio-machine":
|
|
170
|
+
return "highest intensity";
|
|
171
|
+
case "cardio-free":
|
|
152
172
|
return "fastest pace";
|
|
153
173
|
default:
|
|
154
174
|
return "highest effort";
|
|
@@ -189,13 +209,30 @@ const calculateMuscleRecovery = (muscleStressMap, exercises) => {
|
|
|
189
209
|
recoveryHours *= 1.3; // 30% longer for moderate-high fatigue
|
|
190
210
|
else if (fatigueLevel > 40)
|
|
191
211
|
recoveryHours *= 1.1; // 10% longer for moderate fatigue
|
|
192
|
-
// Find exercises that worked this muscle - using
|
|
193
|
-
const involvedExercises =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
212
|
+
// Find exercises that worked this muscle - using manual search instead of includes
|
|
213
|
+
const involvedExercises = [];
|
|
214
|
+
exercises.forEach(({ exercise }) => {
|
|
215
|
+
let found = false;
|
|
216
|
+
// Check primary muscles
|
|
217
|
+
for (let i = 0; i < exercise.primaryMuscles.length; i++) {
|
|
218
|
+
if (exercise.primaryMuscles[i] === muscle) {
|
|
219
|
+
found = true;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Check secondary muscles if not found in primary
|
|
224
|
+
if (!found) {
|
|
225
|
+
for (let i = 0; i < exercise.secondaryMuscles.length; i++) {
|
|
226
|
+
if (exercise.secondaryMuscles[i] === muscle) {
|
|
227
|
+
found = true;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (found) {
|
|
233
|
+
involvedExercises.push(exercise.name);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
199
236
|
recovery[muscle] = {
|
|
200
237
|
fatigueLevel: Math.round(fatigueLevel),
|
|
201
238
|
recoveryHours: Math.round(recoveryHours),
|
|
@@ -270,14 +307,19 @@ const getRecommendedRestDays = (fatigueLevel, muscleRecovery) => {
|
|
|
270
307
|
extreme: 3,
|
|
271
308
|
};
|
|
272
309
|
const restDays = baseDays[fatigueLevel];
|
|
273
|
-
// Consider muscle recovery times - using manual iteration
|
|
310
|
+
// Consider muscle recovery times - using manual iteration
|
|
274
311
|
const recoveryHours = [];
|
|
275
312
|
for (const muscle in muscleRecovery) {
|
|
276
313
|
if (muscleRecovery.hasOwnProperty(muscle)) {
|
|
277
314
|
recoveryHours.push(muscleRecovery[muscle].recoveryHours);
|
|
278
315
|
}
|
|
279
316
|
}
|
|
280
|
-
|
|
317
|
+
let maxRecoveryHours = 0;
|
|
318
|
+
for (let i = 0; i < recoveryHours.length; i++) {
|
|
319
|
+
if (recoveryHours[i] > maxRecoveryHours) {
|
|
320
|
+
maxRecoveryHours = recoveryHours[i];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
281
323
|
const muscleBasedDays = Math.ceil(maxRecoveryHours / 24);
|
|
282
324
|
return Math.max(restDays, muscleBasedDays);
|
|
283
325
|
};
|