Tre leve per migliorare l'accuratezza del piano oltre Mifflin-St Jeor.
1. Composizione corporea (US Navy + Katch-McArdle)
- Profile: neckCm/waistCm/hipsCm opzionali in input
- bodyFatPct calcolato lato server con la formula US Navy metrica
(Hodgdon-Beckett 1984) e cifrato at-rest come dato sanitario
- BMR usa Katch-McArdle (FFM-based) quando bodyFatPct è noto, Mifflin
altrimenti — più accurato del 5-10% sui casi fuori-norma
- Test: estimateBodyFatPercentageUSNavy + calculateBmrKatchMcArdle
2. Deficit dinamico
- Profile.targetWeeklyLossKg (kg/settimana, opzionale)
- computeDailyKcalTarget calcola il deficit da kg/sett ×7700/7 con due cap
di sicurezza: 30% TDEE (Schoenfeld 2014) e 1% peso/sett (Helms 2014)
- plan.routes lo usa al posto del -500/-200 hard-coded
- Test: 6 casi (mantenimento, transizione, intensiva, cap peso, cap TDEE,
default mancante)
3. Condizioni mediche strutturate
- Profile.medicalConditions (array JSON cifrato)
- 11 condizioni: 7 adattabili (tiroide → BMR -10%, diabete II, IBS,
dislipidemia, ipertensione, reni → cap proteine 0.8 g/kg, fegato →
1.0 g/kg) + 4 escludenti (gravidanza, allattamento, diabete I, disturbi
alimentari)
- PATCH /me/profile/conditions per salvataggio mirato dall'onboarding
- hasExcludingCondition: plan.routes restituisce 409 medical_block se
attiva una condizione incompatibile
Onboarding allargato a 6 step: aggiunto "Condizioni mediche" tra Profile
e Preferences. Step labels e numeri rinumerati.
Schema migration 20260429203451_add_profile_health_fields.
44/44 unit test verdi.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
2.5 KiB
TypeScript
72 lines
2.5 KiB
TypeScript
// PRD §5.1 — Calcolo dinamico del deficit calorico.
|
||
//
|
||
// 1 kg di tessuto adiposo ≈ 7700 kcal (regola classica). Quindi un calo di
|
||
// `r` kg/settimana richiede un deficit medio di r × 7700 / 7 kcal/giorno.
|
||
//
|
||
// Limiti di sicurezza:
|
||
// - max deficit ≤ 30% del TDEE (linee guida Schoenfeld 2014)
|
||
// - max calo ≤ 1% peso corporeo / settimana (raccomandazione Helms 2014)
|
||
// Se l'utente chiede oltre, capiamo il deficit e segnaliamo.
|
||
|
||
const KCAL_PER_KG_FAT = 7700;
|
||
const SAFE_DEFICIT_PCT = 0.3; // 30% TDEE
|
||
const SAFE_LOSS_PCT_BODYWEIGHT = 0.01; // 1% peso/settimana
|
||
|
||
export interface DeficitInput {
|
||
tdee: number; // kcal/giorno
|
||
weightCurrentKg: number;
|
||
/**
|
||
* Velocità desiderata di calo peso, kg/settimana. Se omessa, usiamo
|
||
* il fallback per fase passato dal chiamante (default 0.5 kg/wk per LOSE).
|
||
*/
|
||
targetWeeklyLossKg?: number | null;
|
||
/** 1=intensiva, 2=transizione, 3=mantenimento. */
|
||
phase: 1 | 2 | 3;
|
||
}
|
||
|
||
export interface DeficitResult {
|
||
/** kcal/giorno target. */
|
||
kcalTarget: number;
|
||
/** kg/settimana effettivamente raggiungibili col target. */
|
||
effectiveWeeklyLossKg: number;
|
||
/** True se il calo richiesto era oltre i limiti di sicurezza. */
|
||
cappedToSafe: boolean;
|
||
}
|
||
|
||
export function computeDailyKcalTarget(input: DeficitInput): DeficitResult {
|
||
const { tdee, weightCurrentKg, targetWeeklyLossKg, phase } = input;
|
||
|
||
// Fase 3 (mantenimento) → eat at TDEE.
|
||
if (phase === 3) {
|
||
return { kcalTarget: Math.round(tdee), effectiveWeeklyLossKg: 0, cappedToSafe: false };
|
||
}
|
||
|
||
// Fase 2 (transizione) → reverse dieting morbido.
|
||
if (phase === 2) {
|
||
return {
|
||
kcalTarget: Math.round(tdee - 200),
|
||
effectiveWeeklyLossKg: Math.max(0, 200 / (KCAL_PER_KG_FAT / 7)),
|
||
cappedToSafe: false,
|
||
};
|
||
}
|
||
|
||
// Fase 1 (intensiva): deficit derivato da targetWeeklyLossKg, con caps.
|
||
const requested = targetWeeklyLossKg ?? 0.5;
|
||
const safeMaxByWeight = weightCurrentKg * SAFE_LOSS_PCT_BODYWEIGHT;
|
||
const cappedRate = Math.min(requested, safeMaxByWeight);
|
||
|
||
let dailyDeficit = (cappedRate * KCAL_PER_KG_FAT) / 7;
|
||
const maxDeficitByTdee = tdee * SAFE_DEFICIT_PCT;
|
||
const finalDeficit = Math.min(dailyDeficit, maxDeficitByTdee);
|
||
dailyDeficit = finalDeficit;
|
||
|
||
const cappedToSafe =
|
||
requested > safeMaxByWeight || (cappedRate * KCAL_PER_KG_FAT) / 7 > maxDeficitByTdee;
|
||
|
||
return {
|
||
kcalTarget: Math.round(tdee - dailyDeficit),
|
||
effectiveWeeklyLossKg: Number(((dailyDeficit * 7) / KCAL_PER_KG_FAT).toFixed(2)),
|
||
cappedToSafe,
|
||
};
|
||
}
|