HandbookLearning14

Student health & care.

A school nurse needs three things her SIS rarely gives her: a visit log she can close (not just open), an allergy alert that fires before she prescribes, and a parent notification that lands at the school's chosen severity threshold without her clicking Send. YESS treats Student health like the safety system it is. The Transport→Infirmary contract back-links every bus injury to the source incident; the dispense trigger auto-decrements stock; the nightly cron auto-seeds screening cycles; the outbreak fanout is one click. The nurse never has to remember what to do next — the data flows do.

  • 9tables
  • 4triggers + 1 cron
  • 2cross-cluster fanouts
  • EN/FR/ARevery surface

Prologue

What a school nurse's day actually looks like#

07:30. The nurse opens her dashboard at /dashboard/hostel/infirmary. Two new visits already exist — auto-created at 07:18 when the 07:15 bus incident hit the Transport→Infirmary trigger. The rows show a destructive "transport" badge, severity inherited from the incident, and (since 00645) a clickable incident_id back to the bus context: photos, GPS trail, other injured students. The nurse opens each visit, adds vitals + diagnosis + treatment, dispenses paracetamol + ibuprofen (the auto-decrement trigger handles stock), and closes the visits with outcome="returned_to_class" by 07:45.

09:14. A boarder walks in feeling sick. The nurse creates a new visit — the dialog fires a health-profile lookup the moment she picks the student in the combobox, and a red banner surfaces drug allergens (penicillin) and chronic conditions (asthma) above the prescribe surface. She wouldn't have remembered. The visit hits severity='high' and the school has configured parent_notify_severity_threshold='high', so the AFTER INSERT trigger fans a notification to the parents' phones before the dialog even closes.

11:50. A fourth student appears with the same complaint as three earlier visitors. The Outbreaks page already flags the cluster — a 3+ visit pattern in a 7-day window. The nurse promotes it to a formal outbreak, escalates the status, types "Class isolation, deep clean, water-source test" into containment, and clicks Notify parents. The RPC walks every affected student's parents and writes one notification per parent. The button now says "Re-notify parents (sent 1×)" — if the outbreak escalates again tomorrow, she can fanout again with a fresh update.

Nine chapters. One health record. Read this one first.

Chapter 1

Nine tables, one chart#

The schema covers everything a school nurse touches — and ONLY what a school nurse touches. Private by default; only the staff role with health.view sees a student's chronic conditions; only health.manage can edit them.

  • Visits & treatment — infirmary_settings (per-school config, nurse roster, parent-notify threshold), infirmary_visits (the patient log with vitals JSONB, severity, outcome, follow-up, billing hooks), infirmary_meds (catalogue + stock + expiry), infirmary_meds_dispensed (per-visit dispense log + auto-decrement trigger).
  • Persistent records — student_health_profile (allergens — food, drug, environment — chronic conditions, current medications, emergency contacts, treatment consent), vaccinations (WHO EPI catalog + per-student dose records).
  • Surveillance — health_screening_cycles (per-school campaign definitions), health_screening_records (per-student-per-cycle outcomes), health_outbreaks (case management with parent fanout count), health_outbreak_cases (per-affected student).

Every table has RLS scoped to public.get_school_id() and an updated_at trigger. Permissions for the cluster live under the dedicated infirmary.* and health.* keys (00645 seeded them; previously the pages piggybacked on hostel.view — a privacy fix that's now landed).

Visit lifecycle

Student arrives at infirmary

  1. Create — patient + complaint + severity
    • Nurse picks patient (student / staff / visitor), visit kind (walk-in / boarder-sick / class-sent / accident-emergency / routine / follow-up), severity, complaint.
    • INSERT into infirmary_visits stamps visit_started_at + created_by.
    • If transport_trip_incident.injuries_reported=true, the 00633 fanout trigger auto-creates the row with visit_kind='accident_emergency' AND incident_id pointing back to the bus incident (00645 Phase B).
  2. Allergy check — read student_health_profile
    • Visit detail dialog runs useHealthProfile(student_id) the moment patient is set.
    • Red banner: drug allergens (destructive badge), food allergens, chronic conditions, current_medications.
    • Dispense sub-dialog repeats the drug-allergen warning so the nurse can't miss it at the point of prescription.
  3. Treat — vitals + diagnosis + treatment + dispense
    • 6-field vitals form (temp / HR / BP / resp / SpO₂ / weight) writes JSONB to infirmary_visits.vitals.
    • Dispense action INSERTs infirmary_meds_dispensed with unit_cost snapshot; the 00570 trigger auto-decrements infirmary_meds.current_stock.
    • Stock-guard in the hook refuses to dispense more than current_stock — prevents oversold catalogue.
  4. Notify — parent_notify trigger fires conditionally
    • AFTER INSERT trigger trg_notify_parents_on_infirmary_visit (00645 Phase C) consults infirmary_settings.parent_notify_on_visit + parent_notify_severity_threshold.
    • Severity-rank helper compares (any < medium < high < critical).
    • If threshold crossed AND patient is a student, walks parent_student_links → parents → user_profile_id, writes one notifications row per parent, stamps parent_notified_at on the visit.
    • EXCEPTION-wrapped so a comm-hub failure cannot poison the visit insert.
  5. Close — outcome + visit_ended_at
    • Close dialog captures outcome (returned_to_class / returned_to_hostel / sent_home / referred_external / admitted / fatal) + optional final diagnosis/treatment/notes.
    • Mutation filters on visit_ended_at IS NULL — idempotent on already-closed rows.
    • Closed visits become read-only on the detail dialog.

Visit closed + parent notified + ledger optional

Chapter 3

Allergy safety + health profile#

A student's health profile is the safety record three other modules read before they act: Canteen reads it for food allergens before serving a meal; PE reads chronic_conditions before exercises; Infirmary reads drug_allergens before prescribing. The profile is stored in student_health_profile at /dashboard/health-profiles with consent fields for routine treatment + emergency transport.

The visit detail dialog (visit-detail-dialog.tsx) calls useHealthProfile(visit.student_id) as soon as a patient is selected. The hook is a single-row maybe-fetch that returns NULL when no profile exists. When NULL, the dialog renders an italic warning telling the nurse to confirm allergies verbally. When present, drug_allergens render as destructive badges in a red banner above the prescribe surface; food allergens render as default badges; chronic conditions render as secondary badges. The same banner re-renders inside the dispense sub-dialog — the nurse can't miss it at the moment of prescription.

Chapter 4

Dispense + auto- decrement#

The dispense flow is two writes, one trigger. useDispenseMed INSERTs infirmary_meds_dispensed with a snapshot of the med's unit_cost (so historical reports survive future price edits) and the visit_id. The 00570 trigger infirmary_dispense_decrement_stock fires AFTER INSERT and decrements infirmary_meds.current_stock by the dispensed quantity — bounded below by zero via GREATEST(0, …).

The hook also guards before INSERT: it reads current_stock from the med row and raises if quantity > current_stock. So you never get a row that says "dispensed 5" against a med that only had 3 in stock; the nurse sees an error and orders more first.

Chapter 5

Transport → Infirmary contract#

The bus reports an incident with injuries → the infirmary gets the visit before the bus arrives. This is the contract shipped in 00633 and tightened in 00645.

When a transport_trip_incidents row is INSERTed with injuries_reported=true AND injured_student_ids[] non-empty, the trigger transport_trip_incident_create_infirmary_visits (00633:408, rewritten in 00645 Phase B) INSERTs one infirmary_visits row per injured student with:

  • visit_kind = 'accident_emergency'
  • severity mapped 1:1 from the incident (critical → critical, high → high, else medium).
  • complaint = 'Injury reported on transport incident: ' || incident_type
  • notes = the incident's description.
  • incident_id = the source incident's UUID — the 00645 addition. Lets the nurse click back to the bus context: photos, GPS, other injured students.

The trigger is EXCEPTION-wrapped — if the infirmary cluster is down, the transport incident still lands; the nurse sees the missing visit when the cluster recovers and can add it manually.

Chapter 6

Parent notification engine#

One trigger, one helper, one settings table:

  • Settings — infirmary_settings.parent_notify_on_visit (BOOL, default TRUE) + parent_notify_severity_threshold (TEXT, one of 'any' / 'medium' / 'high' / 'critical', default 'medium'). Configured per-school at /dashboard/hostel/infirmary/settings.
  • Helper — infirmary_severity_rank(severity) (IMMUTABLE SQL function) returns 0–4 for any/low/medium/high/critical so the trigger can compare with a single integer.
  • Trigger — trg_notify_parents_on_infirmary_visit (00645 Phase C, AFTER INSERT). Skips if patient isn't a student, skips if settings disabled, skips if severity below threshold. Otherwise: multi-hop join parent_student_links → parents → user_profile_id, writes one notifications row per parent, stamps parent_notified_at on the visit. EXCEPTION-wrapped so a comm-hub failure can't poison the visit INSERT.

The same multi-hop pattern powers Transport→Infirmary parent notifications and Boarder-safety alarms — the join is a load-bearing primitive across the cluster.

Chapter 7

Outbreak detection & parent fanout#

A health outbreak is a pattern: three or more visits sharing a condition in a 7-day window. The outbreak page at /dashboard/health/outbreaks detects them automatically via useOutbreakClusters and renders them as an amber cluster watch list. One tap promotes a cluster to a formal health_outbreaks row.

The status flow has five states (monitoringescalated containedresolved / cancelled). Each status change can carry containment_actions + an optional external_authority (district health office).

The new parent-fanout path lives at the notify_parents_on_outbreak(p_outbreak_id, p_message) RPC (00645 Phase D). The dialog's Notify parents button calls it; the RPC walks every affected student's parents and writes one notifications row per parent. The outbreak row gets parent_notified_at = now() + parent_notify_count += 1 — so subsequent calls (when the outbreak escalates) re-fire the fanout and increment the counter. The button label tells the nurse how many times she's notified already.

Chapter 8

Screenings + vaccinations#

Two cluster-wide health interventions, two surfaces.

Vaccinations. /dashboard/health/vaccinations tracks per-student dose records against the WHO EPI catalog (seedable from the page). The nurse records each dose with batch + date + provider (school clinic vs external). The KPI rail shows due / overdue / completed per cycle.

Screenings. /dashboard/health/screenings defines screening campaigns (eye, dental, hearing, general physical, anthropometric, mental health, spinal, other) with a frequency_months cadence. The nightly cron health-screening-auto-seed-daily @ 04:00 UTC (00645 Phase E) walks every active cycle past its last_seeded_at + frequency_months window and seeds one health_screening_records row per active student. The nurse marks each record completed, partial, or follow-up-required as she works through them.

Both surfaces are read-restricted to health.view; writes need health.manage. Parents see their own child's vaccination history in the parent portal but not other children's.

Permissions

Who can do what#

  • infirmary.view — read visits, meds, settings. Default for nurses (via hostel_warden role until a dedicated nurse role lands), principal, vice_principal, school_admin, and (read-only) teaching_staff for visibility on students they sent to the nurse.
  • infirmary.create — create visits, dispense meds. Default for nurses.
  • infirmary.edit — update diagnosis / treatment / vitals / notes, close visits, mark parent_notified flags. Default for nurses + admins.
  • infirmary.manage — configure settings, manage medication catalogue, set nurse staff. Default for school admins.
  • health.view — read student health profiles, vaccinations, screenings, outbreaks. Default for nurses + admins + teaching_staff + parent_guardian (scoped via RLS to their own children).
  • health.manage — edit health profiles, schedule screenings, manage outbreaks, record vaccinations. Default for school admins.

Privacy gates are enforced at the permission layer (the page guard) AND the RLS layer (parent scope to own child). The 00645 migration moved the gates from hostel.view (where a hostel warden could see every student's allergies) to the dedicated health.view key — a privacy win.

What makes this elite

  1. 01

    Allergy banner before prescription before

    useHealthProfile fires the moment the patient is picked. Drug allergens render as destructive badges above the prescribe surface AND inside the dispense sub-dialog — the nurse cannot miss them at the moment of prescription.

  2. 02

    Transport → Infirmary back-link back-link

    Bus incidents auto-create the infirmary visit with incident_id stamped (00645 Phase B). One click from the visit lands the nurse on the source incident with photos, GPS trail, and every other injured student.

  3. 03

    Parent notify on configurable threshold configurable

    infirmary_settings.parent_notify_severity_threshold lets the school choose any / medium / high / critical. AFTER INSERT trigger respects the choice, walks parent_student_links → parents → user_profile_id, fanouts in the same transaction. EXCEPTION-wrapped.

  4. 04

    Dispense + auto-decrement, never over-sold auto-decrement

    useDispenseMed snapshots unit_cost (so historical reports survive price edits), guards before INSERT that quantity ≤ current_stock, and the 00570 trigger decrements stock atomically with the dispense row.

  5. 05

    Outbreak parent fanout with re-send counter re-send

    notify_parents_on_outbreak RPC fanouts to every affected student's parents and increments parent_notify_count on the outbreak row. The button label changes to 'Re-notify parents (sent N×)' so the nurse knows the history. Re-fire on escalation.

  6. 06

    Screening cron — no manual seed no manual

    health-screening-auto-seed-daily @ 04:00 UTC walks active cycles past their last_seeded_at + frequency_months window and seeds one record per active student. The nurse never has to remember to start a term's screening round.

  7. 07

    Privacy at the permission layer privacy

    health.view + infirmary.view are dedicated keys, not piggyback on hostel.view. A hostel warden no longer sees every student's allergies. RLS adds parent-scope-to-own-child on top.