feat: import jen některých dnů

feat: násobení jídel/cviků
feat: refaktor napojování šablon
This commit is contained in:
Ondřej Anděl committed 2026-09-30 19:10:54 +02:00
1 parent c3990d80f8
commit 8dac67bdd1
37 files changed
+2314 -481

No files matched your search

+48 -10
View File
@@ -74,7 +74,7 @@ cd client && yarn build # tsc --noEmit + vite build → client/dist
`TemplatesPage` (šablony tréninků), `ProfilePage` (tělesné údaje a klidový výdej),
`StatsPage` (statistiky + import)
- **Components:** `components/` (Header, CalorieLookup) a `components/modals/`
(MealModal, ActivityModal, ApplyWorkoutModal, SettingsModal, ImportModal)
(MealModal, ActivityModal, ApplyWorkoutModal, ApplyMealModal, 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)
@@ -111,7 +111,23 @@ 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ží.
než server uloží. Totéž platí pro `applyMultiplier` (`server/src/utils.ts`)
a `deriveRecipe` (`server/src/mealTemplates.ts`).
## Násobek položky dne
Jídlo i aktivita mají volitelný `multiplier` (i necelý, 0,01–100): dvě sušenky
jsou ×2, nedojedená polovina ×0,5. Mění se přímo z přehledu dne
(`/meals/multiplier`, `/activities/multiplier`), bez otevření formuláře.
- **Uložené `calories` jsou vždy za jednu porci** a výsledek je
`applyMultiplier(calories, multiplier)`. Kdyby se ukládal už vynásobený
výsledek, opakovaná změna násobku by kumulovala zaokrouhlení.
- Všechny součty (`buildDayRecord`, `buildActivityDay`, `statsService`) proto
musí jít přes `applyMultiplier`, ne přes holé `calories`.
- **Cena se nenásobí** — za nedojedené jídlo se platí celé. Dvě kusy se stejnou
cenou jsou dva záznamy, nebo jedna cena zadaná za oba.
- Násobek 1 je výchozí stav a neukládá se (stejně jako jedna série).
## Zdroje energetických hodnot
@@ -159,6 +175,11 @@ umí `createMealTemplateFromMeal`, které navíc **přepíše šablonu stejného
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.
**Dodatečná zátěž** (`extraWeightGrams` u aktivity i cviku šablony, v gramech)
je jen informace o tréninku, kalorie **nemění** — žádný jednoduchý vzorec
spotřebu podle zátěže nedává, takže by šlo o vymyšlené číslo. Kdo chce, promítne
ji do sazby na 100 jednotek.
`WorkoutTemplate` (`workouts.ts`) je pojmenovaný seznam položek. Položka je buď
cvik (`kind: CVIK`), nebo odkaz na jinou šablonu (`kind: SABLONA`), jejíž cviky se
při použití rozbalí. Použitím vzniknou běžné aktivity s vazbou `templateId` — jsou
@@ -196,16 +217,27 @@ takže `DayPage` si vystačí s jedním voláním.
## Šablony jídel
`MealTemplate` (`mealTemplates.ts`, klíč `mealTemplates:<login>`) je opakovaně
jedené jídlo. Na rozdíl od šablon tréninků se **neskládá z jiných** — je to plochý
záznam. 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.
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`).
Šablona může nést buď `calories` (celá porce), nebo `caloriesPer100g` + `weight`.
Obojí projde `deriveAmounts`, takže stačí vyplnit, co o jídle víte.
Šablona má druh (`kind`):
Pole `note` slouží i na postup přípravy u domácích jídel. Strukturované suroviny
(recept jako seznam položek s množstvím) mohou přibýt později jako další volitelné
pole, aniž by se muselo měnit cokoli stávajícího.
- **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.
Š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.
Kontrakt má vlastní `MealTemplateInput`, kde je `id` volitelné — `MealTemplate`
ho vyžaduje a nešlo by jím posílat novou šablonu.
@@ -229,7 +261,13 @@ ve všech třech formátech, které Luncher exportuje (XLSX, CSV, JSON — viz
nejsou místa, takže se jako zdroj nepoužijí a záznam zůstane bez zdroje.
- 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;
**NAHRADIT** v dnech, do kterých se něco zakládá, odebere dřívější záznamy
z importu a založí je znovu. Ručně zadaná jídla zůstávají v obou režimech.
- Endpoint podporuje `dryRun` pro náhled před uložením; klient ho vždy použije.
Náhled vrací všechny řádky, které založit jde, i jídla, která už v dotčených
dnech jsou (`existing`). Uživatel v něm řádky odškrtá a ostrý import dostane
jen `selectedKeys`.
**Když se změní formát exportu v Luncheru, je potřeba upravit i tento parser**
a testy v `server/src/tests/luncherImport.test.ts`, které si export sestavují