fix(medications): keep a record-only schedule unless the write names trackIntake

The shipped iPhone app cannot see the schedule of a medication kept as a
record, because it is served with an empty schedules list. Its editor opens
such a medication as daily at 08:00 and, once any schedule field is touched,
sends a full replacement built from that default, plus asNeeded and oneShot,
and never trackIntake. On a record-only medication the update route now
drops schedules, asNeeded and oneShot from a write that does not name
trackIntake and applies the rest. The web schedule editor and the wizard
name the flag on every save.
This commit is contained in:
Marc-André Bombeck committed 2026-09-25 09:24:38 +02:00
1 parent 94ec9ab932
commit e5236d2e6a
6 files changed
+263 -7

No files matched your search

@@ -691,6 +691,7 @@ export function MedicationDetailTabs({
>
<ScheduleTimesEditor
medicationId={id}
trackIntake={!recordOnly}
schedules={medication.schedules.map((s) => ({
id: s.id,
windowStart: s.windowStart,
@@ -82,9 +82,17 @@ function effectiveTimes(s: EditableSchedule): string[] {
export function ScheduleTimesEditor({
medicationId,
schedules,
trackIntake,
}: {
medicationId: string;
schedules: EditableSchedule[];
/**
* v1.39.1 (#1033) — the medication's intake-tracking state, echoed on the
* save. The server keeps the stored schedule of a record-only medication
* unless the write names `trackIntake`, because a client that does not
* know the field cannot have seen that schedule.
*/
trackIntake?: boolean;
}) {
const { t } = useTranslations();
const queryClient = useQueryClient();
@@ -114,6 +122,7 @@ export function ScheduleTimesEditor({
// Replace schedules wholesale — preserve every cadence field from
// the snapshot field-by-field, change only the times + windows.
const body = {
...(trackIntake !== undefined && { trackIntake }),
schedules: schedules.map((s, i) => {
const edit = edits[i];
const times =
@@ -610,11 +610,11 @@ export interface CreateMedicationBody {
*/
asNeeded: boolean;
/**
* v1.39.1 (#1033) — intake tracking. Sent on create and edit for a
* scheduled medication (the wizard hydrates it from the stored value);
* omitted for an as-needed one, which is never due either way.
* v1.39.1 (#1033) — intake tracking, sent on every create and edit (the
* wizard hydrates it from the stored value). An as-needed medication
* always tracks intake.
*/
trackIntake?: boolean;
trackIntake: boolean;
schedules: Array<{
id?: string;
windowStart: string;
@@ -825,7 +825,10 @@ export function buildCreateBody(
}),
oneShot: isOneShot,
asNeeded: isAsNeeded,
...(!isAsNeeded && { trackIntake: committed.trackIntake }),
// Always named, so the server applies this schedule even to a
// medication kept as a record. Turning a medication into an as-needed
// one means doses will be logged, so it tracks intake again.
trackIntake: isAsNeeded ? true : committed.trackIntake,
schedules: draftsToEmit.map((draft) =>
encodeScheduleDraft(draft, isOneShot),
),