Files
HealthLog/src/components/cycle/calendar-window.ts
T
Marc-André Bombeck bd6089734d fix(cycle): let the month grid read the month it is showing
The page reads one window anchored on today, ninety days back and half a
year forward, and the month grid navigates freely past it. A month
outside the window came back with no days: the cells still rendered
(they come from the month, not the read) and still opened the log sheet,
and the write behind it still landed, because the period route is built
for back-dating and re-anchors the neighbouring cycles for it. What did
not happen is anything visible. Saving an April flow in August closed
the sheet onto a month the read never carried, so the entry looked like
it had not been made, and entering it again did not help.

The grid now reports the month it moved to and gets its own bounded read
for it. The anchored window is untouched, because every other surface on
the page means today and the route caps a span at 400 days, so a union
with a month two years back would simply be refused.

A write also says so now. Saving, deleting and both period boundaries
raise a confirmation; the sheet's inline strip already named a failure.
Between the invisible month and the silent success there was no way to
tell a save that worked from one that never happened.

The sheet header carries the date it is writing to, and the one-tap
boundaries drop the word today when the day is not today. It always
wrote to the selected date; saying otherwise made a correction read as a
mistake.

The gear beside the log button is gone. It was a second door to the tab
directly below it and no other page carries one in that corner.

- calendar days carry isCycleStart, so the delete dialog can say that
  removing this day removes the period start with it
- window arithmetic is a pure module with its own tests, including the
  400-day span the anchor cannot be widened past
- integration: a day 111 back is written, is absent from the anchored
  read, and comes back complete on a window over its month
- new strings in all six languages
2026-08-07 15:20:29 +02:00

59 lines
2.4 KiB
TypeScript

/**
* The date range the /cycle page reads, and what a month outside it needs.
*
* The page reads one anchored window — ninety days back, half a year forward —
* and that window drives the ring, the forecast panel and the temperature
* chart, all of which are about today. The month grid is not: it navigates
* freely, and a month the anchored read does not cover came back with no days
* at all. The cells still rendered (they come from the month, not the read) and
* still opened the log sheet, and the write behind it still landed — the period
* route is built for back-dating and re-anchors the neighbouring cycles for it.
* What did not happen is anything visible. Save an April flow in August and the
* sheet closes onto a month the read never carried, so the entry appears not to
* have been made, and entering it again does not help.
*
* So the month grid gets its own read when it steps outside the anchor. A month
* is a bounded thing to fetch, the anchored window stays exactly as it was for
* every surface that means "today", and the two grids merge by date.
*
* Pure and dateless — the caller supplies both the month and the window.
*/
/** An inclusive `YYYY-MM-DD` range, the shape the calendar read takes. */
export interface CalendarWindow {
from: string;
to: string;
}
/** Zero-padded `YYYY-MM-DD`. */
function ymd(year: number, month: number, day: number): string {
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
}
/** First and last day of a `YYYY-MM` month. */
export function monthBounds(month: string): CalendarWindow {
const [year, monthIndex] = month.split("-").map(Number);
// Day 0 of the following month is the last day of this one, leap years and all.
const lastDay = new Date(year, monthIndex, 0).getDate();
return { from: ymd(year, monthIndex, 1), to: ymd(year, monthIndex, lastDay) };
}
/**
* The extra window a visible month needs, or null when `anchor` already covers
* it and the page should read nothing more. Day keys are zero-padded, so a
* plain string compare is a date compare.
*/
export function monthWindowOutside(
month: string,
anchor: CalendarWindow,
): CalendarWindow | null {
const bounds = monthBounds(month);
if (bounds.from >= anchor.from && bounds.to <= anchor.to) return null;
return bounds;
}
/** The `YYYY-MM` a `YYYY-MM-DD` day key belongs to. */
export function monthOf(date: string): string {
return date.slice(0, 7);
}