@dgpholdings/greatoak-shared 1.2.36 → 1.2.44

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/README.md CHANGED
@@ -116,3 +116,34 @@ shared/
116
116
  - **No framework code** — this package must stay framework-agnostic (no React, no NestJS decorators)
117
117
  - **Strict types** — no `any`. Use generics, discriminated unions, or `unknown`
118
118
  - **Scoring is the business core** — the scoring engine drives the muscle fatigue diagram and progress charts. Read [docs/scoring.md](docs/scoring.md) before touching it
119
+
120
+
121
+ ## Renewing token
122
+
123
+ 1. Login using the token (recommended way)
124
+
125
+ In your Git Bash, run:
126
+
127
+ `npm login`
128
+
129
+ It will prompt:
130
+
131
+ Username:
132
+ Password:
133
+ Email:
134
+
135
+ Use this:
136
+
137
+ Username → your npm username
138
+
139
+ Password → paste the granular token (NOT your npm password)
140
+
141
+ Email → your npm email
142
+
143
+ After that, npm stores the token in:
144
+
145
+ `shared\.npmrc`
146
+
147
+ Then publishing works normally:
148
+
149
+ `npm publish`
@@ -1,5 +1,7 @@
1
1
  import { TTemplateExercise } from "./commonTypes";
2
- export type TProPlanTags = "medical" | "free-hand" | "all-gym-equip" | "light-gym-equip" | "bands-free-hand";
2
+ import { TGender } from "./TApiUser";
3
+ export type TProPlanTags = "medical" | "free-hand" | "all-gym-equip" | "light-gym-equip" | "bands-free-hand" | "editors-choise";
4
+ export type TProPlanDayId = "day-1" | "day-2" | "day-3" | "day-4" | "day-5" | "day-6" | "day-7";
3
5
  export type TProPlan = {
4
6
  name: {
5
7
  default: string;
@@ -18,16 +20,14 @@ export type TProPlan = {
18
20
  fr?: string;
19
21
  };
20
22
  planId: string;
21
- planCode: string;
22
- gender: "male" | "female" | "all";
23
- slug: string;
23
+ gender: TGender;
24
24
  level: "beginner" | "intermediate" | "advanced" | "beginner-intermediate" | "intermediate-advanced" | "all";
25
25
  tags: TProPlanTags[];
26
26
  status: "active" | "achieved" | "draft";
27
27
  version: number;
28
28
  versionNote?: string;
29
29
  days: {
30
- dayId: string;
30
+ dayId: TProPlanDayId;
31
31
  name: {
32
32
  default: string;
33
33
  en?: string;
@@ -36,7 +36,7 @@ export type TProPlan = {
36
36
  es?: string;
37
37
  fr?: string;
38
38
  };
39
- exercises: TTemplateExercise[];
39
+ exerciseOrder: TTemplateExercise[][];
40
40
  estimatedDuration: number;
41
41
  planThumbnailImageUrl?: string;
42
42
  }[];
@@ -1,4 +1,4 @@
1
- import { TProPlan, TUserMetric, TExercise } from "../../types";
1
+ import { TProPlan, TUserMetric, TExercise, TTemplateExercise } from "../../types";
2
2
  /**
3
3
  * Calculates the BMI (Body Mass Index).
4
4
  */
@@ -7,3 +7,12 @@ export declare const calculateBMI: (weightKg: number, heightCm: number) => numbe
7
7
  * The Adoption Engine: Safely scales a Master Pro Plan to fit an individual user's body type.
8
8
  */
9
9
  export declare const scaleProPlan: (masterPlan: TProPlan, userMetric: TUserMetric, exercisesDictionary: Record<string, TExercise>) => TProPlan;
10
+ /**
11
+ * Calculates the estimated duration (in seconds) for an exercise based on timing guardrails.
12
+ */
13
+ export declare const calculateExerciseDurationSecs: (templateExercise: TTemplateExercise, exerciseMeta: TExercise | undefined) => number;
14
+ /**
15
+ * Calculates the total estimated duration (in minutes) for a day's plan.
16
+ * It averages the duration of alternative exercises within a single slot ("OR" pattern).
17
+ */
18
+ export declare const calculateDayPlanDuration: (exerciseOrder: TTemplateExercise[][], exercisesDictionary: Record<string, TExercise>) => number;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.scaleProPlan = exports.calculateBMI = void 0;
3
+ exports.calculateDayPlanDuration = exports.calculateExerciseDurationSecs = exports.scaleProPlan = exports.calculateBMI = void 0;
4
4
  const NO_THRESHOLD = Infinity;
5
5
  /**
6
6
  * Calculates the BMI (Body Mass Index).
@@ -75,65 +75,149 @@ const scaleProPlan = (masterPlan, userMetric, exercisesDictionary) => {
75
75
  const { weightKg, heightCm, gender, fitnessLevel } = userMetric;
76
76
  const bmi = weightKg && heightCm ? (0, exports.calculateBMI)(weightKg, heightCm) : 0;
77
77
  tailoredPlan.days.forEach((day) => {
78
- day.exercises = day.exercises.map((templateExercise) => {
79
- const exerciseMeta = exercisesDictionary[templateExercise.exerciseId];
80
- // If we can't find metadata, return as-is
81
- if (!exerciseMeta)
82
- return templateExercise;
83
- let isModified = false;
84
- let newExerciseId = templateExercise.exerciseId;
85
- let newRecords = [...templateExercise.initialRecords];
86
- // 1. Check Safety Triggers (BMI & Bodyweight Dependency)
87
- if (bmi > 0 &&
88
- (exerciseMeta.bodyweightDependency === "high" || exerciseMeta.bodyweightDependency === "medium") &&
89
- exerciseMeta.bmiThresholds) {
90
- const obeseTrigger = getAdjustedBMIThreshold(exerciseMeta.bmiThresholds.obeseTrigger, fitnessLevel);
91
- const overweightTrigger = getAdjustedBMIThreshold(exerciseMeta.bmiThresholds.overweightTrigger, fitnessLevel);
92
- if (bmi >= obeseTrigger) {
93
- // Tier 1: Hard swap to regression (if available) AND scale down volume
94
- newRecords = newRecords.map(applyFallbackScalingToRecord);
95
- isModified = true;
96
- if (exerciseMeta.regressionExerciseId && exercisesDictionary[exerciseMeta.regressionExerciseId]) {
97
- newExerciseId = exerciseMeta.regressionExerciseId;
78
+ day.exerciseOrder = day.exerciseOrder.map((slot) => {
79
+ return slot.map((templateExercise) => {
80
+ const exerciseMeta = exercisesDictionary[templateExercise.exerciseId];
81
+ // If we can't find metadata, return as-is
82
+ if (!exerciseMeta)
83
+ return templateExercise;
84
+ let isModified = false;
85
+ let newExerciseId = templateExercise.exerciseId;
86
+ let newRecords = [...templateExercise.initialRecords];
87
+ // 1. Check Safety Triggers (BMI & Bodyweight Dependency)
88
+ if (bmi > 0 &&
89
+ (exerciseMeta.bodyweightDependency === "high" ||
90
+ exerciseMeta.bodyweightDependency === "medium") &&
91
+ exerciseMeta.bmiThresholds) {
92
+ const obeseTrigger = getAdjustedBMIThreshold(exerciseMeta.bmiThresholds.obeseTrigger, fitnessLevel);
93
+ const overweightTrigger = getAdjustedBMIThreshold(exerciseMeta.bmiThresholds.overweightTrigger, fitnessLevel);
94
+ if (bmi >= obeseTrigger) {
95
+ // Tier 1: Hard swap to regression (if available) AND scale down volume
96
+ newRecords = newRecords.map(applyFallbackScalingToRecord);
97
+ isModified = true;
98
+ if (exerciseMeta.regressionExerciseId &&
99
+ exercisesDictionary[exerciseMeta.regressionExerciseId]) {
100
+ newExerciseId = exerciseMeta.regressionExerciseId;
101
+ }
102
+ }
103
+ else if (bmi >= overweightTrigger) {
104
+ // Tier 2: Softer response. Keep the exercise, but scale down the volume/duration.
105
+ newRecords = newRecords.map(applyFallbackScalingToRecord);
106
+ isModified = true;
98
107
  }
99
108
  }
100
- else if (bmi >= overweightTrigger) {
101
- // Tier 2: Softer response. Keep the exercise, but scale down the volume/duration.
102
- newRecords = newRecords.map(applyFallbackScalingToRecord);
103
- isModified = true;
104
- }
105
- }
106
- // 2. Check Load Multipliers
107
- if (weightKg && exerciseMeta.weightMultiplier && newRecords.length > 0) {
108
- const multiplier = gender === "male"
109
- ? exerciseMeta.weightMultiplier.male
110
- : gender === "female"
111
- ? exerciseMeta.weightMultiplier.female
112
- : exerciseMeta.weightMultiplier.default;
113
- if (multiplier && multiplier > 0) {
114
- // Round to nearest 2.5kg to match real-world gym equipment
115
- const suggestedLoadKg = roundToNearest(weightKg * multiplier, 2.5);
116
- newRecords = newRecords.map((record) => {
117
- if (record.type === "weight-reps") {
118
- isModified = true;
119
- return Object.assign(Object.assign({}, record), { kg: suggestedLoadKg.toString() });
120
- }
121
- // Guard against applying load to a falsy "0" aux weight
122
- if (record.type === "reps-only" || record.type === "duration" || record.type === "cardio-free") {
123
- const currentAux = parseFloat(record.auxWeightKg || "0");
124
- if (!isNaN(currentAux) && currentAux > 0) {
109
+ // 2. Check Load Multipliers
110
+ if (weightKg && exerciseMeta.weightMultiplier && newRecords.length > 0) {
111
+ const multiplier = gender === "male"
112
+ ? exerciseMeta.weightMultiplier.male
113
+ : gender === "female"
114
+ ? exerciseMeta.weightMultiplier.female
115
+ : exerciseMeta.weightMultiplier.default;
116
+ if (multiplier && multiplier > 0) {
117
+ // Round to nearest 2.5kg to match real-world gym equipment
118
+ const suggestedLoadKg = roundToNearest(weightKg * multiplier, 2.5);
119
+ newRecords = newRecords.map((record) => {
120
+ if (record.type === "weight-reps") {
125
121
  isModified = true;
126
- return Object.assign(Object.assign({}, record), { auxWeightKg: suggestedLoadKg.toString() });
122
+ return Object.assign(Object.assign({}, record), { kg: suggestedLoadKg.toString() });
127
123
  }
128
- }
129
- return record;
130
- });
124
+ // Guard against applying load to a falsy "0" aux weight
125
+ if (record.type === "reps-only" ||
126
+ record.type === "duration" ||
127
+ record.type === "cardio-free") {
128
+ const currentAux = parseFloat(record.auxWeightKg || "0");
129
+ if (!isNaN(currentAux) && currentAux > 0) {
130
+ isModified = true;
131
+ return Object.assign(Object.assign({}, record), { auxWeightKg: suggestedLoadKg.toString() });
132
+ }
133
+ }
134
+ return record;
135
+ });
136
+ }
131
137
  }
132
- }
133
- // Return the tailored template exercise
134
- return Object.assign(Object.assign({}, templateExercise), { exerciseId: newExerciseId, initialRecords: newRecords, isAutoScaled: isModified || undefined });
138
+ // Return the tailored template exercise
139
+ return Object.assign(Object.assign({}, templateExercise), { exerciseId: newExerciseId, initialRecords: newRecords, isAutoScaled: isModified || undefined });
140
+ });
135
141
  });
136
142
  });
137
143
  return tailoredPlan;
138
144
  };
139
145
  exports.scaleProPlan = scaleProPlan;
146
+ /**
147
+ * Calculates the estimated duration (in seconds) for an exercise based on timing guardrails.
148
+ */
149
+ const calculateExerciseDurationSecs = (templateExercise, exerciseMeta) => {
150
+ if (!exerciseMeta || !exerciseMeta.timingGuardrails)
151
+ return 0;
152
+ let guardrails = exerciseMeta.timingGuardrails;
153
+ // Defensive parse in case it's a JSON string from DB
154
+ if (typeof guardrails === "string") {
155
+ try {
156
+ guardrails = JSON.parse(guardrails);
157
+ }
158
+ catch (e) {
159
+ return 0;
160
+ }
161
+ }
162
+ let totalSecs = guardrails.setupTypicalSecs || 0;
163
+ const records = templateExercise.initialRecords || [];
164
+ records.forEach((record, index) => {
165
+ var _a, _b, _c, _d, _e, _f, _g, _h;
166
+ let workSecs = 0;
167
+ if (record.type === "reps-only" || record.type === "weight-reps") {
168
+ const repsStr = "reps" in record ? record.reps : "0";
169
+ const reps = parseInt(repsStr, 10) || 0;
170
+ let singleRepSecs = 2.5; // fallback
171
+ if (guardrails.type === "weight-reps" || guardrails.type === "reps-only") {
172
+ singleRepSecs = (_b = (_a = guardrails.singleRep) === null || _a === void 0 ? void 0 : _a.typical) !== null && _b !== void 0 ? _b : 2.5;
173
+ }
174
+ workSecs = reps * singleRepSecs;
175
+ }
176
+ else if (record.type === "duration" ||
177
+ record.type === "cardio-machine" ||
178
+ record.type === "cardio-free") {
179
+ const durationStr = "durationMmSs" in record ? record.durationMmSs : "00:00";
180
+ const [mm, ss] = (durationStr || "00:00").split(":").map((v) => parseInt(v, 10) || 0);
181
+ workSecs = (mm * 60) + ss;
182
+ if (workSecs === 0 && guardrails.type === "duration") {
183
+ workSecs = (_d = (_c = guardrails.setDuration) === null || _c === void 0 ? void 0 : _c.typical) !== null && _d !== void 0 ? _d : 60;
184
+ }
185
+ }
186
+ totalSecs += workSecs;
187
+ // Add rest time if not the last set
188
+ if (index < records.length - 1) {
189
+ const restTime = (_h = (_f = (_e = record.restDurationSecs) !== null && _e !== void 0 ? _e : templateExercise.restTimeSecs) !== null && _f !== void 0 ? _f : (_g = guardrails.restPeriods) === null || _g === void 0 ? void 0 : _g.typical) !== null && _h !== void 0 ? _h : 60;
190
+ totalSecs += restTime;
191
+ }
192
+ });
193
+ return totalSecs;
194
+ };
195
+ exports.calculateExerciseDurationSecs = calculateExerciseDurationSecs;
196
+ /**
197
+ * Calculates the total estimated duration (in minutes) for a day's plan.
198
+ * It averages the duration of alternative exercises within a single slot ("OR" pattern).
199
+ */
200
+ const calculateDayPlanDuration = (exerciseOrder, exercisesDictionary) => {
201
+ if (!exerciseOrder || !Array.isArray(exerciseOrder))
202
+ return 0;
203
+ let totalDaySecs = 0;
204
+ exerciseOrder.forEach((slot) => {
205
+ if (!Array.isArray(slot) || slot.length === 0)
206
+ return;
207
+ let slotTotalSecs = 0;
208
+ let validExercisesCount = 0;
209
+ slot.forEach((templateExercise) => {
210
+ const exerciseMeta = exercisesDictionary[templateExercise.exerciseId];
211
+ if (exerciseMeta) {
212
+ slotTotalSecs += (0, exports.calculateExerciseDurationSecs)(templateExercise, exerciseMeta);
213
+ validExercisesCount++;
214
+ }
215
+ });
216
+ if (validExercisesCount > 0) {
217
+ totalDaySecs += slotTotalSecs / validExercisesCount;
218
+ }
219
+ });
220
+ // Return duration in minutes (rounded up to nearest minute)
221
+ return Math.ceil(totalDaySecs / 60);
222
+ };
223
+ exports.calculateDayPlanDuration = calculateDayPlanDuration;
@@ -8,4 +8,4 @@ export { generatePlanCode } from "./planCode.util";
8
8
  export { maskEmail, isAnonymousEmail, isEmail } from "./email.utils";
9
9
  export { NOOP } from "./noop.utils";
10
10
  export { calculateExerciseScoreV2, calculateTotalVolume } from "./scoring";
11
- export { scaleProPlan, calculateBMI } from "./adoptionEngine/scaleProPlan.util";
11
+ export { scaleProPlan, calculateBMI, calculateDayPlanDuration, calculateExerciseDurationSecs } from "./adoptionEngine/scaleProPlan.util";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.calculateBMI = exports.scaleProPlan = exports.calculateTotalVolume = exports.calculateExerciseScoreV2 = exports.NOOP = exports.isEmail = exports.isAnonymousEmail = exports.maskEmail = exports.generatePlanCode = exports.toError = exports.slugifyText = exports.isDefinedNumber = exports.isDefined = exports.countryToCurrencyCode = exports.getDaysAndHoursDifference = exports.isUserAllowedToUpdate = exports.mmssToSecs = exports.toNumber = void 0;
3
+ exports.calculateExerciseDurationSecs = exports.calculateDayPlanDuration = exports.calculateBMI = exports.scaleProPlan = exports.calculateTotalVolume = exports.calculateExerciseScoreV2 = exports.NOOP = exports.isEmail = exports.isAnonymousEmail = exports.maskEmail = exports.generatePlanCode = exports.toError = exports.slugifyText = exports.isDefinedNumber = exports.isDefined = 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");
@@ -30,3 +30,5 @@ Object.defineProperty(exports, "calculateTotalVolume", { enumerable: true, get:
30
30
  var scaleProPlan_util_1 = require("./adoptionEngine/scaleProPlan.util");
31
31
  Object.defineProperty(exports, "scaleProPlan", { enumerable: true, get: function () { return scaleProPlan_util_1.scaleProPlan; } });
32
32
  Object.defineProperty(exports, "calculateBMI", { enumerable: true, get: function () { return scaleProPlan_util_1.calculateBMI; } });
33
+ Object.defineProperty(exports, "calculateDayPlanDuration", { enumerable: true, get: function () { return scaleProPlan_util_1.calculateDayPlanDuration; } });
34
+ Object.defineProperty(exports, "calculateExerciseDurationSecs", { enumerable: true, get: function () { return scaleProPlan_util_1.calculateExerciseDurationSecs; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dgpholdings/greatoak-shared",
3
- "version": "1.2.36",
3
+ "version": "1.2.44",
4
4
  "description": "Shared TypeScript types and utilities for @dgpholdings projects",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "https://github.com/your-org/greatoak-shared.git"
18
+ "url": "https://gitlab.com/greatoak/shared.git"
19
19
  },
20
20
  "author": "Siddhartha Chowdhury",
21
21
  "license": "MIT",