feat(health): add student health records and infirmary visit log (fixes #264) - #269
feat(health): add student health records and infirmary visit log (fixes #264)#269MOHITKOURAV01 wants to merge 2 commits into
Conversation
Adds a health profile per student and an append-only infirmary visit log. Access is deliberately blunt because this is the most sensitive data the app holds: the owning student, or staff/admin. Every read of someone else's record goes through one `assertMayAccess` gate, so a new endpoint cannot ship without the check. There is no delete route for a visit — the log is append-only, which is the only way it stays worth anything if it is ever questioned. `attendedBy` is taken from the token and ignored if sent in the body; an entry saying who treated a child is worthless if the child's own request can set it. An outcome of sent-home or referred-to-hospital is refused unless the parent has been notified, which is the failure this log exists to prevent. Temperature is bounded to 30-45°C so a Fahrenheit reading typed into a Celsius field is rejected rather than filed as a normal observation. `criticalAlerts` is a virtual over the allergy and condition lists, so the strip the nurse reads before treating cannot drift from the data under it. Closes Sitaram8472#264
Merge note for whoever lands theseThis PR is one of five sibling feature PRs (#267–#271). Each is independent in its own right — separate models, controllers, routers, pages and panels, with no shared logic — but all five register themselves in the same three files:
So the first of the five to merge will go in clean, and the remaining four will then conflict in exactly those three files. Nothing else conflicts. I simulated the full sequential merge locally and verified the combined result. Two things worth passing on:
Verified on the fully merged tree (all five together):
Happy to rebase and push the resolution on this branch as soon as the first sibling lands; just say which order you want them in. |
The profile and visit list endpoints passed the raw query string to `new RegExp`, so the search term was silently reinterpreted as a pattern. On this module that matters more than most: a search of `.*` would return every student who has a health profile, which is the roster of children with recorded medical conditions. A term like `(a+)+$` triggered catastrophic backtracking, roughly two minutes of pinned CPU for a 33-character query string, and an unbalanced `[` threw and surfaced as a 500. Metacharacters are now escaped so terms match literally, capped at 80 characters.
Related Issue
Closes #264
Description
Adds a health profile per student and an append-only infirmary visit log — blood group, allergies, chronic conditions, vaccinations and emergency contacts on one side; complaints, vitals, medication given and outcomes on the other.
A student opening
/healthsees their own card: a red critical-alerts strip at the top, blood group and BMI with a plain-language band rather than a bare number, allergy and condition lists colour-coded by severity, a vaccination timeline flagging what is now due, emergency contacts, and their own visit history.The nurse gets a Dashboard tab: today's visits, a record-a-visit form with vitals and medication rows, a students-with-severe-allergies strip, the week's most common complaints, and follow-ups due.
Things worth a reviewer's attention
This is the most sensitive data the application would hold, so a few decisions are deliberately blunt rather than clever.
There is no "any authenticated user" read path in this module. Every read of someone else's record goes through a single
assertMayAccessgate — the owning student, orstaff/admin. Keeping it in one function means a new endpoint cannot ship without the check by forgetting to add it.There is no DELETE route for a visit. The log is append-only. A medical log that can be tidied up after the fact is not worth having if it is ever questioned.
attendedByis taken from the token and ignored if sent in the body. A record saying who treated a child is worthless if the child's own request can set it.A child cannot be sent home without the parent being told. An outcome of
sent-homeorreferred-to-hospitalis rejected at the model level unlessparentNotifiedis set. This is the specific failure the log exists to prevent, so it is a hard validation rather than a UI nudge.Temperature is bounded to 30–45 °C. The point is not the physiological range — it is that
98.6typed into a Celsius field gets rejected with a message naming the likely cause, instead of being filed as a normal observation and skewing every subsequent report.criticalAlertsis a virtual over the allergy and condition lists rather than a stored field, so the strip the nurse reads before treating cannot drift from the data underneath it. The panel fetches it on blur of the student-id field, so severe allergies are on screen before the treatment is written, not after.Exactly one primary emergency contact is enforced; two is a
400. With a single contact and none flagged, the model picks it rather than making the office tick a box.A note on Mongoose 9
Mongoose 9 has dropped callback-style middleware.
pre('validate', function (next) {...})is silently skipped, andnext(err)throwsnext is not a function. Every hook here is anasyncfunction that throws. Flagging it because the failure is silent — a hook written the old way looks fine and simply never runs, which for the parent-notification guard above would be a genuinely bad outcome.Pages / Components Added or Modified
backend/models/HealthProfile.js— new; allergies, conditions, vaccinations, contacts,criticalAlerts/bmi/agevirtualsbackend/models/InfirmaryVisit.js— new; vitals, medications, outcome guardbackend/controllers/healthController.js— new; 10 handlersbackend/routes/healthRoutes.js— new; mounted at/api/health, no delete route by designbackend/server.js— two lines to register the routerfrontend/src/pages/HealthRecord.jsx— new; student page at/healthfrontend/src/components/teacher/InfirmaryPanel.jsx— new; Dashboard tabfrontend/src/App.jsx— lazy import plus the/healthroutefrontend/src/pages/TeacherDashboard.jsx— one tab entryVerification
node --checkpasses on every new backend filenpx eslintclean on the new frontend filescriticalAlertspicking up severe allergies and active conditions only, two primary contacts rejected, lone contact auto-promoted, future date of birth, invalid blood group, a next-dose date before the administered date, overdue-vaccination filtering, both notification-requiring outcomes rejected withoutparentNotified,98.6rejected as a Celsius temperature, malformed blood pressure rejected while an omitted one passes, a future visit rejected, follow-up without a date rejected, and a missingattendedByrejectedChecklist
Notes
Purely additive; no existing model or controller is touched. If the project later adds a parent role, the
assertMayAccessgate is the one function that needs to learn about it — which was the point of centralising it.