fixed reminder

This commit is contained in:
2026-06-11 12:22:37 +03:00
parent 4108d737e3
commit 41cbef61a9
12 changed files with 410 additions and 59 deletions
+28
View File
@@ -168,9 +168,26 @@ export interface FitnessReminder {
enabled: boolean;
}
export interface FitnessDayOverview {
date: string;
has_data: boolean;
totals: FitnessDailySummary["totals"];
targets: FitnessDailySummary["targets"];
meal_count: number;
workout_count: number;
}
export interface FitnessHistory {
start_date: string;
end_date: string;
days: number;
summaries: FitnessDayOverview[];
}
export interface FitnessSnapshot {
profile: FitnessProfile | null;
today: FitnessDailySummary;
history?: FitnessHistory;
body_metrics: BodyMetric[];
reminders: FitnessReminder[];
}
@@ -355,6 +372,17 @@ export const api = {
getFitnessSnapshot: () => request<FitnessSnapshot>("/api/v1/fitness"),
getFitnessSummary: (day?: string) =>
request<FitnessDailySummary>(
`/api/v1/fitness/summary${day ? `?day=${encodeURIComponent(day)}` : ""}`
),
getFitnessHistory: (days = 7, end?: string) => {
const params = new URLSearchParams({ days: String(days) });
if (end) params.set("end", end);
return request<FitnessHistory>(`/api/v1/fitness/history?${params}`);
},
updateFitnessProfile: (updates: Partial<FitnessProfile>) =>
request<{ ok: boolean; profile: FitnessProfile }>("/api/v1/fitness/profile", {
method: "PUT",
+83
View File
@@ -37,6 +37,89 @@
border-radius: 8px;
}
.fitness-day-nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.fitness-day-nav button {
width: 2rem;
height: 2rem;
border-radius: 8px;
border: 1px solid #2f3748;
background: #1a1f2b;
color: inherit;
cursor: pointer;
}
.fitness-day-nav button:disabled {
opacity: 0.4;
cursor: default;
}
.fitness-day-title {
text-align: center;
flex: 1;
}
.fitness-day-title h3 {
margin: 0 0 0.25rem;
}
.fitness-day-title input[type="date"] {
border: 1px solid #2f3748;
background: #1a1f2b;
color: inherit;
border-radius: 6px;
padding: 0.2rem 0.4rem;
font-size: 0.85rem;
}
.fitness-week-strip {
display: flex;
gap: 0.35rem;
overflow-x: auto;
margin-bottom: 1rem;
padding-bottom: 0.25rem;
}
.fitness-week-day {
flex: 0 0 auto;
min-width: 3.2rem;
border: 1px solid #2f3748;
border-radius: 8px;
background: #1a1f2b;
color: inherit;
padding: 0.35rem 0.4rem;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
}
.fitness-week-day.active {
border-color: #4a7cff;
background: #1c2740;
}
.fitness-week-day.empty .fitness-week-kcal {
color: #6b7280;
}
.fitness-week-date {
font-size: 0.75rem;
color: #8b95a8;
}
.fitness-week-kcal {
font-size: 0.8rem;
font-weight: 600;
}
.fitness-section {
background: #151922;
border: 1px solid #2a2f3a;
+107 -21
View File
@@ -1,12 +1,35 @@
import { FormEvent, useCallback, useEffect, useState } from "react";
import {
api,
FitnessDailySummary,
FitnessHistory,
FitnessProfile,
FitnessReminder,
FitnessSnapshot,
} from "../api/client";
import "./Fitness.css";
function todayIso() {
return new Date().toISOString().slice(0, 10);
}
function shiftDate(iso: string, deltaDays: number) {
const d = new Date(`${iso}T12:00:00`);
d.setDate(d.getDate() + deltaDays);
return d.toISOString().slice(0, 10);
}
function formatDayLabel(iso: string) {
const today = todayIso();
if (iso === today) return "Сегодня";
if (iso === shiftDate(today, -1)) return "Вчера";
return new Date(`${iso}T12:00:00`).toLocaleDateString("ru-RU", {
weekday: "short",
day: "numeric",
month: "short",
});
}
function ProgressBar({ label, current, target, unit }: {
label: string;
current: number;
@@ -31,27 +54,41 @@ function ProgressBar({ label, current, target, unit }: {
export default function Fitness() {
const [snapshot, setSnapshot] = useState<FitnessSnapshot | null>(null);
const [selectedDate, setSelectedDate] = useState(todayIso);
const [daySummary, setDaySummary] = useState<FitnessDailySummary | null>(null);
const [history, setHistory] = useState<FitnessHistory | null>(null);
const [profile, setProfile] = useState<Partial<FitnessProfile>>({});
const [message, setMessage] = useState("");
const [showRaw, setShowRaw] = useState(false);
const [loading, setLoading] = useState(false);
const load = useCallback(async () => {
const load = useCallback(async (day: string = selectedDate) => {
setLoading(true);
try {
const data = await api.getFitnessSnapshot();
const [data, summary, hist] = await Promise.all([
api.getFitnessSnapshot(),
api.getFitnessSummary(day),
api.getFitnessHistory(7, day),
]);
setSnapshot(data);
setDaySummary(summary);
setHistory(hist);
if (data.profile) setProfile(data.profile);
setMessage("");
} catch (err) {
setMessage(err instanceof Error ? err.message : "Ошибка загрузки");
} finally {
setLoading(false);
}
}, []);
}, [selectedDate]);
useEffect(() => {
load().catch(console.error);
}, [load]);
load(selectedDate).catch(console.error);
}, [selectedDate, load]);
const pickDay = (iso: string) => {
setSelectedDate(iso);
};
const handleProfileSave = async (e: FormEvent) => {
e.preventDefault();
@@ -83,9 +120,9 @@ export default function Fitness() {
await load();
};
const today = snapshot?.today;
const totals = today?.totals;
const targets = today?.targets;
const totals = daySummary?.totals;
const targets = daySummary?.targets;
const isToday = selectedDate === todayIso();
return (
<div className="fitness-page">
@@ -95,7 +132,7 @@ export default function Fitness() {
<p>Дневник, цели, напоминания</p>
</div>
<div className="fitness-header-actions">
<button type="button" onClick={() => load()} disabled={loading}>
<button type="button" onClick={() => load(selectedDate)} disabled={loading}>
{loading ? "…" : "Обновить"}
</button>
<button type="button" onClick={() => setShowRaw((v) => !v)}>
@@ -111,7 +148,52 @@ export default function Fitness() {
) : (
<>
<section className="fitness-section">
<h3>Сегодня</h3>
<div className="fitness-day-nav">
<button type="button" onClick={() => pickDay(shiftDate(selectedDate, -1))}>
</button>
<div className="fitness-day-title">
<h3>{formatDayLabel(selectedDate)}</h3>
<input
type="date"
value={selectedDate}
onChange={(e) => pickDay(e.target.value)}
aria-label="Выбор дня"
/>
</div>
<button
type="button"
onClick={() => pickDay(shiftDate(selectedDate, 1))}
disabled={selectedDate >= todayIso()}
>
</button>
</div>
{history && history.summaries.length > 0 && (
<div className="fitness-week-strip">
{history.summaries.map((row) => (
<button
key={row.date}
type="button"
className={`fitness-week-day${row.date === selectedDate ? " active" : ""}${row.has_data ? "" : " empty"}`}
onClick={() => pickDay(row.date)}
title={`${row.date}: ${row.totals.calories.toFixed(0)} ккал`}
>
<span className="fitness-week-date">
{new Date(`${row.date}T12:00:00`).toLocaleDateString("ru-RU", {
day: "numeric",
month: "numeric",
})}
</span>
<span className="fitness-week-kcal">
{row.has_data ? `${row.totals.calories.toFixed(0)}` : "—"}
</span>
</button>
))}
</div>
)}
{totals && targets ? (
<div className="fitness-progress-grid">
<ProgressBar
@@ -146,7 +228,7 @@ export default function Fitness() {
/>
</div>
) : (
<p className="fitness-empty">Нет данных за сегодня</p>
<p className="fitness-empty">Нет записей за этот день</p>
)}
</section>
@@ -230,33 +312,37 @@ export default function Fitness() {
</section>
<section className="fitness-section">
<h3>Логи за сегодня</h3>
<h3>Логи {isToday ? "за сегодня" : `за ${selectedDate}`}</h3>
<h4>Еда</h4>
<ul className="fitness-log-list">
{(today?.meals ?? []).map((m) => (
{(daySummary?.meals ?? []).map((m) => (
<li key={m.id}>
{m.estimated ? "≈" : ""}
{m.description} {m.calories} ккал
<button type="button" onClick={() => handleDeleteMeal(m.id)}>
×
</button>
{isToday && (
<button type="button" onClick={() => handleDeleteMeal(m.id)}>
×
</button>
)}
</li>
))}
</ul>
<h4>Вода</h4>
<ul className="fitness-log-list">
{(today?.water ?? []).map((w) => (
{(daySummary?.water ?? []).map((w) => (
<li key={w.id}>
+{w.amount_ml} мл
<button type="button" onClick={() => handleDeleteWater(w.id)}>
×
</button>
{isToday && (
<button type="button" onClick={() => handleDeleteWater(w.id)}>
×
</button>
)}
</li>
))}
</ul>
<h4>Тренировки</h4>
<ul className="fitness-log-list">
{(today?.workouts ?? []).map((w) => (
{(daySummary?.workouts ?? []).map((w) => (
<li key={w.id}>{w.title}</li>
))}
</ul>