# Food Tracer Aplikace pro evidenci snědených jídel a útraty za jídlo. Ke každému jídlu se dá vedle názvu a zdroje uložit i cena a kalorie, takže jde zpětně zjistit, kolik člověk za jídlo utratil za den, měsíc nebo rok. Umí naimportovat měsíční přehled vyexportovaný ze stránky statistik [Luncheru](../Luncher) — ve formátu XLSX, CSV i JSON. ## Struktura ``` types/ → Sdílené TypeScript typy generované z OpenAPI (zdroj pravdy: types/api.yml) server/ → Express 5 backend (Node.js 22, ts-node) client/ → React 19 frontend (Vite 7, React Bootstrap) ``` Package manager: **Yarn Classic**. Každá složka má vlastní `package.json`. ## Rozjetí ```bash cd types && yarn install && yarn openapi-ts # typy je nutné vygenerovat první cd ../server && yarn install cd ../client && yarn install ``` Do `server/.env.development` patří `JWT_SECRET` o délce alespoň 32 znaků (šablona je v `server/.env.template`). ## Vývoj ```bash ./run_dev.sh # vše naráz v tmuxu (Linux/macOS) .\run_dev.ps1 # dvě PowerShell okna (Windows) ``` Nebo ručně ve dvou terminálech: ```bash cd server && NODE_ENV=development yarn startReload # port 3001 cd client && yarn start # port 3000, proxy /api → 3001 ``` ## Testy a build ```bash cd server && yarn test # Jest, in-memory úložiště cd server && yarn build # tsc → server/dist cd client && yarn build # tsc --noEmit + vite build → client/dist ``` ## Nasazení ```bash JWT_SECRET=... docker compose up --build ``` Produkčně běží aplikace nad Redisem, ve vývoji nad JSON souborem (`server/data/db.json`). Viz `server/.env.template`. ## Datový model Úložiště je klíč–hodnota (JSON soubor ve vývoji, Redis v produkci). „Tabulka" je tedy typ klíče a hodnotou je pod ním buď celé pole záznamů, nebo jeden objekt. Klíče začínají typem a přihlašovacím jménem uživatele, data jednotlivých uživatelů se nemíchají. | Klíč | Hodnota | |---|---| | `meals::` | pole jídel dne (`MealEntry`) | | `mealTemplates:` | pole receptů (`MealTemplate`) | | `mealPhotos:` | rejstřík fotek: klíč jídla (název + zdroj) → pole náhledů | | `mealPhoto::` | plný obrázek jedné fotky (data URL) | | `calorieLibrary:` | vlastní knihovna kalorií: znormalizovaný název → záznam | | `settings:` | nastavení, tělesné údaje a cíle | | `weights:` | vážení: datum → hmotnost v gramech | | `sleep::` | délka spánku v minutách | | `activities::` | pole aktivit dne (`ActivityEntry`) | | `workouts:` | pole šablon tréninků — volné šablony i plány | | `workoutPlans::` | pole záznamů o použití šablon a dnů plánu v daném dni | Výchozí hodnoty se do úložiště nezapisují (jedna série, násobek 1, volný typ šablony, slaná chuť receptu) a dopočítávaná pole (součty, odhady kalorií, ukazatele) se počítají při čtení. ```mermaid erDiagram MEAL_ENTRY { string id PK date date enum mealType "SNIDANE, OBED, ..." string name string source "podnik / obchod" int price "haléře" int weight "g" int pricePer100g "haléře" int calories "kcal za porci" int caloriesPer100g object nutrientsPer100g "g na 100 g, na desetiny" decimal multiplier string note string templateId FK "recept" string importSource string importKey "duplicity importu" datetime createdAt datetime updatedAt } MEAL_TEMPLATE { string id PK string name string source string category "typ: polévka, omáčka..." enum flavor "SLANE, SLADKE" int servings int cookedWeight "g" int prepMinutes int weight "porce, dopočteno" int calories "porce, dopočteno" int price "porce, dopočteno" string note "postup v Markdownu" array sideDishIds FK "přílohy" array similarIds FK "podobné" } RECIPE_INGREDIENT { string name decimal amount "v jiné jednotce než g" enum unit "KS, ML, L, LZICE..." int weight "g" int caloriesPer100g int calories int price } MEAL_PHOTO_INDEX { string mealKey PK "název + zdroj" string id FK string thumbnail "data URL ~240 px" datetime createdAt } MEAL_PHOTO { string id PK string image "data URL ~1600 px" } CALORIE_LIBRARY { string normalizedName PK string name int caloriesPer100g int uses datetime updatedAt } SETTINGS { array sourceRates "podnik a sazba za 100 g" int basalCalories "ručně zadaný klidový výdej" object profile "pohlaví, rok narození, výška, obvody" object goals "tempo, cílová hmotnost, bílkoviny/kg" } WEIGHTS { date date PK int weightGrams } SLEEP { date date PK int minutes } ACTIVITY_ENTRY { string id PK date date string name enum unit "KROKY, MINUTY, SEKUNDY, KM, OPAKOVANI" int quantity "na sérii" int sets int caloriesPer100Units int calories "za jedno provedení" int extraWeightGrams decimal multiplier string note string templateId FK "šablona / plán" string planDayId FK "den plánu" date replacesDate "nahrazuje vynechaný cvik z tohoto dne" datetime createdAt datetime updatedAt } WORKOUT_TEMPLATE { string id PK string name enum type "VOLNY (výchozí), FIXNI, PLOVOUCI" int replaceWithinDays "jen plán" date startDate "jen plán" } WORKOUT_PLAN_DAY { string id PK string name int weekday "1-7, jen fixní plán" } WORKOUT_TEMPLATE_ITEM { enum kind "CVIK, SABLONA" string name enum unit int quantity int sets int caloriesPer100Units int extraWeightGrams string templateRefId FK "volná šablona" } WORKOUT_PLAN { string id PK string templateId FK string templateName "v době použití" string planDayId FK string planDayName "v době použití" int replaceWithinDays "v době použití" datetime appliedAt } WORKOUT_PLAN_ITEM { string entryId FK "chybí = vynecháno" string name enum unit int quantity int sets int caloriesPer100Units int calories object replacement "date + entryId náhrady" } MEAL_ENTRY }o--o| MEAL_TEMPLATE : "templateId" MEAL_TEMPLATE ||--|{ RECIPE_INGREDIENT : "ingredients" MEAL_TEMPLATE }o--o{ MEAL_TEMPLATE : "přílohy, podobné" MEAL_PHOTO_INDEX ||--|| MEAL_PHOTO : "id" MEAL_PHOTO_INDEX }o..o{ MEAL_ENTRY : "název + zdroj" CALORIE_LIBRARY }o..o{ MEAL_ENTRY : "znormalizovaný název" WORKOUT_TEMPLATE ||--o{ WORKOUT_TEMPLATE_ITEM : "items (volná)" WORKOUT_TEMPLATE ||--o{ WORKOUT_PLAN_DAY : "days (plán)" WORKOUT_PLAN_DAY ||--|{ WORKOUT_TEMPLATE_ITEM : "items" WORKOUT_TEMPLATE_ITEM }o--o| WORKOUT_TEMPLATE : "templateRefId" ACTIVITY_ENTRY }o--o| WORKOUT_TEMPLATE : "templateId" ACTIVITY_ENTRY }o--o| WORKOUT_PLAN_DAY : "planDayId" WORKOUT_PLAN }o--|| WORKOUT_TEMPLATE : "templateId" WORKOUT_PLAN }o--o| WORKOUT_PLAN_DAY : "planDayId" WORKOUT_PLAN ||--|{ WORKOUT_PLAN_ITEM : "items" WORKOUT_PLAN_ITEM |o--o| ACTIVITY_ENTRY : "entryId (odcvičeno)" WORKOUT_PLAN_ITEM |o--o| ACTIVITY_ENTRY : "replacement (náhrada)" ``` Vazby na šablony a plány jsou volné — smazaná šablona v historii zůstane pod názvem uloženým v `WORKOUT_PLAN`. Záznam použití (`workoutPlans`) je u plánu zároveň evidence, který den plánu se kdy odcvičil; podle ní se počítá, co je na řadě. ## Den: příjem a výdej Přehled dne má dvě záložky: - **Příjem** — co jste snědli, s cenou, gramáží a kaloriemi. - **Výdej** — pohyb. Kroky, minuty, sekundy, kilometry nebo opakování; z množství a spotřeby na 100 jednotek se dopočtou spálené kalorie (10 000 kroků ≈ 400 kcal). U silových cviků jde zadat i **počet sérií** — 3 × 40 kliků se tak neplete se 120 v kuse, i když kalorie vyjdou stejně. Volitelně se dá k dni zapsat i **délka spánku**. Slouží jako kontext — do energetické bilance nevstupuje, protože klidový výdej už pokrývá celých 24 hodin včetně spánku a žádná standardní rovnice s délkou spánku nepočítá. Nahoře je **bilance dne**: `příjem − (klidový výdej + pohyb)`. ## Tréninky a plány Na stránce **Tréninky** jsou tři typy: - **Plovoucí plán** — sekvence tréninkových dnů (Den 1, Den 2, Den 3…), po které následuje den volna a cyklus se opakuje. Na kalendář navázaný není: další den přijde na řadu, až se splní předchozí. Den volna jde v rámci cyklu jednou **posunout dopředu** — když uprostřed cyklu den necvičíte, bere se to jako volno a po posledním dni se pokračuje rovnou od začátku (Den 1, Den 2, volno, Den 3, Den 1…). Další necvičený den trénink jen odsune. - **Fixní plán** — pevné dny v týdnu (pondělí nohy, středa běh…), ostatní dny jsou volno. - **Volný trénink** — seznam cviků na kdykoli (rozcvička, protažení). Jde ho vložit do dne plánu i do jiného volného tréninku; vkládání je jen na jednu úroveň. Plán se založí jednou a **přehled dne** pak sám navrhne, co je ten den na řadě. Po odcvičení stačí návrh potvrdit; co se nestihlo, se odškrtne a zapíše jako vynechané. Nad rámec plánu se ručně přidávají jen „unikátnosti" — chůze, kroky, jednorázový běh. Jiný den plánu, než který je na řadě, nebo volný trénink jde založit tlačítkem *Z tréninků*. U plánu jde nastavit **Nahraditelné během N dní**: vynechaný cvik (odškrtnutý při potvrzení nebo smazaný ze dne) se pak v následujících N dnech nabízí v přehledu dne k náhradě. Ve statistikách výdeje se u takového dne místo vykřičníku ukáže šipka na den náhrady. Dny volna ani necvičené dny se ve statistikách nijak nezapisují — zobrazuje se jen to, co proběhlo. Plán i trénink jde kdykoli **vylepšit** — přidat cviky, dny, zvýšit zátěž nebo počet opakování. Historie zůstává, jak byla: každé použití si pamatuje, co plán v ten den sliboval. Založené položky jsou samostatné aktivity, takže je jde v dni doupravit, aniž by se změnil plán. Volný trénink jde vytvořit i **opačným směrem** — z toho, co už máte v dni zapsané, tlačítkem *Uložit jako trénink* v záložce Výdej. Recepty (vařená jídla se surovinami a postupem) jsou rozšířením jídla v seznamu jídel na stránce **Jídla**. ## Osobní údaje Na stránce **Osobní údaje** se z pohlaví, roku narození, výšky a hmotnosti spočítá klidový výdej (bazální metabolismus) rovnicí Mifflin–St Jeor. Kdo má hodnotu změřenou přesněji, může ji zadat ručně — ta pak má přednost. Výpočet se záměrně nenásobí koeficientem fyzické aktivity, jak to dělají kalkulačky TDEE: pohyb si aplikace eviduje zvlášť a přičítá ho, takže vynásobení by ho započítalo dvakrát. ## Kalorie a gramáž Ke každému jídlu jde vyplnit **cenu za 100 g** a z ní se dopočítá **gramáž** (u TechToweru typicky 44 Kč/100 g). Gramáž jde kdykoli přepsat ručně a každá položka má vlastní sazbu — hlavní jídlo a salát s jiným cenováním se nepletou. Sazby podniků se nastavují v *Nastavení* a při výběru zdroje se předvyplní. Z gramáže a **energie na 100 g** se pak dopočítají kalorie. Hodnotu nabídne tlačítko *Najít kalorie*: - z **vlastní knihovny** — co jste jednou zadali, aplikace si pamatuje pod názvem jídla a příště nabídne sama, - z **Open Food Facts** (otevřená databáze, bez klíče), - odkazem na **KalorickéTabulky.cz**, které veřejné API nemají — otevřou se v novém panelu a název jídla se zkopíruje do schránky, hodnotu opíšete ručně. Když je externí databáze nedostupná, hledání nespadne — zůstane vlastní knihovna. ## Import z Luncheru Na stránce **Statistiky** je tlačítko *Import z Luncheru*. Stačí nahrát soubor stažený ze statistik Luncheru: - Záznamy se rozdělí do dnů, ke kterým patří (export je typicky za celý měsíc). - Zakládají se jako **oběd**, protože Luncher řeší výběr obědů. Chod jde při importu změnit a jednotlivá jídla pak upravit v přehledu dne. - Před uložením se ukáže náhled toho, co se založí. - Opakovaný import stejného měsíce data nezduplikuje.