feat(medications,admin): the schedule's history is visible and invites count their uses

The schedule tab shows its eras as a collapsible timeline — current
plan since its date, predecessors with their times and span — and a
past era can be added manually for old history, with validation against
overlaps and the live plan. Invite creation chooses one to fifty uses
and the redemption list scrolls.
This commit is contained in:
Marc-André Bombeck committed 2026-06-11 11:45:58 +02:00
1 parent a62e00cfcd
commit 5639da4333
11 files changed
+1847

No files matched your search

+268
View File
@@ -2398,6 +2398,113 @@ paths:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"429": *a4
/api/medications/{id}/schedule-revisions:
get:
tags:
- Medications
summary: List a medication's archived schedule eras
description: Returns every archived schedule era (newest first) plus `currentSince`, the instant the live plan took
over. The dose-history ledger and compliance tallies already mint past days against these eras; this read powers
the Zeitplan-tab history timeline.
parameters:
- in: path
name: id
schema:
type: string
required: true
responses:
"200":
description: Era list.
content:
application/json:
schema:
$ref: "#/components/schemas/ListScheduleRevisionsResponse"
"401": *a1
"404":
description: Medication not found (or owned by another user).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"429": *a4
post:
tags:
- Medications
summary: Append a manual schedule era (pre-tracking history)
description: Records that the medication dosed at the given daily times during `[validFrom, validUntil)` — history from
before the schedule was edited in the app. The era must end at or before the start of the live plan and must not
overlap an existing era; violations return 422. The snapshot is shaped exactly like a write-path archive
(`FREQ=DAILY`, window pulled to the min/max of the times), so every historical surface reads it transparently.
Audits as `medication.schedule_revision.created`.
parameters:
- in: path
name: id
schema:
type: string
required: true
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateScheduleRevisionRequest"
responses:
"201":
description: Manual era created.
content:
application/json:
schema:
$ref: "#/components/schemas/CreateScheduleRevisionResponse"
"401": *a1
"404":
description: Medication not found (or owned by another user).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"429": *a4
/api/medications/{id}/schedule-revisions/{revisionId}:
delete:
tags:
- Medications
summary: Delete a manually added schedule era
description: "Removes a `MANUAL` era the owner appended through the sibling POST. Write-path archives (`source:
ARCHIVED`) are immutable history and refuse with 409. Audits as `medication.schedule_revision.deleted`."
parameters:
- in: path
name: id
schema:
type: string
required: true
- in: path
name: revisionId
schema:
type: string
required: true
responses:
"200":
description: Deletion succeeded.
content:
application/json:
schema:
$ref: "#/components/schemas/DeleteScheduleRevisionResponse"
"401": *a1
"404":
description: Medication or revision not found (or owned by another user).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"409":
description: The revision is a write-path archive (`ARCHIVED`) and cannot be deleted.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"429": *a4
/api/auth/me/coach-prefs:
get:
tags:
@@ -4791,6 +4898,32 @@ components:
description: Free-text medication description payload. The route runs the text through the Coach provider chain and
returns a partial structured payload the wizard merges. Rate-limited 10 requests / 5 minutes / user;
budget-gated against the daily Coach token ceiling.
CreateScheduleRevisionRequest:
type: object
properties:
validFrom:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
description: Inclusive start instant of the manual era (ISO 8601).
validUntil:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
description: Exclusive end instant of the manual era (ISO 8601). Must precede the current plan.
timesOfDay:
minItems: 1
maxItems: 8
type: array
items:
type: string
pattern: ^([01]\d|2[0-3]):([0-5]\d)$
description: Daily dose times (HH:mm, user local) the era ran at. 1–8 entries.
required:
- validFrom
- validUntil
- timesOfDay
description: "Manual schedule-era payload: era bounds plus the daily dose times that were live during the era."
DisableCoachFlag:
type: object
properties:
@@ -9021,6 +9154,141 @@ components:
additionalProperties: false
description: The two-row card block whose windows scale with dosing cadence (dense meds keep 7 / 30 days, sparse meds
step both windows up). NOT the 30-day denominator — read `compliance30.totalExpected` for that.
ListScheduleRevisionsResponse:
type: object
properties:
data:
type: object
properties:
currentSince:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
description: "Instant the LIVE plan took over: the newest revision's `validUntil`, or the medication's `createdAt` when
no era has been archived."
revisions:
type: array
items:
$ref: "#/components/schemas/MedicationScheduleRevision"
description: Archived eras, newest first.
required:
- currentSince
- revisions
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
MedicationScheduleRevision:
type: object
properties:
id:
type: string
validFrom:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
description: Inclusive start instant of the era.
validUntil:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
description: Exclusive end instant of the era — the moment the next plan took over.
source:
type: string
enum:
- ARCHIVED
- MANUAL
description: Provenance. ARCHIVED = minted by the schedule-replace write path (immutable). MANUAL = user-entered
pre-tracking era (deletable).
entries:
type: array
items:
$ref: "#/components/schemas/ScheduleRevisionEntry"
required:
- id
- validFrom
- validUntil
- source
- entries
additionalProperties: false
description: One archived schedule era covering `[validFrom, validUntil)`. The dose-history ledger, compliance tallies,
and cadence chips mint past days against the era that was live then.
ScheduleRevisionEntry:
type: object
properties:
timesOfDay:
type: array
items:
type: string
description: Daily dose times (HH:mm, user local) the era ran at.
label:
anyOf:
- type: string
- type: "null"
dose:
anyOf:
- type: string
- type: "null"
scheduleType:
type: string
description: Schedule-type discriminator of the archived row (SCHEDULED / PRN / CYCLIC).
required:
- timesOfDay
- label
- dose
- scheduleType
additionalProperties: false
description: Display summary of one archived schedule row inside an era. The full snapshot (windows, rrule, doseWindows,
…) stays server-side; this projection carries what the timeline renders.
CreateScheduleRevisionResponse:
type: object
properties:
data:
$ref: "#/components/schemas/MedicationScheduleRevision"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
DeleteScheduleRevisionResponse:
type: object
properties:
data:
type: object
properties:
deleted:
type: boolean
required:
- deleted
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
GetCoachPrefsResponse:
type: object
properties: