feat(cycle): single-day read endpoint, symptom severity, and a richer log-day sheet

Add `GET /api/cycle/day-logs?date=YYYY-MM-DD` returning the full day-log DTO
(or null) for one tz-anchored day so the web log-day sheet pre-fills instead
of opening blank — a blank re-log no longer nulls untouched or HealthKit-sourced
fields. Delete resolves the row id from the same read.

Persist an optional 1-4 Likert severity per symptom link (migration
0133_v1150_cycle_symptom_severity), carried through the shared symptom-link
writer and surfaced in the day-log DTO.

Sheet now captures the previously missing fields — spotting between periods,
pregnancy/progesterone home-test results, contraceptive kind — plus per-symptom
severity chips, a one-tap end-period boundary, and a Delete action. The wheel
ring marker sits at the 12-o'clock origin again (drop the double rotation), the
cycle view goes two-column on desktop, and the empty ring carries a first-period
call to action. New copy is localized across all six locales; the prediction and
fertility surfaces keep their disclaimers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.8 committed 2026-06-06 12:39:28 +02:00
1 parent 824dcbb01a
commit 4b10bdc5da
23 files changed
+846 -143

No files matched your search

+1 -5
View File
@@ -139,11 +139,7 @@ export const PATCH = apiHandler(
// Replace symptom links only when `symptoms` was supplied.
if (body.symptoms !== undefined) {
await replaceSymptomLinks(
user.id,
id,
body.symptoms.map((s) => s.key),
);
await replaceSymptomLinks(user.id, id, body.symptoms);
}
const row = await prisma.cycleDayLog.findUniqueOrThrow({
+38 -1
View File
@@ -26,7 +26,10 @@ import {
} from "@/lib/api-response";
import { withIdempotency } from "@/lib/idempotency";
import { requireCycleEnabled } from "@/lib/cycle/gate";
import { cycleDayLogInputSchema } from "@/lib/validations/cycle";
import {
cycleDayLogInputSchema,
cycleDayLogQuerySchema,
} from "@/lib/validations/cycle";
import { upsertCycleDayLog } from "@/lib/cycle/day-log-write";
import { findOwningCycleId } from "@/lib/cycle/cycle-attribution";
import { toCycleDayLogDTO, dayLogSymptomInclude } from "@/lib/cycle/dto";
@@ -34,6 +37,40 @@ import { DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
export const POST = apiHandler(withIdempotency<[NextRequest]>(postDayLog));
/**
* `GET /api/cycle/day-logs?date=YYYY-MM-DD` — the full `CycleDayLogDTO`
* for one tz-anchored day, or `null` when nothing is logged. Lets the
* log-day sheet pre-fill (no blank-sheet data loss) and Delete resolve the
* row id. Gated + owner-scoped + `deletedAt:null`.
*/
export const GET = apiHandler(async (request: NextRequest) => {
const { user } = await requireAuth();
const gate = await requireCycleEnabled(user.id, user.gender);
if (!gate.enabled) return gate.response;
const parsed = cycleDayLogQuerySchema.safeParse({
date: new URL(request.url).searchParams.get("date"),
});
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "cycle.day-log.invalid",
});
}
const row = await prisma.cycleDayLog.findFirst({
where: { userId: user.id, date: parsed.data.date, deletedAt: null },
include: dayLogSymptomInclude,
});
annotate({
action: { name: "cycle.day-log.read", entity_type: "cycle_day_log" },
meta: { found: row !== null },
});
return apiSuccess(row ? toCycleDayLogDTO(row) : null);
});
async function postDayLog(request: NextRequest): Promise<Response> {
const { user } = await requireAuth();