fixed dynamic TDEE

This commit is contained in:
2026-06-16 08:04:15 +03:00
parent a3f01cd850
commit 0f2827030b
11 changed files with 603 additions and 18 deletions
+14
View File
@@ -293,6 +293,12 @@ export interface FitnessTdeeBreakdown {
steps: number;
}
export interface FitnessTdeeExpected extends FitnessTdeeBreakdown {
source: "weekly_avg" | "baseline" | "defaults";
lookback_days: number;
days_with_data: number;
}
export interface FitnessTargets {
calories: number;
protein_g: number;
@@ -338,6 +344,10 @@ export interface FitnessProfile {
goal?: string;
target_weight_kg?: number | null;
neat_base_kcal?: number;
activity_level?: string;
weekly_workouts?: number;
baseline_steps?: number | null;
baseline_workout_kcal?: number | null;
calorie_target?: number;
protein_g?: number;
fat_g?: number;
@@ -387,7 +397,9 @@ export interface FitnessDailySummary {
steps?: number;
};
targets: FitnessTargets;
targets_expected?: FitnessTargets;
tdee_breakdown?: FitnessTdeeBreakdown;
tdee_expected?: FitnessTdeeExpected;
steps?: StepLogItem[];
steps_total?: number;
meals: FoodLogItem[];
@@ -434,7 +446,9 @@ export interface FitnessDayOverview {
has_data: boolean;
totals: FitnessDailySummary["totals"];
targets: FitnessDailySummary["targets"];
targets_expected?: FitnessTargets;
tdee_breakdown?: FitnessTdeeBreakdown;
tdee_expected?: FitnessTdeeExpected;
meal_count: number;
workout_count: number;
}
+10 -1
View File
@@ -2,7 +2,16 @@ import { useMemo } from "react";
import { FitnessChartPoint, FitnessChartSeries, FitnessChartsResponse } from "../api/client";
import "./FitnessCharts.css";
const CHART_KEYS = ["weight_kg", "calories", "protein_g", "water_l", "steps", "body_fat_pct"] as const;
const CHART_KEYS = [
"weight_kg",
"calories",
"tdee",
"tdee_expected",
"protein_g",
"water_l",
"steps",
"body_fat_pct",
] as const;
interface MetricChartProps {
series: FitnessChartSeries;
+12
View File
@@ -385,6 +385,18 @@
border-radius: 0 4px 4px 0;
}
.fitness-progress-plan-marker {
position: absolute;
top: -2px;
width: 2px;
height: calc(100% + 4px);
background: #c9a227;
border-radius: 1px;
transform: translateX(-50%);
z-index: 3;
pointer-events: none;
}
.fitness-activity-block {
margin-bottom: 0.75rem;
font-size: 0.85rem;
+48 -5
View File
@@ -38,26 +38,38 @@ function ProgressBar({
label,
current,
target,
targetPlan,
unit,
}: {
label: string;
current: number;
target: number;
targetPlan?: number;
unit: string;
}) {
const scaleMax = Math.max(target, current, 1);
const showPlan = targetPlan != null && Math.abs(targetPlan - target) >= 1;
const scaleMax = Math.max(target, targetPlan ?? target, current, 1);
const fillPct = (Math.min(current, scaleMax) / scaleMax) * 100;
const overflowPct = current > target ? ((current - target) / scaleMax) * 100 : 0;
const planPct = showPlan ? (Math.min(targetPlan!, scaleMax) / scaleMax) * 100 : 0;
return (
<div className="fitness-progress">
<div className="fitness-progress-header">
<span>{label}</span>
<span>
{current.toFixed(0)}/{target.toFixed(0)} {unit}
{current.toFixed(0)}/{target.toFixed(0)}
{showPlan ? ` · план ${targetPlan!.toFixed(0)}` : ""} {unit}
</span>
</div>
<div className="fitness-progress-track fitness-progress-track-v2">
<div className="fitness-progress-fill" style={{ width: `${fillPct}%` }} />
{showPlan ? (
<div
className="fitness-progress-plan-marker"
style={{ left: `${planPct}%` }}
title={`План: ${targetPlan!.toFixed(0)} ${unit}`}
/>
) : null}
{overflowPct > 0 ? (
<div className="fitness-progress-overflow" style={{ width: `${overflowPct}%` }} />
) : null}
@@ -66,6 +78,15 @@ function ProgressBar({
);
}
function expectedSourceLabel(source: string, daysWithData: number, lookbackDays: number) {
if (source === "weekly_avg") {
return `среднее за ${lookbackDays} дн., ${daysWithData} дн. с данными`;
}
if (source === "baseline") return "baseline профиля";
if (source === "defaults") return "по activity_level";
return source;
}
export default function Fitness() {
const [snapshot, setSnapshot] = useState<FitnessSnapshot | null>(null);
const [selectedDate, setSelectedDate] = useState(todayIso);
@@ -168,7 +189,9 @@ export default function Fitness() {
const totals = daySummary?.totals;
const targets = daySummary?.targets;
const targetsExpected = daySummary?.targets_expected;
const tdeeBreakdown = daySummary?.tdee_breakdown;
const tdeeExpected = daySummary?.tdee_expected;
const workoutStats = snapshot?.workout_stats;
const latestMetric: BodyMetric | undefined = snapshot?.body_metrics?.[0];
const isToday = selectedDate === todayIso();
@@ -248,14 +271,32 @@ export default function Fitness() {
{tdeeBreakdown ? (
<div className="fitness-activity-block">
<p>
TDEE: BMR {tdeeBreakdown.bmr} + NEAT {tdeeBreakdown.neat_kcal} + шаги{" "}
<strong>TDEE факт:</strong> BMR {tdeeBreakdown.bmr} + NEAT {tdeeBreakdown.neat_kcal} + шаги{" "}
{tdeeBreakdown.steps_kcal} ({daySummary?.steps_total ?? tdeeBreakdown.steps}) + тренировки{" "}
{tdeeBreakdown.workout_kcal} = {tdeeBreakdown.tdee} ккал
</p>
<p>Цель ккал: {tdeeBreakdown.calorie_target}</p>
<p>Цель ккал (факт): {tdeeBreakdown.calorie_target}</p>
{tdeeExpected ? (
<>
<p>
<strong>TDEE план:</strong> BMR {tdeeExpected.bmr} + NEAT {tdeeExpected.neat_kcal} + шаги{" "}
{tdeeExpected.steps_kcal} (~{tdeeExpected.steps}) + тренировки {tdeeExpected.workout_kcal} ={" "}
{tdeeExpected.tdee} ккал
</p>
<p>
Цель ккал (план): {tdeeExpected.calorie_target}
{" · "}
{expectedSourceLabel(
tdeeExpected.source,
tdeeExpected.days_with_data,
tdeeExpected.lookback_days,
)}
</p>
</>
) : null}
{!daySummary?.steps_total && !daySummary?.workouts?.length ? (
<p className="fitness-hint">
Шаги и тренировки не внесены TDEE = BMR + NEAT. Внесите данные через чат для точной цели.
Шаги и тренировки не внесены факт = BMR + NEAT. План основан на средней активности за неделю.
</p>
) : null}
</div>
@@ -276,6 +317,7 @@ export default function Fitness() {
label="Калории"
current={totals.calories}
target={targets.calories}
targetPlan={targetsExpected?.calories}
unit="ккал"
/>
<ProgressBar
@@ -294,6 +336,7 @@ export default function Fitness() {
label="Углеводы"
current={totals.carbs_g}
target={targets.carbs_g}
targetPlan={targetsExpected?.carbs_g}
unit="г"
/>
<ProgressBar