feat: založení základní stránky příjem/výdej + import a statistiky
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Food Tracer je aplikace pro evidenci snědených jídel a útraty za jídlo. Czech-language UI.
|
||||
Full-stack TypeScript monorepo. Technologicky i vzhledově navazuje na sesterský projekt
|
||||
**Luncher** (`../Luncher`) — sdílí s ním stack, konvence i design systém.
|
||||
|
||||
## Monorepo Structure
|
||||
|
||||
```
|
||||
types/ → Shared OpenAPI-generated TypeScript types (source of truth: types/api.yml)
|
||||
server/ → Express 5 backend (Node.js 22, ts-node)
|
||||
client/ → React 19 frontend (Vite 7, React Bootstrap)
|
||||
```
|
||||
|
||||
Package manager: **Yarn Classic**. Deployment: `Dockerfile`, `compose.yml`.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Initial setup
|
||||
```bash
|
||||
cd types && yarn install && yarn openapi-ts # Generate API types first
|
||||
cd ../server && yarn install
|
||||
cd ../client && yarn install
|
||||
```
|
||||
|
||||
### Running dev environment
|
||||
```bash
|
||||
./run_dev.sh # tmux (Linux/macOS)
|
||||
.\run_dev.ps1 # dvě PowerShell okna (Windows)
|
||||
|
||||
# Nebo ručně:
|
||||
cd server && NODE_ENV=development yarn startReload # Port 3001
|
||||
cd client && yarn start # Port 3000, proxies /api → 3001
|
||||
```
|
||||
|
||||
### Building & tests
|
||||
```bash
|
||||
cd types && yarn openapi-ts # Regenerate types from api.yml
|
||||
cd server && yarn build # tsc → server/dist
|
||||
cd server && yarn test # Jest (in-memory storage)
|
||||
cd server && yarn test meals # Run one file by name
|
||||
cd client && yarn build # tsc --noEmit + vite build → client/dist
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### API Types (types/)
|
||||
- OpenAPI 3.0 spec in `types/api.yml` — thin aggregator, endpoint specs in
|
||||
`types/paths/<domain>/*.yml`, shared schemas in `types/schemas/_index.yml`
|
||||
- `yarn openapi-ts` generates `types/gen/` (client.gen.ts, sdk.gen.ts, types.gen.ts)
|
||||
- Both server and client import from these generated types
|
||||
- **When changing API contracts: update api.yml first, then regenerate**
|
||||
|
||||
### Server (server/src/)
|
||||
- **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ů), `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
|
||||
- **Storage:** `storage/index.ts` factory dle proměnné `STORAGE`; backendy:
|
||||
`json.ts` (soubor, vývoj), `redis.ts` (produkce), `memory.ts` (testy)
|
||||
- **Config:** `.env.development` / `.env.production` (viz `.env.template`)
|
||||
|
||||
### 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),
|
||||
`StatsPage` (statistiky + import)
|
||||
- **Components:** `components/` (Header, CalorieLookup) a `components/modals/`
|
||||
(MealModal, ActivityModal, WorkoutModal, SettingsModal, ImportModal)
|
||||
- **Context:** `context/auth.tsx` (JWT), `context/settings.tsx` (světlý/tmavý motiv)
|
||||
- **Routing:** konstanty adres jsou v `routes.ts`, ne v `AppRoutes.tsx` — hlavička
|
||||
je potřebuje a kruhový import by je nechal nedefinované
|
||||
- **Styling:** Bootstrap 5 + React Bootstrap + SCSS; design systém a proměnné
|
||||
`--ft-*` jsou v `App.scss`, stránkové styly co-located vedle komponent
|
||||
- **API:** přes OpenAPI SDK z `types/gen/`
|
||||
|
||||
## Data model
|
||||
|
||||
- Jídlo (`MealEntry`) patří jednomu dni a jednomu uživateli; klíč úložiště je
|
||||
`meals:<login>:<YYYY-MM-DD>` a hodnotou je pole záznamů dne.
|
||||
- **Ceny jsou všude celá čísla v haléřích** — v úložišti, v API i mezi klientem
|
||||
a serverem. Na koruny se převádí až při zobrazení (`formatPrice` v `Utils.tsx`).
|
||||
Stejnou konvenci má Luncher, díky tomu se částky z importu přenesou beze ztráty.
|
||||
- Kalorie jsou volitelné celé číslo v kcal. Jídla bez kalorií se do součtů
|
||||
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.
|
||||
|
||||
## Gramáž a kalorie
|
||||
|
||||
`deriveAmounts` v `server/src/meals.ts` je jediný zdroj pravdy pro dopočty:
|
||||
|
||||
1. **gramáž** = `price / pricePer100g * 100`, pokud není zadaná ručně
|
||||
2. **kalorie** = `weight / 100 * caloriesPer100g`, jinak ručně zadaná hodnota
|
||||
|
||||
Ručně zadaná gramáž má vždy přednost — porci lze zvážit přesněji, než kolik řekne
|
||||
cena. Sazba je na každém záznamu zvlášť, takže hlavní jídlo za 44 Kč/100 g a salát
|
||||
s jiným cenováním můžou být ve stejném dni vedle sebe. Výchozí sazby podniků drží
|
||||
`server/src/settings.ts` (`sourceRates`) a klient jimi předvyplňuje pole, když
|
||||
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ží.
|
||||
|
||||
## Zdroje energetických hodnot
|
||||
|
||||
`server/src/calorieProvider.ts` definuje rozhraní `CalorieProvider` a jeho
|
||||
implementaci nad Open Food Facts. Poskytovatel je záměrně vyměnitelný za jeden
|
||||
soubor a **nikdy nevyhazuje výjimku** — při nedostupnosti vrací `unavailable`,
|
||||
takže hledání jen přijde o návrhy a nespadne. `CALORIE_PROVIDER=none` externí
|
||||
dotazy vypne (offline provoz, testy).
|
||||
|
||||
Poznámky ke zdrojům, ověřené v září 2026:
|
||||
|
||||
- **KalorickéTabulky.cz nemají veřejné API.** Jejich robots.txt zakazuje
|
||||
`/*query.page` (jejich vyhledávání) a smluvní podmínky omezují užití nad rámec
|
||||
zamýšleného účelu, takže se odtud **nesmí scrapovat**. Aplikace jen nabídne
|
||||
odkaz na jejich tabulku potravin a zkopíruje název jídla do schránky.
|
||||
Předvyplnit jejich hledání z URL nejde — běží v JavaScriptu a parametry v query
|
||||
stringu ignoruje (ověřeno porovnáním odpovědí).
|
||||
- **Open Food Facts:** používá se `search.openfoodfacts.org`, ne hlavní
|
||||
`world.openfoodfacts.org` — tamní `/cgi/search.pl` i `/api/v2/search` vrací 503.
|
||||
Produkty bez `energy-kcal_100g` se odfiltrují.
|
||||
- **Vlastní knihovna** (`server/src/calories.ts`) si pamatuje kcal/100 g pod
|
||||
znormalizovaným názvem jídla a řadí se před externí návrhy — na kantýnová jídla
|
||||
sedí líp než databáze balených potravin. Plní se sama při uložení jídla.
|
||||
|
||||
## Pohyb a energetická bilance
|
||||
|
||||
Den má dvě strany a `DayPage` je dělí do záložek:
|
||||
|
||||
- **Příjem** — jídla (`MealEntry`), klíč `meals:<login>:<datum>`
|
||||
- **Výdej** — pohyb (`ActivityEntry`), klíč `activities:<login>:<datum>`
|
||||
|
||||
Aktivita se měří v jednotce (`ActivityUnit`: KROKY, MINUTY, KM, OPAKOVANI) a
|
||||
kalorie se dopočtou jako `quantity / 100 * caloriesPer100Units`. **Sto jednotek,
|
||||
ne jedna** — u kroků by sazba na jeden krok byla zlomek (~0,04 kcal) a aplikace
|
||||
všude pracuje s celými čísly. Stejná konvence jako u energie jídla na 100 g.
|
||||
|
||||
`WorkoutTemplate` (`workouts.ts`) je pojmenovaný seznam cviků. Použitím vzniknou
|
||||
běžné aktivity s vazbou `templateId` — jsou samostatné, takže úprava založené
|
||||
položky šablonu nemění.
|
||||
|
||||
`buildEnergyBalance` v `dayOverview.ts` počítá:
|
||||
`bilance = příjem − (klidový výdej + pohyb)`. Záporná hodnota je deficit.
|
||||
Bez nastaveného klidového výdeje (`basalCalories` v nastavení) 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.
|
||||
|
||||
`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.
|
||||
|
||||
## Import z Luncheru
|
||||
|
||||
`server/src/luncherImport.ts` čte měsíční přehled ze stránky statistik Luncheru
|
||||
ve všech třech formátech, které Luncher exportuje (XLSX, CSV, JSON — viz
|
||||
`../Luncher/server/src/userExport.ts`).
|
||||
|
||||
- Sloupce se hledají **podle názvu hlavičky**, ne podle pozice.
|
||||
- CSV má BOM, oddělovač `;`, datum `DD.MM.YYYY` a desetinnou čárku (český Excel).
|
||||
- XLSX má dva listy — čte se `Přehled`, list `Souhrn` se ignoruje.
|
||||
- Řádky se zakládají jako **oběd**, pokud se neurčí jinak (Luncher řeší obědy).
|
||||
- `name` = jídlo → poznámka → typ záznamu. U voleb "Budu objednávat" a
|
||||
"Rozhoduji se" bývá sloupec s jídlem prázdný a co se reálně jedlo stojí
|
||||
v poznámce ("Chefie - Těstovinový salát"), proto ten mezikrok. Poznámka, ze
|
||||
které se stal název, se už nekopíruje do `note`.
|
||||
- `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.
|
||||
- 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.
|
||||
- Endpoint podporuje `dryRun` pro náhled před uložením; klient ho vždy použije.
|
||||
|
||||
**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í
|
||||
přesně tak, jak ho Luncher generuje.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Czech naming for domain variables and UI strings; English for infrastructure code
|
||||
- TypeScript strict mode in both client and server
|
||||
- Server module resolution: Node16; Client: ESNext/bundler
|
||||
- Komentáře v češtině, u netriviálních míst vysvětlují **proč**, ne co
|
||||
Reference in New Issue
Block a user