Vaccinations: the immunization record, visible and reachable - #751
Merged
Conversation
…red-record family - register /vaccinations in the clinical nav spine, module-gated on vaccinations (default-on) and part of the shared record - join /vaccinations to the profile shared-record route family so a scoped profile grant can present the page it is allowed to read - add the page shell: authenticated, module-gated, with the standard module-off redirect the medications surface uses - add the nav label and the surface title/subtitle/empty copy in six locales
…series - read the immunization log through the query-key factory, unwrapping the envelope; the series arrive resolved per component antigen from the DTO - render one group per antigen, a combined dose appearing in each of its component groups with that component's own position; free-text and dead-slug rows fold under their verbatim name - neutral cards, meta in muted, no due status on the page - add the dose-list dependent-keys bundle, evicting the preventive-care root alongside the dose list so a satisfied booster never lingers on checkups - series/lot/error copy in six locales
- one sheet for create and edit, saving with a date and one identity arm (catalogue pick or free text); nothing else can block the save - catalogue picker searches localised names and generic synonyms over the static antigen list, never overwriting the free-text arm - optional dose/series, lot, site, practice combobox, visit suggestion and note, all reusing the encounter-form primitives - write hooks fan out through the dose dependent-keys and the reminder reads - form/site/sheet copy in six locales
- compose rung-1 sentences from the seed's series length, booster interval and category over a handful of i18n templates, with the per-entry source as a verbatim footnote; nothing personal, no age or history conditioning - surface it in the picker's chosen-entry detail and behind the antigen group header's info affordance - schedule templates in six locales, plural-aware
- rung 2: a prefilled, user-confirmed, editable, declinable prompt writes an ordinary VORSORGE reminder keyed on the dose's primary antigen; the published origin enum and reminder DTO are unchanged - idempotent by antigen — a second confirmation re-anchors the existing reminder rather than minting a second (WR-9 seen red on the reminder count, reverted by content hash) - rolling day cadence so logging the next dose re-anchors the due date through the Phase 1 satisfy matcher; the whole loop proven through real routes - booster prompt copy in six locales, plural-aware
- from the record: a document link picker on the capture form, gated on the documents module (blanked when off, never post-filtered) - from the upload: a VACCINATION scan offers to file against a dose recorded within the shared ±7-day window; a single candidate pre-selects, two offer a picker with nothing pre-selected (WR-8 seen red on the pre-select rule, reverted by content hash), zero shows nothing, and it never blocks the upload - the suggestion rule reuses the visit moment's window constant rather than forking it; it registers with the shared filing module when that lands - suggestion and link copy in six locales
- IMMUNIZATIONS report leaf in the history group, gated both ways: the vaccinations module and the leaf selection each exclude independently (WR-7 seen red on the selection gate, reverted by content hash) - collector loads the whole live history (reference data, not windowed, like allergies) with server-resolved series positions; the PDF renders a compact per-dose table — date, vaccine, dose, lot — with no due-status or gap analysis - added to the default template so a new report offers it; stored selections and frozen share links never widen - leaf label and section copy in six locales
- the ~35 antigen and combination display names, medical orthography per locale, umlauts intact end to end; disease and antigen names only, never a trade name - the three clinical proper nouns whose written form is identical across locales (Hib, Hepatitis A/B in Spanish) are recorded as legitimate matches
…aces - freeze the booster-mint and upload-suggestion routes into the sharing surface guard with their why sentences; delegable-write and mutation inventories move by the two routes they add - inventory the two new routes as module-exempt data routes (SURFACE-gated, like the medications posture) - give the booster-plan audit action its own owner-facing verb in six locales - register the dose delete as a destructive control; route the vaccination success toasts through the outcome module - move the report leaf counts (structured 15→16, all 92→93) and the history group count in the panel; regenerate the OpenAPI selection bound - seed the immunization rows in the export-route mock so the aggregator no longer throws for a fully-selected report
Five Playwright flows over the built app, each asserting stable data-slot attributes rather than viewport text: transcribe a catalogue pick with a lot into its antigen group, a combination dose rendering under each of its three component groups, the booster prompt minting a Vorsorge reminder that shows on checkups and moves its due date when the next dose is logged, the module-off redirect with the nav entry gone, and an axe pass over the list and the form. The catalogue option gains a data-catalog-slug hook so a flow can pick an exact antigen deterministically. The spec runs in a single project and serial, and clears its own rows before each test, so a grouped verdict counts only what the test wrote.
… paths Two record-scope properties through the real routes and a real Postgres. A Guardian standing inside a managed profile writes a dose against the resolved record — the profile's rows, never the Guardian's own — and reads it straight back. A delegate holding a profile READ grant on an ordinary record lists and reads the doses but is refused the PATCH with a 403, the escalation stopped at the manage check before the row is looked up, and the row is left untouched.
The capture helper branched on an instant isVisible() read to pick between the header add and the empty-state add. Right after a navigation that read races the client render and loses, so it fell through to the empty-state button, which is gone once a record exists, and the click timed out. The header add renders in both states; clicking it directly lets click() auto-wait for hydration.
The booster mint adds one mutating handler, so the admitted inventory and its strict-driver and effect mirrors are 75, not 74. The registry and the frozen route count already moved; these three assertions had not.
…fter satisfy A rolling reminder at the engine's ten-year maximum interval lost its next due date the moment its cadence was satisfied. The rolling branch of nextOccurrenceAfter bounded its search by the 10-year anti-spin hardCap, and a satisfied reminder computes its next slot with after === lastSatisfiedAt, so the single deterministic slot lands exactly after + 3650 days — equal to the hardCap to the millisecond. The notify-hour slot then spilled a few hours past the raw boundary and was clipped to nothing, leaving nextDueAt null. The rolling slot is one deterministic instant, not an open-ended walk, so the anti-spin cap must never gate it. Bound the rolling search by that slot's own day instead; endsOn stays enforced inside expandRolling, so a course-window reminder still self-expires. This surfaced on a ten-year tetanus booster: logging the next dose silently dropped the reminder instead of rolling it forward a decade.
…ur slot's own day A rolling reminder at the ten-year maximum interval lost its next due when the satisfy instant fell in the late-UTC evening. The next-occurrence search bounded the single deterministic rolling slot by the end of the anchor's UTC day, but the applied notify-hour slot lands on the following UTC day whenever the anchor sits in the evening (the local morning is the next calendar day in a positive-offset zone). The anchor-day ceiling was then a day short and clipped the very slot it meant to admit, so logging the next dose set nextDueAt to null and the booster silently disappeared. Key the ceiling off the applied slot instant instead; endsOn stays enforced inside expandRolling.
The booster route was admitted into the MANAGE matrix but had no driver, so the strict-driver and real-effect inventories stayed one short of the admission count and the completeness guards failed. Register the write-effect driver that mints the reminder against a tetanus dose and asserts the delegated WRITE actor on the audit row, matching every other admitted write.
A role=combobox button is not named from its content, so the visible placeholder text left both the vaccine-catalogue trigger and the shared practitioner trigger without an accessible name, and the wrapping field label pointed at an id the button did not carry. Name each from its field label. The practitioner trigger is shared, so the visit form gains the same name. Also settle CSS animations before the accessibility scan and poll the reminder reads in the booster flow: the scan otherwise raced the capture sheet's fade and measured a half-opacity blend as a contrast failure, and the flow read the reminder list before the mint and the re-anchor had propagated. Both wait for the settled state rather than relaxing any assertion.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1 laid the data layer, the catalogue and the satisfy matcher without a surface. This makes the record reachable.
A vaccinations page lists a lifetime immunization record grouped by antigen, with each series resolved server-side (position, total, booster). A combination shot appears in every component group. A record whose catalogue entry no longer resolves, or a free-text entry, reads under its own name.
Capture is a sheet: pick from the catalogue (searching disease and antigen names, never trade names) or type it as the pass shows it, either alone is enough. Date plus one identity arm saves; nothing else is required. Lot, site, practitioner, an optional visit link and a note are all optional.
Where the catalogue carries a schedule, the entry shows it with its source, as information rather than a personal recommendation.
After a dose whose catalogue entry carries a booster interval, the record offers a prefilled reminder. Confirming mints an ordinary preventive-care reminder, so it lists on the checkups page, the digest and the report with no special case. Logging the next dose re-anchors it forward, and one antigen never holds two minted reminders.
A vaccination scan filed in the vault can attach to its record, and the record can link documents it already holds. The doctor report gains an immunization section, gated by the module and by its own selection, and no existing report or share link widens.
Six locales throughout, including the catalogue names. End-to-end coverage exercises transcribe, the combination case, the mint and satisfy loop, the module-off state and accessibility.