fixed dynamic TDEE
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user