Files
HealthLog/src/lib/validations/cycle-feed.ts
T
Marc-André Bombeck 546b9b2569 feat(cycle): choose how the calendar feed draws a forecast (#1350)
A forecast period was one all-day event over every day it might fall on,
so a period of a few days showed up as a week-long estimate next to the
logged ones. Each feed now carries a forecast style:

- EXACT: the calculated period only, from the most likely start for the
  predicted number of bleeding days.
- MARGIN: that event, plus separate "possible" events for the days it may
  start earlier and the days it may still run on, each only where the
  band reaches that side. Discreet titles use an open dot for them.
- BAND: the single range, unchanged.

Migration 0495 adds the column with BAND as its default, so every live
feed keeps the events it serves. A feed turned on from now on starts on
MARGIN, set by the mint route. PATCH changes it, rotate copies it, the
status and the audit diff carry it, and the settings card offers it with
a one-line explanation of the chosen style. The horizon and its stop rule
are the same in every style, and the period event keeps its UID across
styles so a calendar updates it in place.
2026-11-09 10:59:18 +01:00

56 lines
1.8 KiB
TypeScript

/**
* The cycle calendar feed's request contract (#1350).
*
* Only the options are writable. The token, the record and the carrier are
* never body fields: the record comes from the resolved session, the carrier
* is the actor, and the token is minted by the server.
*/
import { z } from "zod/v4";
/** The feed's title modes. Mirrors `CycleCalendarFeedTitleMode`. */
export const cycleFeedTitleModeEnum = z.enum(["DISCREET", "DESCRIPTIVE"]);
/** How a forecast period is drawn. Mirrors `CycleCalendarFeedForecastStyle`. */
export const cycleFeedForecastStyleEnum = z.enum(["EXACT", "MARGIN", "BAND"]);
/**
* The style a newly minted feed starts with. The column's own default is BAND,
* which keeps every feed minted before the option on the events it had; a new
* one starts with the calculated days and their margin drawn apart.
*/
export const NEW_FEED_FORECAST_STYLE: z.infer<
typeof cycleFeedForecastStyleEnum
> = "MARGIN";
/** Forecast cycles ahead. The feed caps itself further by band width. */
export const FEED_HORIZON_MIN = 1;
export const FEED_HORIZON_MAX = 6;
/** Observed cycles included as history. */
export const FEED_HISTORY_MIN = 0;
export const FEED_HISTORY_MAX = 12;
export const cycleFeedOptionsPatchSchema = z
.object({
horizonCycles: z
.number()
.int()
.min(FEED_HORIZON_MIN)
.max(FEED_HORIZON_MAX)
.optional(),
historyCycles: z
.number()
.int()
.min(FEED_HISTORY_MIN)
.max(FEED_HISTORY_MAX)
.optional(),
includeFertile: z.boolean().optional(),
titleMode: cycleFeedTitleModeEnum.optional(),
forecastStyle: cycleFeedForecastStyleEnum.optional(),
})
.strict()
.refine((v) => Object.keys(v).length > 0, {
message: "At least one option is required",
});
export type CycleFeedOptionsPatch = z.infer<typeof cycleFeedOptionsPatchSchema>;