@nickchristensen/cliftin 1.0.2
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 +206 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/exercises/list.d.ts +13 -0
- package/dist/commands/exercises/list.js +54 -0
- package/dist/commands/exercises/show.d.ts +21 -0
- package/dist/commands/exercises/show.js +140 -0
- package/dist/commands/programs/list.d.ts +6 -0
- package/dist/commands/programs/list.js +27 -0
- package/dist/commands/programs/show.d.ts +13 -0
- package/dist/commands/programs/show.js +71 -0
- package/dist/commands/workouts/list.d.ts +15 -0
- package/dist/commands/workouts/list.js +72 -0
- package/dist/commands/workouts/show.d.ts +9 -0
- package/dist/commands/workouts/show.js +72 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +24 -0
- package/dist/lib/db.d.ts +104 -0
- package/dist/lib/db.js +15 -0
- package/dist/lib/json-error.d.ts +5 -0
- package/dist/lib/json-error.js +6 -0
- package/dist/lib/json-weight.d.ts +7 -0
- package/dist/lib/json-weight.js +42 -0
- package/dist/lib/names.d.ts +5 -0
- package/dist/lib/names.js +70 -0
- package/dist/lib/output.d.ts +7 -0
- package/dist/lib/output.js +73 -0
- package/dist/lib/repositories/exercises.d.ts +30 -0
- package/dist/lib/repositories/exercises.js +280 -0
- package/dist/lib/repositories/programs.d.ts +6 -0
- package/dist/lib/repositories/programs.js +217 -0
- package/dist/lib/repositories/selectors.d.ts +3 -0
- package/dist/lib/repositories/selectors.js +35 -0
- package/dist/lib/repositories/workouts.d.ts +13 -0
- package/dist/lib/repositories/workouts.js +128 -0
- package/dist/lib/rpe.d.ts +2 -0
- package/dist/lib/rpe.js +4 -0
- package/dist/lib/time.d.ts +10 -0
- package/dist/lib/time.js +49 -0
- package/dist/lib/types.d.ts +123 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/units.d.ts +16 -0
- package/dist/lib/units.js +81 -0
- package/oclif.manifest.json +386 -0
- package/package.json +93 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { formatExerciseDisplayName } from '../names.js';
|
|
2
|
+
import { normalizeRpe } from '../rpe.js';
|
|
3
|
+
import { appleSecondsToIso, dateRangeToAppleSeconds } from '../time.js';
|
|
4
|
+
import { convertKgToDisplayVolume, convertKgToDisplayWeight, resolveGlobalWeightUnit } from '../units.js';
|
|
5
|
+
import { resolveIdOrName } from './selectors.js';
|
|
6
|
+
function asBool(value) {
|
|
7
|
+
return value === 1;
|
|
8
|
+
}
|
|
9
|
+
export async function listWorkouts(db, filters) {
|
|
10
|
+
const dateRange = dateRangeToAppleSeconds({ from: filters.from, on: filters.on, to: filters.to });
|
|
11
|
+
let query = db
|
|
12
|
+
.selectFrom('ZWORKOUTRESULT as wr')
|
|
13
|
+
.leftJoin('ZROUTINE as r', 'r.Z_PK', 'wr.ZROUTINE')
|
|
14
|
+
.leftJoin('ZPERIOD as per', 'per.Z_PK', 'r.ZPERIOD')
|
|
15
|
+
.leftJoin('ZWORKOUTPLAN as pDirect', 'pDirect.Z_PK', 'r.ZWORKOUTPLAN')
|
|
16
|
+
.leftJoin('ZWORKOUTPLAN as pFromPeriod', 'pFromPeriod.Z_PK', 'per.ZWORKOUTPLAN')
|
|
17
|
+
.select([
|
|
18
|
+
'wr.Z_PK as id',
|
|
19
|
+
'wr.ZSTARTDATE as startDate',
|
|
20
|
+
'wr.ZDURATION as duration',
|
|
21
|
+
'wr.ZROUTINENAME as routineNameFromResult',
|
|
22
|
+
'r.ZNAME as routineNameFromPlan',
|
|
23
|
+
'pDirect.ZNAME as programNameDirect',
|
|
24
|
+
'pFromPeriod.ZNAME as programNameFromPeriod',
|
|
25
|
+
]);
|
|
26
|
+
if (filters.program) {
|
|
27
|
+
const programId = await resolveIdOrName(db, 'ZWORKOUTPLAN', filters.program);
|
|
28
|
+
query = query.where((eb) => eb.or([eb('r.ZWORKOUTPLAN', '=', programId), eb('per.ZWORKOUTPLAN', '=', programId)]));
|
|
29
|
+
}
|
|
30
|
+
if (filters.routine) {
|
|
31
|
+
const routineId = await resolveIdOrName(db, 'ZROUTINE', filters.routine);
|
|
32
|
+
query = query.where('wr.ZROUTINE', '=', routineId);
|
|
33
|
+
}
|
|
34
|
+
if (dateRange.from !== undefined)
|
|
35
|
+
query = query.where('wr.ZSTARTDATE', '>=', dateRange.from);
|
|
36
|
+
if (dateRange.to !== undefined)
|
|
37
|
+
query = query.where('wr.ZSTARTDATE', '<=', dateRange.to);
|
|
38
|
+
query = query.orderBy('wr.ZSTARTDATE', 'desc');
|
|
39
|
+
if (filters.limit !== undefined) {
|
|
40
|
+
query = query.limit(filters.limit);
|
|
41
|
+
}
|
|
42
|
+
const rows = await query.execute();
|
|
43
|
+
return rows.map((row) => ({
|
|
44
|
+
date: appleSecondsToIso(row.startDate),
|
|
45
|
+
duration: row.duration,
|
|
46
|
+
id: row.id,
|
|
47
|
+
program: row.programNameDirect ?? row.programNameFromPeriod,
|
|
48
|
+
routine: row.routineNameFromResult ?? row.routineNameFromPlan,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
export async function getWorkoutDetail(db, workoutId) {
|
|
52
|
+
const unitPreference = await resolveGlobalWeightUnit(db);
|
|
53
|
+
const workout = await db
|
|
54
|
+
.selectFrom('ZWORKOUTRESULT as wr')
|
|
55
|
+
.leftJoin('ZROUTINE as r', 'r.Z_PK', 'wr.ZROUTINE')
|
|
56
|
+
.leftJoin('ZPERIOD as per', 'per.Z_PK', 'r.ZPERIOD')
|
|
57
|
+
.leftJoin('ZWORKOUTPLAN as pDirect', 'pDirect.Z_PK', 'r.ZWORKOUTPLAN')
|
|
58
|
+
.leftJoin('ZWORKOUTPLAN as pFromPeriod', 'pFromPeriod.Z_PK', 'per.ZWORKOUTPLAN')
|
|
59
|
+
.select([
|
|
60
|
+
'wr.Z_PK as id',
|
|
61
|
+
'wr.ZSTARTDATE as startDate',
|
|
62
|
+
'wr.ZDURATION as duration',
|
|
63
|
+
'wr.ZROUTINENAME as routineNameFromResult',
|
|
64
|
+
'r.ZNAME as routineNameFromPlan',
|
|
65
|
+
'pDirect.ZNAME as programNameDirect',
|
|
66
|
+
'pFromPeriod.ZNAME as programNameFromPeriod',
|
|
67
|
+
])
|
|
68
|
+
.where('wr.Z_PK', '=', workoutId)
|
|
69
|
+
.executeTakeFirst();
|
|
70
|
+
if (!workout)
|
|
71
|
+
throw new Error(`Workout not found: ${workoutId}`);
|
|
72
|
+
const exerciseRows = await db
|
|
73
|
+
.selectFrom('ZEXERCISERESULT as er')
|
|
74
|
+
.leftJoin('ZEXERCISECONFIGURATION as ec', 'ec.Z_PK', 'er.ZCONFIGURATION')
|
|
75
|
+
.leftJoin('ZEXERCISEINFORMATION as ei', 'ei.Z_PK', 'ec.ZINFORMATION')
|
|
76
|
+
.select(['er.Z_PK as exerciseResultId', 'ei.Z_PK as exerciseId', 'ei.ZISUSERCREATED as isUserCreated', 'ei.ZNAME as exerciseName'])
|
|
77
|
+
.where('er.ZWORKOUT', '=', workoutId)
|
|
78
|
+
.orderBy('er.Z_FOK_WORKOUT', 'asc')
|
|
79
|
+
.orderBy('er.Z_PK', 'asc')
|
|
80
|
+
.execute();
|
|
81
|
+
const exerciseResultIds = exerciseRows.map((row) => row.exerciseResultId);
|
|
82
|
+
const setRows = exerciseResultIds.length === 0
|
|
83
|
+
? []
|
|
84
|
+
: await db
|
|
85
|
+
.selectFrom('ZGYMSETRESULT')
|
|
86
|
+
.select([
|
|
87
|
+
'Z_PK as id',
|
|
88
|
+
'ZEXERCISE as exerciseResultId',
|
|
89
|
+
'ZREPS as reps',
|
|
90
|
+
'ZRPE as rpe',
|
|
91
|
+
'ZWEIGHT as weight',
|
|
92
|
+
'ZTIME as timeSeconds',
|
|
93
|
+
'ZVOLUME as volume',
|
|
94
|
+
])
|
|
95
|
+
.where('ZEXERCISE', 'in', exerciseResultIds)
|
|
96
|
+
.orderBy('Z_FOK_EXERCISE', 'asc')
|
|
97
|
+
.orderBy('Z_PK', 'asc')
|
|
98
|
+
.execute();
|
|
99
|
+
const setsByExercise = new Map();
|
|
100
|
+
for (const row of setRows) {
|
|
101
|
+
if (row.exerciseResultId === null)
|
|
102
|
+
continue;
|
|
103
|
+
const current = setsByExercise.get(row.exerciseResultId) ?? [];
|
|
104
|
+
current.push({
|
|
105
|
+
id: row.id,
|
|
106
|
+
reps: row.reps,
|
|
107
|
+
rpe: normalizeRpe(row.rpe),
|
|
108
|
+
timeSeconds: row.timeSeconds,
|
|
109
|
+
volume: convertKgToDisplayVolume(row.volume, unitPreference),
|
|
110
|
+
weight: convertKgToDisplayWeight(row.weight, unitPreference),
|
|
111
|
+
});
|
|
112
|
+
setsByExercise.set(row.exerciseResultId, current);
|
|
113
|
+
}
|
|
114
|
+
const exercises = exerciseRows.map((row) => ({
|
|
115
|
+
exerciseId: row.exerciseId,
|
|
116
|
+
exerciseResultId: row.exerciseResultId,
|
|
117
|
+
name: formatExerciseDisplayName(row.exerciseName, asBool(row.isUserCreated)),
|
|
118
|
+
sets: setsByExercise.get(row.exerciseResultId) ?? [],
|
|
119
|
+
}));
|
|
120
|
+
return {
|
|
121
|
+
date: appleSecondsToIso(workout.startDate),
|
|
122
|
+
duration: workout.duration,
|
|
123
|
+
exercises,
|
|
124
|
+
id: workout.id,
|
|
125
|
+
program: workout.programNameDirect ?? workout.programNameFromPeriod,
|
|
126
|
+
routine: workout.routineNameFromResult ?? workout.routineNameFromPlan,
|
|
127
|
+
};
|
|
128
|
+
}
|
package/dist/lib/rpe.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type DateFilterInput = {
|
|
2
|
+
from?: string;
|
|
3
|
+
on?: string;
|
|
4
|
+
to?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function appleSecondsToIso(value: null | number): null | string;
|
|
7
|
+
export declare function dateRangeToAppleSeconds(input: DateFilterInput): {
|
|
8
|
+
from?: number;
|
|
9
|
+
to?: number;
|
|
10
|
+
};
|
package/dist/lib/time.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { endOfDay, formatISO, isAfter, isValid, parse, startOfDay } from 'date-fns';
|
|
2
|
+
const APPLE_EPOCH_UNIX_OFFSET_SECONDS = 978_307_200;
|
|
3
|
+
export function appleSecondsToIso(value) {
|
|
4
|
+
if (value === null || value === undefined)
|
|
5
|
+
return null;
|
|
6
|
+
return formatISO(appleSecondsToDate(value));
|
|
7
|
+
}
|
|
8
|
+
function parseLocalDate(value) {
|
|
9
|
+
const date = parse(value, 'yyyy-MM-dd', new Date());
|
|
10
|
+
if (!isValid(date)) {
|
|
11
|
+
throw new Error(`Invalid date format: ${value}. Use YYYY-MM-DD.`);
|
|
12
|
+
}
|
|
13
|
+
// Guard against partial parse behavior; require exact YYYY-MM-DD roundtrip.
|
|
14
|
+
if (formatISO(date, { representation: 'date' }) !== value) {
|
|
15
|
+
throw new Error(`Invalid date format: ${value}. Use YYYY-MM-DD.`);
|
|
16
|
+
}
|
|
17
|
+
return date;
|
|
18
|
+
}
|
|
19
|
+
function appleSecondsToDate(value) {
|
|
20
|
+
return new Date((value + APPLE_EPOCH_UNIX_OFFSET_SECONDS) * 1000);
|
|
21
|
+
}
|
|
22
|
+
function dateToAppleSeconds(date) {
|
|
23
|
+
return date.getTime() / 1000 - APPLE_EPOCH_UNIX_OFFSET_SECONDS;
|
|
24
|
+
}
|
|
25
|
+
export function dateRangeToAppleSeconds(input) {
|
|
26
|
+
if (input.on) {
|
|
27
|
+
const start = startOfDay(parseLocalDate(input.on));
|
|
28
|
+
const end = endOfDay(start);
|
|
29
|
+
return {
|
|
30
|
+
from: dateToAppleSeconds(start),
|
|
31
|
+
to: dateToAppleSeconds(end),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const output = {};
|
|
35
|
+
if (input.from) {
|
|
36
|
+
const start = startOfDay(parseLocalDate(input.from));
|
|
37
|
+
output.from = dateToAppleSeconds(start);
|
|
38
|
+
}
|
|
39
|
+
if (input.to) {
|
|
40
|
+
const end = endOfDay(parseLocalDate(input.to));
|
|
41
|
+
output.to = dateToAppleSeconds(end);
|
|
42
|
+
}
|
|
43
|
+
if (output.from !== undefined &&
|
|
44
|
+
output.to !== undefined &&
|
|
45
|
+
isAfter(appleSecondsToDate(output.from), appleSecondsToDate(output.to))) {
|
|
46
|
+
throw new Error('Invalid date range: --from must be before or equal to --to.');
|
|
47
|
+
}
|
|
48
|
+
return output;
|
|
49
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export type IdName = {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
export type ProgramSummary = {
|
|
6
|
+
dateAdded: null | string;
|
|
7
|
+
id: number;
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
isTemplate: boolean;
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
export type PlannedSet = {
|
|
13
|
+
id: null | number;
|
|
14
|
+
reps: null | number;
|
|
15
|
+
rpe: null | number;
|
|
16
|
+
timeSeconds: null | number;
|
|
17
|
+
weight: null | number;
|
|
18
|
+
};
|
|
19
|
+
export type PlannedExercise = {
|
|
20
|
+
exerciseConfigId: number;
|
|
21
|
+
id: null | number;
|
|
22
|
+
name: null | string;
|
|
23
|
+
plannedReps: null | number;
|
|
24
|
+
plannedSets: null | number;
|
|
25
|
+
plannedTimeSeconds: null | number;
|
|
26
|
+
plannedWeight: null | number;
|
|
27
|
+
sets: PlannedSet[];
|
|
28
|
+
};
|
|
29
|
+
export type ProgramRoutine = {
|
|
30
|
+
exercises: PlannedExercise[];
|
|
31
|
+
id: number;
|
|
32
|
+
name: null | string;
|
|
33
|
+
};
|
|
34
|
+
export type ProgramWeek = {
|
|
35
|
+
id: number;
|
|
36
|
+
routines: ProgramRoutine[];
|
|
37
|
+
};
|
|
38
|
+
export type ProgramDetailTree = {
|
|
39
|
+
program: ProgramSummary;
|
|
40
|
+
weeks: ProgramWeek[];
|
|
41
|
+
};
|
|
42
|
+
export type WorkoutSummary = {
|
|
43
|
+
date: null | string;
|
|
44
|
+
duration: null | number;
|
|
45
|
+
id: number;
|
|
46
|
+
program: null | string;
|
|
47
|
+
routine: null | string;
|
|
48
|
+
};
|
|
49
|
+
export type WorkoutSet = {
|
|
50
|
+
id: number;
|
|
51
|
+
reps: null | number;
|
|
52
|
+
rpe: null | number;
|
|
53
|
+
timeSeconds: null | number;
|
|
54
|
+
volume: null | number;
|
|
55
|
+
weight: null | number;
|
|
56
|
+
};
|
|
57
|
+
export type WorkoutExerciseDetail = {
|
|
58
|
+
exerciseId: null | number;
|
|
59
|
+
exerciseResultId: number;
|
|
60
|
+
name: null | string;
|
|
61
|
+
sets: WorkoutSet[];
|
|
62
|
+
};
|
|
63
|
+
export type WorkoutDetail = {
|
|
64
|
+
date: null | string;
|
|
65
|
+
duration: null | number;
|
|
66
|
+
exercises: WorkoutExerciseDetail[];
|
|
67
|
+
id: number;
|
|
68
|
+
program: null | string;
|
|
69
|
+
routine: null | string;
|
|
70
|
+
};
|
|
71
|
+
export type ExerciseSummary = {
|
|
72
|
+
equipment: null | string;
|
|
73
|
+
id: number;
|
|
74
|
+
lastPerformed: null | string;
|
|
75
|
+
name: null | string;
|
|
76
|
+
primaryMuscles: null | string;
|
|
77
|
+
secondaryMuscles: null | string;
|
|
78
|
+
supports1RM: boolean;
|
|
79
|
+
timerBased: boolean;
|
|
80
|
+
timesPerformed: number;
|
|
81
|
+
};
|
|
82
|
+
export type ExerciseHistoryRow = {
|
|
83
|
+
date: null | string;
|
|
84
|
+
routine: null | string;
|
|
85
|
+
sets: number;
|
|
86
|
+
topReps: null | number;
|
|
87
|
+
topWeight: null | number;
|
|
88
|
+
totalReps: number;
|
|
89
|
+
volume: number;
|
|
90
|
+
workoutId: number;
|
|
91
|
+
};
|
|
92
|
+
export type ExerciseHistorySet = {
|
|
93
|
+
reps: null | number;
|
|
94
|
+
setId: number;
|
|
95
|
+
timeSeconds: null | number;
|
|
96
|
+
volume: null | number;
|
|
97
|
+
weight: null | number;
|
|
98
|
+
};
|
|
99
|
+
export type ExerciseHistoryWithSetsRow = {
|
|
100
|
+
date: null | string;
|
|
101
|
+
routine: null | string;
|
|
102
|
+
sets: ExerciseHistorySet[];
|
|
103
|
+
topReps: null | number;
|
|
104
|
+
topWeight: null | number;
|
|
105
|
+
totalReps: number;
|
|
106
|
+
volume: number;
|
|
107
|
+
workoutId: number;
|
|
108
|
+
};
|
|
109
|
+
export type ExerciseDetail = {
|
|
110
|
+
defaultProgressMetric: null | string;
|
|
111
|
+
equipment: null | string;
|
|
112
|
+
id: number;
|
|
113
|
+
lastHistoryEntry: ExerciseHistoryRow | null;
|
|
114
|
+
name: null | string;
|
|
115
|
+
perceptionScale: null | string;
|
|
116
|
+
primaryMuscles: null | string;
|
|
117
|
+
recentRoutines: string[];
|
|
118
|
+
secondaryMuscles: null | string;
|
|
119
|
+
supports1RM: boolean;
|
|
120
|
+
timerBased: boolean;
|
|
121
|
+
totalRoutines: number;
|
|
122
|
+
totalWorkouts: number;
|
|
123
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Kysely } from 'kysely';
|
|
2
|
+
import { DatabaseSchema } from './db.js';
|
|
3
|
+
export type UnitPreference = 'imperial' | 'metric' | 'unknown';
|
|
4
|
+
export type WeightDisplayUnit = 'kg' | 'lb';
|
|
5
|
+
export type WeightValueWithUnit = {
|
|
6
|
+
unit: WeightDisplayUnit;
|
|
7
|
+
value: null | number;
|
|
8
|
+
};
|
|
9
|
+
export declare function convertKgToDisplayWeight(weight: null | number, unitPreference: UnitPreference): null | number;
|
|
10
|
+
export declare function convertDisplayWeightToKg(weight: number, unitPreference: UnitPreference): number;
|
|
11
|
+
export declare function convertKgToDisplayVolume(volume: null | number, unitPreference: UnitPreference): null | number;
|
|
12
|
+
export declare function weightUnitLabel(unitPreference: UnitPreference): WeightDisplayUnit;
|
|
13
|
+
export declare function withWeightUnit(weight: null | number, unitPreference: UnitPreference): WeightValueWithUnit;
|
|
14
|
+
export declare function resolveGlobalWeightUnit(db: Kysely<DatabaseSchema>): Promise<UnitPreference>;
|
|
15
|
+
export declare function resolveProgramWeightUnit(db: Kysely<DatabaseSchema>, programId: number): Promise<UnitPreference>;
|
|
16
|
+
export declare function resolveExerciseWeightUnit(db: Kysely<DatabaseSchema>, exerciseId: number): Promise<UnitPreference>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const KG_TO_LB_MULTIPLIER = 2.2;
|
|
2
|
+
function normalizeUnitPreference(value) {
|
|
3
|
+
if (!value)
|
|
4
|
+
return 'unknown';
|
|
5
|
+
const normalized = value.trim().toLowerCase();
|
|
6
|
+
if (normalized.includes('imperial') || normalized === 'lbs' || normalized === 'lb')
|
|
7
|
+
return 'imperial';
|
|
8
|
+
if (normalized.includes('metric') || normalized === 'kg')
|
|
9
|
+
return 'metric';
|
|
10
|
+
return 'unknown';
|
|
11
|
+
}
|
|
12
|
+
export function convertKgToDisplayWeight(weight, unitPreference) {
|
|
13
|
+
if (weight === null)
|
|
14
|
+
return null;
|
|
15
|
+
if (unitPreference !== 'imperial')
|
|
16
|
+
return weight;
|
|
17
|
+
return Number((weight * KG_TO_LB_MULTIPLIER).toFixed(2));
|
|
18
|
+
}
|
|
19
|
+
export function convertDisplayWeightToKg(weight, unitPreference) {
|
|
20
|
+
if (unitPreference !== 'imperial')
|
|
21
|
+
return weight;
|
|
22
|
+
return Number((weight / KG_TO_LB_MULTIPLIER).toFixed(6));
|
|
23
|
+
}
|
|
24
|
+
export function convertKgToDisplayVolume(volume, unitPreference) {
|
|
25
|
+
if (volume === null)
|
|
26
|
+
return null;
|
|
27
|
+
if (unitPreference !== 'imperial')
|
|
28
|
+
return volume;
|
|
29
|
+
return Number((volume * KG_TO_LB_MULTIPLIER).toFixed(2));
|
|
30
|
+
}
|
|
31
|
+
export function weightUnitLabel(unitPreference) {
|
|
32
|
+
return unitPreference === 'imperial' ? 'lb' : 'kg';
|
|
33
|
+
}
|
|
34
|
+
export function withWeightUnit(weight, unitPreference) {
|
|
35
|
+
return {
|
|
36
|
+
unit: weightUnitLabel(unitPreference),
|
|
37
|
+
value: weight,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function resolveGlobalWeightUnit(db) {
|
|
41
|
+
const settings = await db
|
|
42
|
+
.selectFrom('ZSETTINGS')
|
|
43
|
+
.select('ZMEASURMENTUNIT as unit')
|
|
44
|
+
.executeTakeFirst();
|
|
45
|
+
return normalizeUnitPreference(settings?.unit ?? null);
|
|
46
|
+
}
|
|
47
|
+
export async function resolveProgramWeightUnit(db, programId) {
|
|
48
|
+
const settingsUnit = await resolveGlobalWeightUnit(db);
|
|
49
|
+
if (settingsUnit !== 'unknown')
|
|
50
|
+
return settingsUnit;
|
|
51
|
+
const equipmentUnits = await db
|
|
52
|
+
.selectFrom('ZROUTINE as r')
|
|
53
|
+
.leftJoin('ZPERIOD as p', 'p.Z_PK', 'r.ZPERIOD')
|
|
54
|
+
.leftJoin('Z_12ROUTINES as j', 'j.Z_28ROUTINES', 'r.Z_PK')
|
|
55
|
+
.leftJoin('ZEXERCISECONFIGURATION as ec', 'ec.Z_PK', 'j.Z_12EXERCISES')
|
|
56
|
+
.leftJoin('ZEXERCISEINFORMATION as ei', 'ei.Z_PK', 'ec.ZINFORMATION')
|
|
57
|
+
.leftJoin('ZEQUIPMENT2 as eq', 'eq.Z_PK', 'ei.ZEQUIPMENT')
|
|
58
|
+
.select('eq.ZMEASURMENTUNIT as unit')
|
|
59
|
+
.where((eb) => eb.or([eb('p.ZWORKOUTPLAN', '=', programId), eb('r.ZWORKOUTPLAN', '=', programId)]))
|
|
60
|
+
.where('r.ZSOFTDELETED', 'is not', 1)
|
|
61
|
+
.where('eq.ZMEASURMENTUNIT', 'is not', null)
|
|
62
|
+
.execute();
|
|
63
|
+
const normalized = new Set(equipmentUnits.map((row) => normalizeUnitPreference(row.unit)));
|
|
64
|
+
if (normalized.has('imperial'))
|
|
65
|
+
return 'imperial';
|
|
66
|
+
if (normalized.has('metric'))
|
|
67
|
+
return 'metric';
|
|
68
|
+
return 'unknown';
|
|
69
|
+
}
|
|
70
|
+
export async function resolveExerciseWeightUnit(db, exerciseId) {
|
|
71
|
+
const settingsUnit = await resolveGlobalWeightUnit(db);
|
|
72
|
+
if (settingsUnit !== 'unknown')
|
|
73
|
+
return settingsUnit;
|
|
74
|
+
const row = await db
|
|
75
|
+
.selectFrom('ZEXERCISEINFORMATION as ei')
|
|
76
|
+
.leftJoin('ZEQUIPMENT2 as eq', 'eq.Z_PK', 'ei.ZEQUIPMENT')
|
|
77
|
+
.select('eq.ZMEASURMENTUNIT as unit')
|
|
78
|
+
.where('ei.Z_PK', '=', exerciseId)
|
|
79
|
+
.executeTakeFirst();
|
|
80
|
+
return normalizeUnitPreference(row?.unit ?? null);
|
|
81
|
+
}
|