The nutrition feature lives in src/features/nutrition. It renders the coach’s nutrition plan, lets the trainee tick, swap, resize and remove plan foods, add their own meals, and log water. Everything the trainee changes for a day is kept in one Zustand store and synced to the server as a day snapshot. Related pages:

Folder map

Note the two store folders. store/ (singular) holds the day log. stores/ (plural) holds only the meal draft.

Routes

All route files are thin wrappers, except meal/add.tsx and meal/result.tsx, which contain their screens inline.

Plan structure

getNutritionPlan() calls GET /v1/trainee/nutrition and returns an array of plans. A trainee can have several nutrition plans at once.
Mapping rules applied in nutritionApi.ts:
  • kind comes from planKindOf(plan) in src/features/plans/lib/planKind.ts. The server’s kind wins when present. An older API that only sends pdfUrl still produces a file plan.
  • autoAlts is true only when the server sends exactly true.
  • Per-day targets: day.targets ?? plan.targets. A day without its own targets inherits the plan’s.
  • A meal’s label is the coach’s custom name, else a label looked up by type (breakfast, snack_am, lunch, pre, post, dinner), else a generic meal label.
  • Each item’s options[0] is the coach’s main food. The rest are alternatives mapped by altOptionOf.
  • An option’s qtyText is the server’s display.text when present, else qty plus a unit label.
  • meal.target is only set when the server sends one.
NutritionTargetsDto carries kcal, protein, carbs, fat, water and optional mbpProtein, mbpCarb, mbpFat.

Multiple plans, days and file plans

PlanScreen shows two pickers:
  • PlanTitlePicker (components/PlanTitlePicker.tsx) switches between plans. It is a plain label when there is one plan and a modal list when there are more. A file plan shows a badge.
  • PlanSwitcher (src/components/ui/PlanSwitcher.tsx) is a horizontal chip row. On the nutrition tab it is fed the selected plan’s days, not plans, and renders only when the plan has more than one day.
Which plan and day are shown is resolved by resolveNutritionPlanPick and resolveNutritionDayPick in lib/day.ts, in this order:
  1. The trainee’s explicit pick for this date (planPick and dayPick local state, keyed by date).
  2. For today only, the persisted preferred day while it is still active.
  3. The plan day the day’s edits reference most, from referencedDayOf in data/calc.ts.
  4. The first plan and the first day.
See Day log and sync for the preferred day and how the pick reaches the home screen. src/features/plans/lib/planKind.ts decides what a file plan is and how to show it. FilePlanView (src/features/plans/components/FilePlanView.tsx) renders the file in a WebView. It has an expand button that opens the shared ContentPdfViewer full screen, and one action button: a link opens in the browser, a PDF is downloaded to the cache directory and passed to the share sheet, falling back to opening the url. FilePlanNotice is the passive card shown where macro progress would normally go. A file plan has no days or meals. When a file plan is selected on an editable day, PlanScreen turns its own scroll off (a WebView needs a bounded height) and shows the water tile, the notice, the file, any meals the trainee added by hand, and the add buttons.

Screens

PlanScreen

presentation/PlanScreen.tsx is the tab. It calls useNutritionDaySync() once and reads the whole session store into a DayEdits object. The body depends on the selected date: An editable day is today or yesterday. DateNavigator disables future dates in its calendar. The macro header is built in the macros memo. Goals are day.targets when the coach set them, else the sum of the day’s planned meals (plannedDayMacros). Consumed is eatenDayMacros. When the studio uses MBP, the four rows show MBP totals instead of kcal and grams. The two add buttons go to /meal/add with mode: 'photo' and to /meal/new with the current plan and day ids. Deleting a meal asks for confirmation with Alert.alert. A trainee meal that has a logId also deletes that meal log through useDeleteMealLog. A plan meal is reset to PLANNED and then marked removed for the day.

MealScreen

presentation/MealScreen.tsx finds the meal by id across every plan and day (findMeal). It shows:
  • MealMacroSummary with the meal total and what was eaten.
  • MealTargetGauge when the meal has a coach limit.
  • AiComponentRow for foods the trainee added to this meal.
  • FoodItemRow for each plan item that is not removed, with the tick, the swap chip, the amount editor and swipe to delete.
  • A footer with “mark all eaten” and an AI replace button that opens MealAiLogger in a modal.
A meal the coach left empty but gave a limit is “composed” by the trainee. Opening such a meal on an editable day opens AddComponentSheet automatically after AUTO_OPEN_DELAY_MS (350 ms), once per visit. On a future date the screen shows a lock banner, disables the list and hides the footer.

MealComposerScreen

presentation/MealComposerScreen.tsx builds a trainee’s own meal (CustomMeal). Foods come from AddComponentSheet. Each row has a PortionStepper when the food carries portion context, and three editable macro fields. The header shows “remaining today”: the day goal minus what is already eaten minus the draft. Saving calls upsertCustomMeal(dateKey, meal) and goes back. When editing, the original meal is captured once on mount so a store echo cannot reset the draft.

Endpoints used by the data layer

All paths are relative to the API base and go through apiClient (see API client). Water uses /v1/trainee/water and is documented in Water.

Query keys

Defined in hooks/useNutrition.ts. useSaveNutritionDay invalidates ['home'] on success, because the home rings read the same day log. useLogQuickMeal and useDeleteMealLog invalidate the menu, the history and ['home'].

Tests

Four Node test scripts in scripts/ load the pure modules with a small TypeScript transpile loader and assert the intended behaviour: test-nutrition-portions.cjs, test-meal-target.cjs, test-added-food-amount.cjs and test-plan-kind.cjs. They use node:test. No package.json script references them by name. See Testing and lint.