Water is logged from the nutrition tab through one component, WaterLogger in src/features/nutrition/components/WaterLogger.tsx. It is self-contained: it owns its query, its two mutations and its modal. It does not use the nutrition session store, and water is not part of the day snapshot. The write functions and types live in the tracking feature: src/features/tracking/data/trackingApi.ts and src/features/tracking/data/types.ts.

Props and placement

PlanScreen renders it in three places and always passes key={dateParam ?? 'today'}, so the component remounts when the date changes and its local state resets. dateParam is undefined when the tab is on today and the local ISO date otherwise.

Reading the day

The query refetches every 60 seconds while mounted, and again each time the trainee opens the sheet.

Goal

The app does not compute the goal. It shows goalMl from the response.
  • progress = goal > 0 ? total / goal : 0.
  • With a goal, the label is the waterProgress string with current and goal.
  • With goalMl at 0 or below, the label is the total in millilitres and the sheet adds the waterNoGoal caption.
The nutrition plan payload also carries a water value inside each day’s targets (NutritionTargetsDto.water). The mobile code does not read it for this component. How the server derives goalMl is not visible from the app.

Logging an amount

The response type is DailyMetricTotals: waterMl and steps. Constants: Ways to add:
  • Plus button. save(100).
  • Presets. 200, 330, 500, 1000 and 1500 ml, labelled as cups and bottles from the strings file.
  • Custom. Opens a small dialog with a number field (maxLength 5). applyCustom accepts a whole number above 0 and at most 5000.
save(value) builds the payload:
Notes on that payload:
  • date is the date the server returned for this day, not a value the device computed again.
  • requestId is a fresh client-generated id per tap. The mutation has retry: 1, so a retried request carries the same id.
  • save rejects a non-integer, zero or an absolute value above MAX_ML with the waterInvalid message before any request.

Subtracting

The minus button calls save(-Math.min(STEP_ML, total)). The amount is negative and never larger than the current total, and the button is disabled when the total is 0. A subtraction is a new entry with a negative amountMl. It is listed in the history with its sign.

Guarding against double taps

A pending ref blocks a second save or remove until the first settles. All controls are also disabled while either mutation is pending, while the query has no data, and when the query errored.

History and deletion

The sheet has a history toggle. When open it lists data.entries newest first ([...entries].reverse()), each row showing the signed amount, the local time of createdAt, and a trash button. An empty list shows the waterEmpty message.
remove(entryId) sends date: data.date with the entry id. The response is DailyMetricTotals. This mutation also has retry: 1. Opening the history scrolls the sheet so the toggle sits at the top. The list gets a minHeight of the scroll viewport minus the toggle height, so there is always room to scroll to it. Closing it scrolls back to the top. WaterDay.entries has an optional deductEntryId field. This component does not read it.

Cache invalidation

Both mutations call the same refresh on success:
The home screen and the progress screen show water totals, so they refetch after any change. A successful add also closes the custom dialog and clears its field.

Errors

Sheet structure

The sheet is a transparent slide Modal over a KeyboardAvoidingView (padding on iOS, height on Android). The custom amount dialog is a second Modal nested inside the first, with a fade animation. It focuses its field in onShow.

Endpoints