mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
feat: hydration logging and daily goal ring
Add a WATER_INTAKE measurement type and a per-user daily goal so the Insights overview can track water intake against a goal ring. Logging rides the existing measurements create path (type WATER_INTAKE, unit ml, summed cumulatively); GET/PATCH /api/hydration read the day total and own the goal. The goal-ring math (empty / partial / met / exceeded) lives in a pure helper, free of Prisma and React, and is unit-tested in isolation. Wire the new type through the measurement enum, value ranges, cumulative HealthKit handling, categories, display transform, list metadata, record direction, and the chartable-token allowlist. The HealthKit DietaryWater import stays on the nutrition hold, so the type carries no import mapping by design. Document GET/PATCH /api/hydration in the OpenAPI contract.
This commit is contained in:
1 parent
bc617adc2c
commit
31c35baec2
20 files changed
+865
-1
No files matched your search
@@ -77,6 +77,7 @@ const openApiBase: Pick<
|
||||
{ name: "Illness" },
|
||||
{ name: "Admin" },
|
||||
{ name: "Meta" },
|
||||
{ name: "Hydration" },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* OpenAPI route module — hydration daily-goal surface (v1.25).
|
||||
*
|
||||
* `GET /api/hydration` reads today's summed water intake vs the user's goal;
|
||||
* `PATCH /api/hydration` sets the per-user daily goal. Logging rides the
|
||||
* measurements create path (POST /api/measurements, type WATER_INTAKE), so it
|
||||
* is documented there, not here.
|
||||
*
|
||||
* Part of the OpenAPI route table; aggregated in `./index.ts`.
|
||||
*/
|
||||
import { z } from "zod/v4";
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
|
||||
import { dataEnvelope, stdResponses } from "./shared";
|
||||
|
||||
const hydrationEntry = z.object({
|
||||
id: z.string(),
|
||||
value: z.number().describe("Logged amount in millilitres."),
|
||||
measuredAt: z.string().describe("ISO-8601 instant the entry was logged."),
|
||||
});
|
||||
|
||||
const hydrationTodayResponse = z
|
||||
.object({
|
||||
date: z.string().describe("ISO-8601 start of the user's local day."),
|
||||
totalMl: z.number().describe("Today's summed intake in ml."),
|
||||
goalMl: z.number().describe("Effective daily goal in ml."),
|
||||
percent: z.number().describe("Progress toward the goal, capped at 100."),
|
||||
rawPercent: z.number().describe("Uncapped progress; can exceed 100."),
|
||||
met: z.boolean().describe("Whether the day's total reached the goal."),
|
||||
remainingMl: z.number().describe("Remaining ml to the goal (0 once met)."),
|
||||
entries: z.array(hydrationEntry),
|
||||
})
|
||||
.meta({
|
||||
id: "HydrationToday",
|
||||
description: "Today's water-intake total vs the user's daily goal.",
|
||||
});
|
||||
|
||||
const hydrationGoalRequest = z
|
||||
.object({
|
||||
goalMl: z
|
||||
.number()
|
||||
.int()
|
||||
.min(250)
|
||||
.max(8000)
|
||||
.describe("Daily hydration goal in millilitres."),
|
||||
})
|
||||
.meta({
|
||||
id: "HydrationGoalRequest",
|
||||
description: "Set the per-user daily hydration goal (ml).",
|
||||
});
|
||||
|
||||
const hydrationGoalResponse = z
|
||||
.object({ goalMl: z.number() })
|
||||
.meta({ id: "HydrationGoalResponse" });
|
||||
|
||||
export const hydrationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
"/api/hydration": {
|
||||
get: {
|
||||
tags: ["Hydration"],
|
||||
summary: "Today's hydration total vs goal",
|
||||
description:
|
||||
"Sums the calling user's WATER_INTAKE measurements for the current local day and compares them against the per-user goal (default 2000 ml).",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Today's hydration summary.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
hydrationTodayResponse,
|
||||
"HydrationTodayEnvelope",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
patch: {
|
||||
tags: ["Hydration"],
|
||||
summary: "Set the daily hydration goal",
|
||||
description: "Updates the per-user daily hydration goal in millilitres.",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: hydrationGoalRequest } },
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Goal updated.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
hydrationGoalResponse,
|
||||
"HydrationGoalEnvelope",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user