feat: předělání historie a profilu
This commit is contained in:
1 parent
9e72f431a1
commit
8fa7f601f2
97 files changed
+7502
-1034
No files matched your search
@@ -59,8 +59,10 @@ cd client && yarn build # tsc --noEmit + vite build → client/dist
|
||||
- **Entry:** `index.ts` — Express app, auth middleware, error middleware
|
||||
- **Routes:** `routes/` — `dayRoutes`, `mealRoutes`, `activityRoutes`, `workoutRoutes`,
|
||||
`statsRoutes`, `calorieRoutes`, `settingsRoutes`, `importRoutes`
|
||||
- **Domain:** `meals.ts` (jídlo), `activities.ts` (pohyb), `workouts.ts` (šablony
|
||||
tréninků), `sleep.ts` (spánek), `profile.ts` (tělesné údaje a výpočet BMR),
|
||||
- **Domain:** `meals.ts` (jídlo a historie jídel), `nutrients.ts` (živiny),
|
||||
`mealTemplates.ts` (recepty), `activities.ts` (pohyb), `workouts.ts` (šablony
|
||||
tréninků), `sleep.ts` (spánek), `weights.ts` (vážení po dnech), `profile.ts`
|
||||
(tělesné údaje a výpočet BMR), `bodyMetrics.ts` (BMI, podíl tuku, cíle),
|
||||
`dayOverview.ts` (spojení dne a bilance), `statsService.ts` (agregace),
|
||||
`luncherImport.ts` (parsování exportů z Luncheru), `settings.ts`, `calories.ts`
|
||||
- **Auth:** `auth.ts` — JWT + volitelná autentizace z hlavičky reverzní proxy
|
||||
@@ -71,10 +73,12 @@ cd client && yarn build # tsc --noEmit + vite build → client/dist
|
||||
### Client (client/src/)
|
||||
- **Entry:** `index.tsx` → `AppRoutes.tsx`; `Login.tsx` je přihlašovací obrazovka
|
||||
- **Pages:** `pages/` — `DayPage` (přehled dne se záložkami Příjem/Výdej),
|
||||
`TemplatesPage` (šablony tréninků), `ProfilePage` (tělesné údaje a klidový výdej),
|
||||
`TemplatesPage` (šablony tréninků a recepty), `ProfilePage` (Profil — ukazatele,
|
||||
vývoj hmotnosti, tělesné údaje, cíle a klidový výdej),
|
||||
`StatsPage` (statistiky příjmu a výdeje + import; výdej v `ActivityStatsPanel`)
|
||||
- **Components:** `components/` (Header, CalorieLookup) a `components/modals/`
|
||||
(MealModal, ActivityModal, ApplyWorkoutModal, ApplyMealModal, SettingsModal, ImportModal)
|
||||
- **Components:** `components/` (Header, CalorieLookup, MealSuggestions, NutrientsFieldset)
|
||||
a `components/modals/` (MealModal, ActivityModal, ApplyWorkoutModal, ApplyMealModal —
|
||||
výběr z historie a receptů, SettingsModal, ImportModal)
|
||||
- **Klientské enumy** patří do `enums.ts` jako TypeScript `enum` s mapou popisků,
|
||||
ne jako inline union u komponenty (`DayTab`, `StatsRange`)
|
||||
- **Context:** `context/auth.tsx` (JWT), `context/settings.tsx` (světlý/tmavý motiv)
|
||||
@@ -95,6 +99,12 @@ cd client && yarn build # tsc --noEmit + vite build → client/dist
|
||||
nezapočítávají.
|
||||
- Gramáž (`weight`, g) a jednotkové hodnoty (`pricePer100g` v haléřích za 100 g,
|
||||
`caloriesPer100g` v kcal) umožňují dopočet — viz níže.
|
||||
- Nutriční hodnoty (`nutrientsPer100g`: bílkoviny, sacharidy, z toho cukry, tuky,
|
||||
z toho nasycené, vláknina, sůl) jsou volitelné, v gramech na 100 g jako na etiketě.
|
||||
**Jako jediné nejsou celočíselné** — ukládají se na desetiny gramu, u soli
|
||||
a vlákniny by celé gramy smazaly celou hodnotu. Hodnoty porce se dopočtou
|
||||
z gramáže a násobku (`getPortionNutrients` v `nutrients.ts`), takže jídlo bez
|
||||
gramáže do součtu dne (`DayRecord.totalNutrients`) nevstupuje.
|
||||
|
||||
## Gramáž a kalorie
|
||||
|
||||
@@ -111,8 +121,9 @@ uživatel vybere zdroj.
|
||||
|
||||
`client/src/Utils.tsx` má stejnojmennou funkci pro živý náhled ve formuláři.
|
||||
**Když se změní jedna, musí se změnit i druhá** — jinak klient ukazuje něco jiného,
|
||||
než server uloží. Totéž platí pro `applyMultiplier` (`server/src/utils.ts`)
|
||||
a `deriveRecipe` (`server/src/mealTemplates.ts`).
|
||||
než server uloží. Totéž platí pro `applyMultiplier` (`server/src/utils.ts`),
|
||||
`deriveRecipe` (`server/src/mealTemplates.ts`) a `getPortionNutrients`
|
||||
(`server/src/nutrients.ts`).
|
||||
|
||||
## Násobek položky dne
|
||||
|
||||
@@ -170,10 +181,9 @@ trénink než 120 v kuse, i když spálené kalorie vyjdou stejně. Jedna série
|
||||
výchozí stav a neukládá se.
|
||||
|
||||
Opačný směr k použití šablony je `createWorkoutTemplateFromActivities` —
|
||||
z aktivit odcvičeného dne (všech, nebo vybraných) vznikne šablona. U jídla to
|
||||
umí `createMealTemplateFromMeal`, které navíc **přepíše šablonu stejného názvu**;
|
||||
opakované "uložit jako šablonu" u téhož jídla má dát jednu šablonu, ne několik
|
||||
stejných. Záznamy ve dni zůstanou v obou případech beze změny.
|
||||
z aktivit odcvičeného dne (všech, nebo vybraných) vznikne šablona. Záznamy ve dni
|
||||
zůstanou beze změny. U jídla nic takového není, opakovaně jedená jídla se nabízejí
|
||||
z historie (viz Historie jídel a recepty).
|
||||
|
||||
**Dodatečná zátěž** (`extraWeightGrams` u aktivity i cviku šablony, v gramech)
|
||||
je jen informace o tréninku, kalorie **nemění** — žádný jednoduchý vzorec
|
||||
@@ -198,13 +208,29 @@ Bez nastaveného klidového výdeje porovnává bilance jen jídlo proti pohybu
|
||||
**to není skutečný deficit**, proto se to přes `hasBasal: false` propisuje do UI,
|
||||
aby to číslo nikoho nemátlo.
|
||||
|
||||
Klidový výdej se bere z `settings.effectiveBasalCalories`: ručně zadaný
|
||||
`basalCalories` má přednost před hodnotou spočítanou z tělesných údajů
|
||||
(`computeBasalCalories` v `profile.ts`, rovnice Mifflin–St Jeor). Výpočet
|
||||
Klidový výdej: ručně zadaný `basalCalories` má přednost před hodnotou spočítanou
|
||||
z tělesných údajů (`computeBasalCalories` v `profile.ts`, rovnice Mifflin–St Jeor).
|
||||
`getDayOverview` ho počítá s **hmotností platnou k danému dni** (poslední vážení
|
||||
v ten den nebo dřív), takže starší dny se se zhubnutím zpětně nepřepočítají;
|
||||
`settings.computedBasalCalories` je hodnota z posledního vážení. Výpočet
|
||||
**záměrně nenásobí koeficientem fyzické aktivity (PAL)**, jak to dělají kalkulačky
|
||||
TDEE — pohyb se eviduje zvlášť a přičítá se, takže vynásobení by ho započítalo
|
||||
dvakrát.
|
||||
|
||||
**Vážení** (`weights.ts`, klíč `weights:<login>` — mapa den → gramy) se zapisuje
|
||||
v přehledu dne nebo na stránce Profil. Hmotnost v profilu (`profile.weightGrams`)
|
||||
je pozůstatek z doby před vážením po dnech a použije se, jen dokud uživatel
|
||||
žádné vážení nemá; klient ji při uložení profilu posílá zpět beze změny.
|
||||
|
||||
**Ukazatele** (`bodyMetrics.ts`, `UserSettings.bodyMetrics`) počítá server, nic
|
||||
z nich se neukládá: BMI s pásmem podle WHO, zdravé rozmezí hmotnosti (BMI
|
||||
18,5–25), podíl tuku metodou US Navy z obvodů (muž pas a krk, žena navíc boky)
|
||||
a z cílů (`Goals`) cílová denní bilance a bílkoviny. Tempo počítá s **7 700 kcal
|
||||
na kilogram** — −0,5 kg týdně je −550 kcal denně. Cílová bilance
|
||||
(`EnergyBalance.targetBalance`) i cíl bílkovin (`DayOverview.proteinTargetGrams`)
|
||||
jsou jen informace pro uživatele, do bilance nevstupují. Popisky tempa ve výběru
|
||||
na stránce Profil počítají kcal/den se stejnou konstantou.
|
||||
|
||||
**Spánek** (`sleep.ts`, klíč `sleep:<login>:<datum>`) se eviduje jako kontext dne
|
||||
a do bilance **nevstupuje**. Klidový výdej je hodnota za celých 24 hodin včetně
|
||||
spánku a žádná ze standardních rovnic délku spánku jako proměnnou nemá — přičítat
|
||||
@@ -226,37 +252,54 @@ přesunutý z jiného dne) se počítají jako splněné přesně dle plánu.
|
||||
`GET /api/day?date=` vrací `DayOverview` se vším naráz (jídlo, pohyb, bilance),
|
||||
takže `DayPage` si vystačí s jedním voláním.
|
||||
|
||||
## Šablony jídel
|
||||
## Historie jídel a recepty
|
||||
|
||||
`MealTemplate` (`mealTemplates.ts`, klíč `mealTemplates:<login>`) je opakovaně
|
||||
jedené jídlo. Na rozdíl od šablon tréninků se **neskládá z jiných**. Použitím
|
||||
vznikne běžný `MealEntry` přes `addMeal`, takže gramáž, cenu i kalorie dopočítá
|
||||
stejná logika jako u ručně zadaného jídla. Při zakládání jídla v přehledu dne se
|
||||
šablony nabízejí k rozepsanému názvu (`TemplateSuggestions`).
|
||||
Hotová jídla (objednaná, koupená) **nemají šablony**. Při zakládání jídla se
|
||||
k rozepsanému názvu nabízí historie: `getMealHistory` v `meals.ts`
|
||||
(`GET /api/meals/history`) projde všechna jídla uživatele a každý název vrátí
|
||||
jednou (`MealSuggestion`). Názvy se porovnávají znormalizované přes
|
||||
`normalizeName`, hodnoty jsou z **posledního** záznamu toho jména (cena i gramáž
|
||||
se časem mění) a řadí se od nejčastěji jedených.
|
||||
|
||||
Šablona má druh (`kind`):
|
||||
Návrh nese jen to, co popisuje jídlo samo — **chod, datum, násobek ani poznámku
|
||||
ne**, ty patří ke konkrétnímu dni. Gramáž dopočtená z ceny se vynechá, aby se
|
||||
v novém záznamu dopočetla z jeho ceny; ruční kalorie jen tam, kde nejdou
|
||||
dopočítat z gramáže. Jinak by se z dopočtené hodnoty stala ručně zadaná.
|
||||
Když poslední záznam kalorie nemá (typicky import), doplní se do návrhu
|
||||
z dřívějšího přes `createNutritionFiller`, stejně jako při importu.
|
||||
|
||||
- **HOTOVE** — objednané nebo koupené jídlo. Nese jen výsledné hodnoty porce:
|
||||
buď `calories`, nebo `caloriesPer100g` + `weight`; obojí projde `deriveAmounts`.
|
||||
- **RECEPT** — vařené jídlo se surovinami (`ingredients`), počtem porcí
|
||||
(`servings`), hmotností hotového jídla (`cookedWeight`) a postupem v `note`.
|
||||
`deriveRecipe` z nich při uložení spočítá hodnoty **jedné porce** a uloží je
|
||||
do stejných plochých polí (`weight`, `caloriesPer100g`, `calories`, `price`),
|
||||
takže použití šablony je pro oba druhy stejné. Energie na 100 g se bere
|
||||
z hmotnosti hotového jídla, ne ze součtu surovin — vařením se voda odpaří
|
||||
nebo nasákne. Postup se do záznamu dne nekopíruje.
|
||||
Návrhy se nabízejí i při **úpravě** jídla, ale až po změně názvu, a doplní jen
|
||||
prázdná pole (`mergeSuggestion` v `MealModal`) — slouží k přejmenování
|
||||
naimportovaného jídla, jehož cena a zdroj jsou skutečné. Po uložení jídla
|
||||
s kaloriemi `DayPage` nabídne (`FillSameNameOffer`) doplnit stejné hodnoty do
|
||||
ostatních jídel stejného názvu, která je nemají (`fillSameNameMeals`,
|
||||
`POST /api/meals/fillSameName`); jídla s kaloriemi se nepřepisují.
|
||||
|
||||
Šablony uložené dřív, než druhy přibyly, nemají `kind`; `withKind` jim ho při
|
||||
načtení doplní na `HOTOVE`, v úložišti se nic nepřepisuje.
|
||||
`createMealTemplateFromMeal` recept stejného názvu **nepřepíše** (přišel by
|
||||
o suroviny a postup) a odmítne to chybou.
|
||||
Pole Zdroj (`SourceInput`) nabízí zdroje z historie (`getMealSources`,
|
||||
`GET /api/meals/sources`, nejčastější první) a podniky se sazbou z nastavení;
|
||||
psát jde i nový.
|
||||
|
||||
Jídlo založené ze šablony nese `templateId`. Nastavuje ho jen server
|
||||
`MealTemplate` (`mealTemplates.ts`, klíč `mealTemplates:<login>`) je **recept** —
|
||||
vařené jídlo se surovinami (`ingredients`), počtem porcí (`servings`), hmotností
|
||||
hotového jídla (`cookedWeight`) a postupem v `note`. `deriveRecipe` z nich při
|
||||
uložení spočítá hodnoty **jedné porce** a uloží je do plochých polí (`weight`,
|
||||
`caloriesPer100g`, `calories`, `price`); použitím vznikne běžný `MealEntry` přes
|
||||
`addMeal`. Energie na 100 g se bere z hmotnosti hotového jídla, ne ze součtu
|
||||
surovin — vařením se voda odpaří nebo nasákne. Postup se do záznamu dne nekopíruje.
|
||||
Recepty se nabízejí v našeptávání i ve výběru z historie (`ApplyMealModal`)
|
||||
a jídlo stejného jména z historie se vedle receptu nenabízí podruhé.
|
||||
|
||||
Dřív pod stejným klíčem ležely i šablony hotových jídel (`kind: HOTOVE`, nebo
|
||||
bez `kind`). Ty zanikly: `loadRecipes` je při čtení vynechá (poznají se podle
|
||||
toho, že nemají suroviny) a první zápis receptu je z úložiště odklidí. **Jídla
|
||||
z nich založená ve dnech zůstávají**, dál nesou `templateId` zaniklé šablony.
|
||||
|
||||
Jídlo založené z receptu nese `templateId`. Nastavuje ho jen server
|
||||
(`addMeal` s třetím parametrem) — `MealInput` ho nemá, takže ho klient
|
||||
nepodstrčí a úprava jídla ho zachová.
|
||||
|
||||
Kontrakt má vlastní `MealTemplateInput`, kde je `id` volitelné — `MealTemplate`
|
||||
ho vyžaduje a nešlo by jím posílat novou šablonu.
|
||||
Kontrakt má vlastní `MealTemplateInput`, kde je `id` volitelné a hodnoty porce
|
||||
chybí — dopočítá je server.
|
||||
|
||||
## Import z Luncheru
|
||||
|
||||
@@ -275,6 +318,11 @@ ve všech třech formátech, které Luncher exportuje (XLSX, CSV, JSON — viz
|
||||
- `source` = obchod objednávky, jinak podnik. Stavy volby z `NON_PLACE_TYPES`
|
||||
("Budu objednávat", "Rozhoduji se", "Objednávka", "Mám vlastní/neobědvám")
|
||||
nejsou místa, takže se jako zdroj nepoužijí a záznam zůstane bez zdroje.
|
||||
- Luncher kalorie nezná, takže je `createNutritionFiller` (`meals.ts`) doplní
|
||||
z historie: vzorem je nejnovější jídlo **stejného názvu**, které energii nebo
|
||||
živiny má, přednostně ze stejného zdroje. Cena zůstává z importu. Sazba za
|
||||
100 g se přebírá jen ze stejného zdroje (gramáž se pak dopočte z nové ceny),
|
||||
jinak se vezme gramáž vzoru.
|
||||
- Duplicity řeší `importKey` složený z data, pořadí řádku v rámci dne, typu,
|
||||
jídla a částky — opakovaný import stejného měsíce nic nezduplikuje.
|
||||
- Režim (`ImportMode`): **PRIDAT** (výchozí) přeskočí už naimportované řádky;
|
||||
|
||||
Reference in new issue
Block a user