fix(illness): log a day of a recovered illness inside the episode

"Log day" always opened on today and let the picker run to today, so
for an illness marked recovered on Monday a note added on Thursday was
filed three days after the recovery, and the day view showed a sick day
with no illness beside it. Both places that open the sheet now hand it
the episode's last day: today while it runs, the recovery day (read in
the profile zone, as the server reads it) once it ended. The picker
opens there and offers nothing later.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 22:33:41 +01:00
1 parent 71d4516808
commit 3edb63d759
5 files changed
+94 -9

No files matched your search

@@ -47,7 +47,10 @@ vi.mock("@/components/symptoms/symptoms-section", () => ({
SymptomsSection: () => null,
}));
vi.mock("../new-episode-sheet", () => ({ NewEpisodeSheet: () => null }));
vi.mock("../log-day-sheet", () => ({ LogDaySheet: () => null }));
vi.mock("../log-day-sheet", () => ({
LogDaySheet: () => null,
useLastLoggableDay: (today: string) => today,
}));
vi.mock("@/components/day/day-link", () => ({
DAY_LINK_SLOT: "{date}",
DayLinkStated: ({ children }: { children: React.ReactNode }) => children,
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { lastLoggableDay } from "../log-day-sheet";
/**
* "Log day" on an illness opens on the day it suggests and bounds the picker
* there. For a recovered episode that used to be today, so one tap on Save
* filed a sick day days after the recovery, and the day view then showed it
* with no illness running beside it.
*/
describe("lastLoggableDay", () => {
it("is today while the episode runs", () => {
expect(lastLoggableDay("2026-11-12", null, "Europe/Berlin")).toBe(
"2026-11-12",
);
});
it("is the recovery day once the episode ended before today", () => {
expect(
lastLoggableDay(
"2026-11-12",
"2026-11-09T08:00:00.000Z",
"Europe/Berlin",
),
).toBe("2026-11-09");
});
it("stays today when the episode ended today", () => {
expect(
lastLoggableDay(
"2026-11-12",
"2026-11-12T08:00:00.000Z",
"Europe/Berlin",
),
).toBe("2026-11-12");
});
it("reads the recovery day in the profile zone", () => {
// 23:30 UTC is already the next day in Berlin.
expect(
lastLoggableDay(
"2026-11-12",
"2026-11-09T23:30:00.000Z",
"Europe/Berlin",
),
).toBe("2026-11-10");
});
});
@@ -30,7 +30,7 @@ import { canReadBodySites } from "@/hooks/use-body-sites";
import { IllnessCorrelationCard } from "./illness-correlation-card";
import { IllnessDayTimeline } from "./illness-day-timeline";
import { LogDaySheet } from "./log-day-sheet";
import { LogDaySheet, useLastLoggableDay } from "./log-day-sheet";
import { NewEpisodeSheet } from "./new-episode-sheet";
import { EpisodeMenu } from "./episode-menu";
import { useIllnessEpisode, useResolveEpisode } from "./use-illness";
@@ -66,6 +66,7 @@ export function IllnessEpisodeDetail({ episodeId }: { episodeId: string }) {
const [logOpen, setLogOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
const today = todayLocal();
const logLastDay = useLastLoggableDay(today, episode?.resolvedAt);
if (isError) {
return (
@@ -226,7 +227,7 @@ export function IllnessEpisodeDetail({ episodeId }: { episodeId: string }) {
open={logOpen}
onOpenChange={setLogOpen}
episodeId={episode.id}
date={today}
date={logLastDay}
onsetDate={episode.onsetAt.slice(0, 10)}
/>
<NewEpisodeSheet
+3 -2
View File
@@ -42,7 +42,7 @@ import { cn } from "@/lib/utils";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { applyOrder, useModuleListPrefs } from "@/lib/module-list-prefs";
import { LogDaySheet } from "./log-day-sheet";
import { LogDaySheet, useLastLoggableDay } from "./log-day-sheet";
import { NewEpisodeSheet } from "./new-episode-sheet";
import { EpisodeMenu } from "./episode-menu";
import { IllnessInsightsCard } from "./illness-insights-card";
@@ -384,6 +384,7 @@ export function IllnessView() {
const [logEpisodeId, setLogEpisodeId] = useState<string | null>(null);
const logEpisode = episodes?.find((e) => e.id === logEpisodeId) ?? null;
const logLastDay = useLastLoggableDay(today, logEpisode?.resolvedAt);
/**
* Group the flat list into active / resolved, nesting flares and recurring
@@ -532,7 +533,7 @@ export function IllnessView() {
if (!open) setLogEpisodeId(null);
}}
episodeId={logEpisodeId}
date={today}
date={logLastDay}
onsetDate={logEpisode?.onsetAt.slice(0, 10)}
/>
) : null}
+36 -4
View File
@@ -24,7 +24,12 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import {
useDisplayTimezone,
useFormatters,
useTranslations,
} from "@/lib/i18n/context";
import { userDayKey } from "@/lib/tz/format";
import { useUnitDisplay } from "@/hooks/use-unit-display";
import { ILLNESS_SYMPTOM_CATALOG } from "./symptom-catalog";
@@ -35,7 +40,11 @@ interface LogDaySheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
episodeId: string;
/** The day to seed the picker with (usually today). */
/**
* The last day of the episode (`YYYY-MM-DD`): the day the picker opens on
* and the latest it offers. Today while the episode runs, its recovery day
* once it ended (`useLastLoggableDay`).
*/
date: string;
/**
* Episode onset day (`YYYY-MM-DD`) — the earliest day the picker allows, so
@@ -45,6 +54,29 @@ interface LogDaySheetProps {
onsetDate?: string;
}
/**
* The last day a log can belong to: today, or the recovery day when the
* episode ended before today. The recovery day is read in the profile zone,
* the zone the server reads it in.
*/
export function lastLoggableDay(
today: string,
resolvedAt: string | null | undefined,
timeZone: string,
): string {
if (!resolvedAt) return today;
const resolvedDay = userDayKey(new Date(resolvedAt), timeZone);
return resolvedDay < today ? resolvedDay : today;
}
/** `lastLoggableDay` in the viewer's profile zone. */
export function useLastLoggableDay(
today: string,
resolvedAt: string | null | undefined,
): string {
return lastLoggableDay(today, resolvedAt, useDisplayTimezone());
}
/** Mini toggle chip — the shared selection vocabulary. */
function Chip({
active,
@@ -147,8 +179,8 @@ export function LogDaySheet({
const { toDisplay, fromDisplay, unitFor } = useUnitDisplay();
const feverUnit = unitFor("BODY_TEMPERATURE");
// The day being logged — seeded from `date` (today), but the picker lets the
// user backdate within [onset, today] for the retrospective journal.
// The day being logged — seeded from `date` (the episode's last day), and
// the picker lets the user backdate within [onset, date] for the journal.
const [selectedDate, setSelectedDate] = useState(date);
// Re-seed the picker each time the sheet opens (the open transition).
const [pickerWasOpen, setPickerWasOpen] = useState(false);