108 lines
3.0 KiB
TypeScript
108 lines
3.0 KiB
TypeScript
import type { ActivityLevel, DailyTargets, GoalType, Sex } from "@app/shared-types";
|
||
|
||
/**
|
||
* Energiberäkning enligt Mifflin–St Jeor. Detta är vägledning baserad på
|
||
* officiella rekommendationer – Appen är inte medicinsk rådgivning (spec §6, §21).
|
||
*/
|
||
|
||
export interface EnergyProfile {
|
||
sex: Sex;
|
||
age: number;
|
||
heightCm: number;
|
||
weightKg: number;
|
||
activityLevel: ActivityLevel;
|
||
primaryGoal?: GoalType | undefined;
|
||
}
|
||
|
||
const ACTIVITY_FACTORS: Record<ActivityLevel, number> = {
|
||
sedentary: 1.2,
|
||
light: 1.375,
|
||
moderate: 1.55,
|
||
active: 1.725,
|
||
very_active: 1.9,
|
||
};
|
||
|
||
/** Justering per mål (kcal/dag). Konservativa, väldokumenterade nivåer. */
|
||
const GOAL_ADJUSTMENTS: Partial<Record<GoalType, number>> = {
|
||
lose_weight: -500,
|
||
gain_weight: 400,
|
||
build_muscle: 250,
|
||
maintain_weight: 0,
|
||
};
|
||
|
||
/** Lägsta rekommenderade energiintag – under detta kapas aldrig målet (säkerhetsgolv). */
|
||
const MIN_KCAL = 1200;
|
||
|
||
export function bmrMifflinStJeor(
|
||
p: Pick<EnergyProfile, "sex" | "age" | "heightCm" | "weightKg">,
|
||
): number {
|
||
const base = 10 * p.weightKg + 6.25 * p.heightCm - 5 * p.age;
|
||
if (p.sex === "male") return base + 5;
|
||
if (p.sex === "female") return base - 161;
|
||
// Ospecificerat: medelvärde av formlerna, transparent redovisat i UI.
|
||
return base - 78;
|
||
}
|
||
|
||
export function tdee(p: EnergyProfile): number {
|
||
return bmrMifflinStJeor(p) * ACTIVITY_FACTORS[p.activityLevel];
|
||
}
|
||
|
||
export interface DailyTargetsResult {
|
||
targets: DailyTargets;
|
||
basis: {
|
||
bmrKcal: number;
|
||
tdeeKcal: number;
|
||
goalAdjustmentKcal: number;
|
||
activityLevel: ActivityLevel;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Dagsmål: energi via TDEE + måljustering; protein per kg kroppsvikt;
|
||
* fett som andel av energi; kolhydrater = resten; fiber 3 g/MJ (nordisk rekommendation);
|
||
* salt max 6 g/dag.
|
||
*/
|
||
export function computeDailyTargets(p: EnergyProfile): DailyTargetsResult {
|
||
const bmr = bmrMifflinStJeor(p);
|
||
const maintenance = tdee(p);
|
||
const adjustment = GOAL_ADJUSTMENTS[p.primaryGoal ?? "maintain_weight"] ?? 0;
|
||
const kcal = Math.max(MIN_KCAL, Math.round(maintenance + adjustment));
|
||
|
||
const proteinPerKg =
|
||
p.primaryGoal === "build_muscle" || p.primaryGoal === "more_protein"
|
||
? 1.8
|
||
: p.primaryGoal === "lose_weight"
|
||
? 1.6
|
||
: 1.2;
|
||
const proteinG = Math.round(p.weightKg * proteinPerKg);
|
||
|
||
const fatShare = p.primaryGoal === "less_fat" ? 0.25 : 0.3;
|
||
const fatG = Math.round((kcal * fatShare) / 9);
|
||
|
||
const carbsKcal = Math.max(0, kcal - proteinG * 4 - fatG * 9);
|
||
const carbsG = Math.round(carbsKcal / 4);
|
||
|
||
// 3 g fiber per MJ (1 MJ ≈ 239 kcal)
|
||
const fiberG = Math.round((kcal / 239) * 3);
|
||
|
||
return {
|
||
targets: { kcal, proteinG, carbsG, fatG, fiberG, saltMaxG: 6 },
|
||
basis: {
|
||
bmrKcal: Math.round(bmr),
|
||
tdeeKcal: Math.round(maintenance),
|
||
goalAdjustmentKcal: adjustment,
|
||
activityLevel: p.activityLevel,
|
||
},
|
||
};
|
||
}
|
||
|
||
/** Standardmål när profil saknas (visas tydligt som schablon i appen). */
|
||
export const DEFAULT_TARGETS: DailyTargets = {
|
||
kcal: 2000,
|
||
proteinG: 80,
|
||
carbsG: 220,
|
||
fatG: 67,
|
||
fiberG: 25,
|
||
saltMaxG: 6,
|
||
};
|