Parivar — Clinical Reporting Module
Companion to the Technical Architecture blueprint. Full specification of the nurse reporting workflow for the NestJS backend, Next.js admin/web, and the Flutter/React Native nurse app.
This is the most behaviour-heavy part of the platform. The old API reference modelled "assessments" as a single flat table; the live system is a multi-form reporting workspace with drafts, versioning, a 24-hour edit window, and an approval loop. This document is the source of truth for that workflow and supersedes the simplified ClinicalModule section in the architecture doc.
1. Eligibility — when a report can be created
A report can be created only while its visit is in_progress. The nurse must first Start Visit, which:
- sets
visits.status = 'in_progress', and - sets
nurses.status = 'on_visit'.
The Reports screen lists only visits.status === 'in_progress' as selectable. In NestJS this is a guard on POST /visits/:id/assessment*: reject with 422 BUSINESS_RULE (VISIT_NOT_IN_PROGRESS) otherwise.
2. The four form types (+ one read-only)
The reporting screen is not one form — it is a tabbed workspace over five surfaces:
| Tab | Availability | Table | Required? |
|---|---|---|---|
| Visit Report (Regular) | Always | assessments | ✅ Mandatory for every visit |
| First Visit Assessment | Only if no first_visit_assessments row exists for the parent | first_visit_assessments | Only on the first-ever visit |
| Wound Care | Always (optional) | wound_care_forms | ❌ Optional, multiple per visit |
| Clinical Modules | Always (optional) | clinical_modules | ❌ Optional |
| View Admission Form | Only if admission_forms exists | admission_forms (read-only) | N/A |
⚠️ "First visit" is determined by absence of a
first_visit_assessmentsrow for the parent, not byvisits.visit_type(which is onlyroutine/follow_up). Do not gate this on visit_type.
Suggested NestJS routes
| Action | Route |
|---|---|
| Save/submit regular report | PUT /visits/:visitId/assessment (draft) · POST /visits/:visitId/assessment/submit |
| First visit assessment | POST /visits/:visitId/first-visit-assessment |
| Add wound record | POST /visits/:visitId/wound-care |
| Clinical module | POST /visits/:visitId/clinical-module |
| Read admission form | GET /parents/:parentId/admission-form |
3. Regular Visit Report — field specification (assessments)
Section A — Vital Signs
| Field | Column | Type | Required | Range | Notes |
|---|---|---|---|---|---|
| BP 1 Systolic | bp_systolic_1 | int | ✅ | 50–250 | Paired with diastolic via "/" |
| BP 1 Diastolic | bp_diastolic_1 | int | ✅ | 30–150 | |
| BP 2 Systolic | bp_systolic_2 | int | ❌ | 50–250 | Optional second reading |
| BP 2 Diastolic | bp_diastolic_2 | int | ❌ | 30–150 | |
| Respiratory Rate | respiratory_rate | int | ✅ | 4–60 | breaths/min |
| Pulse Rate | pulse_rate | int | ✅ | 20–300 | bpm |
| Temperature | temperature | decimal | ✅ | 30–45 | °C |
| SpO2 | spo2 | decimal | ✅ | 50–100 | % |
| Blood Sugar | blood_sugar_value | decimal | Conditional | 20–600 | Required if the parent's admission_forms.is_diabetic = true |
| Blood Sugar Type | blood_sugar_type | blood_sugar_type | with sugar | — | fasting / random |
Diabetic detection: read admission_forms.is_diabetic for the parent; if true, surface blood sugar as a required, highlighted field.
Section B — Observations
| Field | Column | Type | Required | Options |
|---|---|---|---|---|
| General Condition | general_condition | general_condition enum | ✅ | normal, weak, dizzy, stable, improving, deteriorating, critical, other |
| Concerns / Issues | concerns | text | ❌ | Free text |
| Progress Notes | progress_notes | text | ✅ | Free text |
Section C — Follow-Up
| Field | Column | Type | Required | Notes |
|---|---|---|---|---|
| Doctor Review Required? | doctor_review_required | bool | ❌ | Routes to doctor queue if true |
| Emergency Escalation? | emergency_escalation | bool | ❌ | If true → immediate high-severity admin alert + likely escalations row |
Section D — Additional Notes
nurse_notes (text, optional).
Section E — Photo Documentation
| Field | Column | Type | Constraints |
|---|---|---|---|
| Photo consent | (client state) | bool | Must be ON to enable upload |
| Photos | photo_paths | text[] | ≤ 5 photos, 10 MB each, JPG/PNG/HEIC |
Storage path: {parentId}/{visitId}/{timestamp}-{filename} in the reports bucket. Upload is non-blocking — a failed photo upload must not block report submission.
Section F — Nurse Confirmation (mandatory)
nurse_confirmed (bool, ✅): "I confirm this report reflects the patient's condition during the visit." Submit is disabled until checked.
Server-managed columns
nurse_id (from nurses, not the user id), client_user_id (copied from visits.client_user_id), is_draft, submitted_at, approval_status, admin_viewed, version_count.
4. Draft vs. Submit
| Button | Validation | Sets |
|---|---|---|
| Save Draft | None | is_draft = true, submitted_at = null — visit stays in_progress |
| Submit Report | Full (Section §6 required fields) | is_draft = false, submitted_at = now(), approval_status = 'pending', admin_viewed = false, version_count = 1 — visit → report_submitted |
Drafts live in a separate "Drafts" tab with a count badge and "Last saved X ago", resumable via Continue. A draft never changes visit status.
🔁 Rebuild note. Keep draft autosave on the client; persist drafts via
PUT /visits/:visitId/assessmentwithis_draft=true. The submit endpoint runs the full validator (class-validatorDTO) and the state transition in one transaction.
5. Edit / resubmit — the 24-hour window
After submission a nurse may edit within 24 hours and only while not yet approved.
Rules:
isWithinEditWindow = (now - submitted_at) < 24h.- Blocked once
approval_status === 'approved'. - Each edit writes a snapshot to
assessment_versionsand incrementsversion_count. - Resubmission resets
approval_status = 'pending'andadmin_viewed = false. - After 24h the report locks (🔒) — "contact admin to change".
Version snapshot
assessment_versions.insert({
assessment_id, version_number, nurse_id, nurse_name,
action: "submitted" | "edited",
snapshot: { ...full assessment at this version }
})
Implement the window check server-side (never trust the client clock): compare submitted_at against server now() in the edit guard, returning 422 EDIT_WINDOW_EXPIRED.
6. Validation summary
Required to submit (not for drafts): visit selected, bp_systolic_1, bp_diastolic_1, respiratory_rate, pulse_rate, temperature, spo2, general_condition, progress_notes (non-empty), nurse_confirmed = true, plus blood_sugar_value when the parent is diabetic.
Vital ranges: Systolic 50–250 · Diastolic 30–150 · Pulse 20–300 · Temp 30–45 · SpO2 50–100 · Respiratory 4–60 · Blood Sugar 20–600.
Inputs are
type="text"on the client to avoid mobile spinner widgets; parse to number on submit. Enforce ranges again on the server.
7. First Visit Assessment (first_visit_assessments)
A one-time comprehensive intake, lock-once (is_locked = true, locked_at set on save; no draft concept; single "Save Assessment" button). Six sections:
Vital Signs (single BP reading): bp_systolic, bp_diastolic, pulse_rate, respiratory_rate, temperature, spo2, blood_sugar_value, blood_sugar_type.
Mobility: mobility_level (independent/with_aid/wheelchair/bedbound), mobility_aid (text), fall_risk (bool), fall_history (text, shown if fall_risk).
Nutrition: nutrition_status (adequate/poor/at_risk), weight, height, bmi (auto, read-only), swallowing_difficulty (bool), dietary_requirements (text).
Cognition: cognition_level (alert/confused/disoriented/unresponsive), orientation (text), memory_issues (bool), communication_ability (text).
Skin: skin_condition (intact/dry/fragile/broken), pressure_areas (bool), pressure_area_details (text, if checked), existing_wounds (bool), wound_details (text, if checked).
Continence: continence_status (text), continence_aids (text), catheter (bool), catheter_type (text, if checked).
After locking, the tab becomes "View First Visit" (read-only). In NestJS, reject edits to a locked record with 409 CONFLICT (FIRST_VISIT_LOCKED).
8. Wound Care (wound_care_forms)
Multiple wound records per visit (additive — each save creates a new record; existing records render as read-only cards above the form).
| Field | Column | Type | Required | Options |
|---|---|---|---|---|
| Wound Type | wound_type | enum | ✅ | surgical, pressure_ulcer, diabetic_ulcer, venous_ulcer, arterial_ulcer, laceration, burn, skin_tear, other |
| Location | wound_location | text | ❌ | |
| Length/Width/Depth (cm) | length_cm/width_cm/depth_cm | decimal | ❌ | |
| Colour | wound_colour | enum | ❌ | red (granulating), yellow (sloughy), black (necrotic), pink (epithelialising), green (infected) |
| Discharge | discharge_type | enum | ❌ | none, serous, sanguineous, purulent, serosanguineous |
| Photo | photo_path | file | ❌ | Single photo → wound-photos bucket |
| Dressing Plan | dressing_plan | text | ❌ | |
| Review Required | review_required | bool | ❌ | |
| Review Date | review_date | date | ❌ | Shown if review_required |
Photo path: {parentId}/{visitId}/{timestamp}-{filename} in wound-photos. Camera capture uses capture="environment".
9. Clinical Modules (clinical_modules)
Optional structured add-ons attached to a visit (FKs visit_id, nurse_id, parent_id). Treat as a flexible module container; specific module fields weren't enumerated in the source — confirm the catalogue with the clinical team and model each module as a typed payload (jsonb or a child table per module).
10. Submission side effects & state machine
On submit (not draft), inside one transaction + post-commit events:
visits.status→report_submitted.- Emit
assessment.submitted→ create anadmin_alertsrow and queue the report email (send-notification-emailequivalent →MailService,{ type: 'assessment_submitted', assessment_id }). - If
emergency_escalation = true→ high-severity alert (and create anescalationsrow of typeurgent_health).
Approval loop (admin):
in_progress
├─ Save Draft ──────────► assessments.is_draft = true (visit stays in_progress)
└─ Submit Report ───────► assessments.is_draft = false
visits.status = report_submitted
├─ Admin Approve ─► visits.status = approved
│ assessments.approval_status = approved
│ → insert nurse_earnings (pending), send purchase email + PDF
└─ Admin Reject ──► visits.status = in_progress
assessments.approval_status = rejected
└─ Nurse edits & resubmits (≤24h) ─► report_submitted
(24h elapses) ────► report locked; admin contact required
NestJS endpoints: POST /admin/assessments/:id/approve, POST /admin/assessments/:id/reject. Approval inserts earnings and fires the customer email with the report PDF link.
11. Cache invalidation (client) / event fan-out (server)
The web/mobile clients invalidate these query keys after create/update; the server should emit the matching realtime events so other clients refresh without polling:
| Client query key | Server event → realtime room |
|---|---|
nurse-assessments, nurse-visits | visits:nurse:<nurse_id> |
client-assessments, client-pending-reports, client-visits | assessments:parent:<parent_id>, visits:client:<user_id> |
admin-assessments, admin-visits, unviewed-reports-count | visits:admin, admin_alerts |
assessment-versions (on edit) | — |
12. Rebuild checklist for this module
- DTOs with
class-validatorenforcing every range and conditional (diabetic → blood sugar). - Visit-state guard (
in_progressrequired) on all create routes. - Draft autosave endpoint separate from submit; submit runs validation + transition in a transaction.
- Server-side 24h edit-window check; version snapshot written on every edit.
- First-visit lock enforced server-side; wound records additive.
- Non-blocking photo upload via pre-signed URLs (
reports,wound-photosbuckets). - Approval/reject endpoints emit earnings + email + realtime events.
- Idempotency on submit (avoid duplicate reports on retry) using a request key in Redis.
End of Clinical Reporting Module.