Nexus Docs Documentation for the Nexus hospital management system

Guides

Laboratory

OpenELIS-style lab workbench with patient-context popups — the sixth kiosk.

Two parts:

  • Part A — Business level. What lab staff (pathologists, lab nurses, lab technicians filling those roles) are responsible for, what the screen looks like when they sign in, and how the day flows.
  • Part B — Technical level. What the routes, components, RBAC, and APIs do under the hood.

Companion docs: docs/ICU_GUIDE.md, docs/MATERNITY_GUIDE.md, docs/EMERGENCY_GUIDE.md, docs/OPERATIONS_GUIDE.md, docs/WARD_GUIDE.md, docs/LAB_MODULE.md, docs/WORKFLOWS.md.

This is the sixth ward-style kiosk. It pins lab staff to the existing OpenELIS-style lab page (/lab) and adds a patient-context popup so a tech running a sample can pull up the clinical picture without leaving their workbench.


Part A — Business level

A.1 What a Lab doctor / nurse owns

ResponsibilityDetail
Lab workbenchThe single screen they sign in to: nine workflow tabs — Orders, Receiving, Workbench, Verify, Worksheets, Lab Transfers, Analytics, Reports, Catalog.
Order intakeOrders coming in from the wards (ICU / ER / Maternity / Operations / Ward) appear on the Orders tab the moment a doctor places them, routed by default to the Main Lab branch.
Specimen receiptWhen a tube arrives, Receiving records the accession number, specimen type, condition, and rejection reason if any.
Result entryWorkbench is where a tech enters numeric / coded results against each open order. Verify is where a second person signs the results off before they become final.
WorksheetsA batch view that lets the bench run multiple specimens in one go and sweep results back onto orders.
Lab-to-Lab transfersWhen the hospital runs more than one lab (Main + Microbiology + Blood Bank + satellite labs), the Lab Transfers tab is where work moves between them. A pending transfer shows up on the receiving lab’s Inbox sub-tab; they Accept (taking ownership) or Reject (with a reason).
Operational analyticsThe Analytics tab is the lab director’s view: total / pending / completed / cancelled / STAT counts over a window, leaderboards of top referring doctors and most-ordered tests, per-branch usage table, 14-day order-volume sparkline.
Walk-in patient registrationLab staff can register a new patient directly from the Orders tab (+ Register Patient button) instead of bouncing them back to reception. The form does a name / phone / national-ID search before allowing create so duplicate MRNs don’t accumulate.
Final reportingReports publishes the verified results as DiagnosticReport resources — visible to the ordering doctor under Results on their patient chart.
CatalogThe test/panel catalog itself (codes, reference ranges, units, specimen requirements) — usually edited by the lab director.
Patient contextClick any patient name on any tab to open a chart popup with the order itself plus vitals, allergies, conditions, current medications, prior reports, care plans, consents — all the bedside context needed to interpret a sample correctly.
Recording observationsThe chart’s editable categories (Vitals, Allergies, Conditions, Nursing Assess.) are mostly for context — bedside teams own these — but a phlebotomy difficulty note or a critical-value-driven allergy update can be added inline.

A.2 What they cannot do

The Lab shell is intentionally narrow — kiosk-style, like the five ward kiosks:

  • No sidebar, no global navigation, no dashboard.
  • No access to ward boards (ICU / ER / Maternity / Operations / Ward), no access to pharmacy, radiology, billing, HR, or admissions screens.
  • They CAN now create a patient record (walk-in registration from the Orders tab) and place lab orders against any patient — but they cannot see clinical history outside the lab chart popup, cannot admit a patient to a ward, and cannot collect cashier deposits.

Forbidden URLs land on the standard Forbidden page.

A.3 What the screen looks like

When a lab doctor or nurse logs in:

┌────────────────────────────────────────────────────────────────────────────────┐
│ M  Mando · Laboratory       [ Laboratory ]          🔔   Dr. Samir Khalil ┄    │
├────────────────────────────────────────────────────────────────────────────────┤
│                                                                                │
│   Laboratory                                                                   │
│   OpenELIS-style workflow · FHIR R5 compatible                                 │
│                                                                                │
│   [Orders] Receiving Workbench Verify Worksheets Lab-Transfers Analytics       │
│   Reports Catalog                                                              │
│                                                                                │
│  [ search… ] [ All statuses ▾ ]    [ + Register Patient ] [ + Place Order ]    │
│                                                                                │
│  Order #    Patient        Test / Panel         Lab          Priority  Status  │
│  LAB-2026…  John Smith     CBC (58410-2)        Main Lab     STAT      active  │
│  LAB-2026…  Maria Garcia   Glucose (2345-7)     Microbiology routine   active  │
│  …                                                                             │
└────────────────────────────────────────────────────────────────────────────────┘

Top bar: brand → single static tab “Laboratory” → notification bell → user pill → Sign out.

A.4 The day, end to end

  1. Sign in. The system bounces them straight to /lab. No dashboard.
  2. Watch the queue. Orders is the default tab. Each row shows the order #, patient name, test/panel, lab branch, priority, status, and date.
  3. Walk-in patient? Click + Register Patient. Type a name (or phone or national ID) — the form shows possible matches inline before letting you create. If it’s a real new patient, fill the form and submit; the system jumps straight into the Place Order modal with the new patient pre-selected.
  4. Click a patient name on any row → the chart popup opens with 11 cards: Lab Order, Vitals, Allergies, Conditions, Nursing Assess., Medications, Lab Orders (history), Imaging Orders, Results, Care Plans, Consents.
  5. Click “Lab Order” → snapshot of the specific order the user clicked: order #, code, display, priority, status, category, ordered date, specimen type, reason / clinical question, notes. STAT orders get a red banner.
  6. Click “Lab Orders” to see all of this patient’s lab history. The current order is highlighted with a “this order” badge so it’s easy to spot.
  7. Click “Conditions” / “Medications” to inform interpretation — e.g. an elevated glucose is expected on a patient with active diabetes who’s on insulin.
  8. Add a vital / allergy / condition / nursing note if the chart turned up something the bedside team missed.
  9. Move the sample through the workflow. Switch tabs as the workflow demands:
    • Receiving — log accession, specimen type, condition. Reject if hemolysed, clotted, mislabelled.
    • Workbench — pick the order, enter results against each test.
    • Verify — second tech reviews and signs off; preliminary → verified.
    • Worksheets — batch view for runs.
    • Reports — verified results aggregated into a DiagnosticReport and published.
  10. Need a different lab to run it? Open Lab Transfers → pick the order → choose destination → add reason → send. The receiving lab gets a notification on their bell and the transfer appears on their Inbox sub-tab.
  11. End of shift: the lab director opens Analytics to scan the day/week — pending count, STAT count, top referring doctors, most-ordered tests, per-branch volumes.
  12. Patient name is clickable on every tab — tabs share the same chart popup, mounted once at the page level.

A.5 Walk-in patient registration

A patient walks in for an outpatient draw without going through reception. Lab staff register them directly:

  1. From Orders, click + Register Patient.
  2. Start typing into the search box (top of the modal). The form runs a debounced search across name, phone, email, and national ID. Possible matches appear in an orange “Possible matches” panel.
  3. If a match appears: click it → the modal closes and the Place Order modal opens with that patient pre-selected. No duplicate MRN, no extra typing.
  4. If no match: fill the create form — full name (required), date of birth, gender, phone, national ID, email, address — and click Register & Place Order. The system creates the patient, mints an MRN, refreshes the patient list, and jumps straight into the order modal with the new patient pre-selected.

Why search-first: the system already has Master Patient Index dedup on the receptionist surface, but a walk-in lab visit is the most common path to accidentally creating a parallel record (no admission, no insurance lookup, no demographics double-check). The orange match panel is the single guardrail.

A.6 Lab Transfers

When the hospital runs more than one lab, the Lab Transfers tab is the inter-lab workflow surface.

Branch picker (top of tab): “Working at this lab” — defaults to Main Lab, persisted in localStorage. The picker shows live counters: open orders currently routed here + pending transfers waiting on this branch.

Three sub-tabs:

Sub-tabWhat it shows
InboxTransfers other labs sent to me, status pending. The badge on the tab is the count. Each row has Accept / Reject buttons.
OutgoingTransfers I sent. Read-only — I can see whether they were accepted, rejected, or completed.
All activityInbox + Outgoing combined, sorted newest first. The full audit trail.

How the workflow walks:

  1. Tech at Main Lab clicks + Transfer Order → picks an open order at this branch, picks a destination, picks a priority (routine / urgent / STAT), adds a reason (“requires special analyzer”, “overflow”, “after-hours coverage”). Submit.
  2. The transfer is created with status pending. The receiving lab gets a high-priority notification (STAT/urgent) or a normal one (routine) on their bell.
  3. The destination tech opens Lab Transfers, sees the row in their Inbox, and either:
    • Accept — status flips to accepted, the order’s current_lab_id is updated to the receiving lab. The order now appears on the receiving lab’s Orders queue and disappears from the sender’s. The sender is notified.
    • Reject — status flips to rejected with a required note (why). The order stays at the sending lab. The sender is notified at high priority.
  4. Once the work is done, the destination optionally Marks complete — separate from accepted, this is the “result has been returned” state. Useful for split-batch flows where one transfer corresponds to one analyte run, not the whole order.

Server-side guardrails:

  • A transfer cannot have from_lab_id == to_lab_id (HTTP 400).
  • Only pending transfers can be Accepted or Rejected (HTTP 409 otherwise).
  • Only pending transfers can be deleted (HTTP 409 otherwise — already-decided rows survive as audit).

Common pitfalls:

  • “The order isn’t on my Orders list anymore.” It was transferred. Switch the branch picker on Lab Transfers to your branch and check Outgoing.
  • “I accepted by mistake.” Cannot un-accept. Send a new transfer back to the original lab with a “wrong destination” reason.
  • “I want to reject without a note.” Reject requires a note — by design, so the sender knows what to fix.

A.7 Analytics

The Analytics tab is the lab director’s operational dashboard. Single API call, no charts library.

Window selector (top): Last 7 / 30 / 90 / 180 / 365 days. Default 30. Refresh button forces a re-fetch.

KPI grid: total orders, pending, completed, STAT/urgent, cancelled, transfers pending. Pending and STAT are colour-coded amber and red respectively.

Order-volume sparkline (last 14 days, clamped to the selected window): a CSS bar-chart of daily counts so you can see a Saturday dip or an outbreak spike.

Top referring doctors (left panel): top 10 requesters by count over the window. Each row is a horizontal bar plus a count.

Most ordered tests (right panel): top 10 tests by count over the window. Same bar layout.

Usage by lab branch (bottom panel): every active branch with its order volume, pending, and completed counts over the window. Useful for capacity planning (“Microbiology is sitting at 80% pending for the third week — we need a second analyzer”).

The dashboard is intentionally lightweight — bars are CSS percentages, not a chart library — so it loads fast on a kiosk machine and never needs a CDN.

A.8 Notifications

Inbound (the bell flashes):

  • New lab order placed by a doctor (the wards) — fires lab_order_new on the Laboratory department feed.
  • STAT priority orders fire at high priority; routine orders fire normal.
  • New transfer inlab_transfer_in fires when another lab routes work to this branch. STAT/urgent priority bumps to high.
  • Transfer acceptedlab_transfer_accepted fires back to the sender.
  • Transfer rejectedlab_transfer_rejected fires back to the sender at high priority.

Outbound (the lab fires automatically):

  • Verified result published → ordering doctor’s notification feed (planned — see follow-ups below).
  • Critical-value flagged → ward + ordering doctor (planned).

A.9 Common pitfalls

  • “I want to enter a result but the chart popup is open.” The chart is for context, not result entry. Close it and use the Workbench tab — pick the order, type the value.
  • “I don’t see the order I’m expecting.” Filter on Status and Priority at the top of Orders. The default is “All statuses”. If a doctor only just placed it, the table refreshes when you re-enter the tab. Also check the Lab column — it may have been transferred to a different branch.
  • “I tried to open another ward to check the patient.” Correct — lab staff can’t browse ward boards. Use the chart popup to see the bedside context (active conditions, current meds, allergies, vitals).
  • “The order has no specimen type.” That’s an oversight by the ordering doctor. You can still receive the specimen; record the actual type on the Receiving form.
  • “My patient has 30 prior reports.” That’s expected on a long admission. Use the Results tab’s search to filter by report number, patient name, or display.
  • “I registered the same walk-in twice.” The orange match panel was probably ignored. The system shows up to 6 candidates — always click an existing record before creating a new one.
  • “The transfer is stuck in pending forever.” The receiving branch hasn’t acted. Open the Lab Transfers Outgoing sub-tab — if it’s been hours, ring the destination lab manually; transfer SLAs aren’t enforced by the system today.

Part B — Technical level

B.1 Identity & ward shell

A user is treated as a “Lab kiosk user” when all of these are true:

  • users.role is doctor or nurse.
  • users.department (case-insensitive, trimmed) is one of: lab, laboratory, clinical laboratory, pathology, clinical pathology, anatomic pathology, hematology, haematology, microbiology, clinical chemistry, biochemistry, blood bank, transfusion medicine, molecular diagnostics.

This list lives in AppRoot.WARD_CONFIG.lab.depts and matches the new entries in WARD_NAV in frontend/src/services/role-permissions.js.

Shell decision is generic — AppRoot.wardForUser(user) returns the ward key ('icu' / 'maternity' / 'emergency' / 'operations' / 'ward' / 'lab') or null. When non-null, app-root.js:

  1. Sets the post-login home to #/<wardRoute> (skips /dashboard).
  2. Bounces any #/dashboard hit back to the ward route.
  3. Renders renderWardShell(wardKey) — for lab, the slot hosts <lab-page> (the existing OpenELIS-style multi-tab workbench). The route is /lab.
  4. renderWardPage(wardKey) switches on the key and returns the right page element.

All six kiosks (ICU / Maternity / Emergency / Operations / Ward / Lab) now share the same generic shell.

B.2 Seed data

Six ward-doctor logins are created idempotently on every boot by ensureWardDoctors. For Lab:

  • email: lab.doctor@hospital.com
  • password: admin123
  • name: Dr. Samir Khalil
  • department: Laboratory
  • specialization (in doctors table): Clinical Pathology
  • role: doctor

Idempotency: users.email UNIQUEON CONFLICT (email) DO NOTHING for users; INSERT … WHERE NOT EXISTS for the matching doctors row.

Lab nurses are created the same way once a user record with role='nurse' and department='Laboratory' (or one of the discipline aliases) is added (no nurse seed today — add via /hr or /users).

B.3 RBAC

Backend (path-based middleware): the doctor and nurse role permission tables in backend/internal/middleware/roles.go now include lab for both roles. Doctors get full CRUD; nurses get GET/POST/PUT. They keep excluding radiology, pharmacy, pharmacy-dispense, general-inventory. They allow prescriptions, service-requests, diagnostic-reports, imaging-studies, observations, allergies, conditions, nursing-assessments, care-plans, consents, plus all six ward routes.

New resources for the lab kiosk:

ResourceDoctorNurseWhy
lab-branchesGET/POST/PUT/DELETEGET/POST/PUTBranch catalog (CRUD admin lives here so the lab director can edit from the kiosk).
lab-transfersGET/POST/PUT/DELETEGET/POST/PUTTransfer queue. Doctor / nurse parity, since either can hand off a sample.
lab-analyticsGETGETDashboard read-only.
patientsGET/POST/PUTGET/POST/PUT ⬅ changedLab nurses (typically lab techs whose users.role='nurse') now need POST/PUT to register a walk-in. The frontend WARD_NAV ensures only lab-department nurses see the lab kiosk in the first place.

Note: the backend allows lab for ANY doctor/nurse — the frontend WARD_NAV is what narrows non-lab doctors out of the lab nav. A non-lab doctor URL-hacking to /lab is technically allowed today; tightening to department-aware backend RBAC is on the follow-up list. The same caveat applies to patients POST: any nurse can technically POST a patient now, but the kiosk surface is only exposed to lab-department nurses.

Frontend: getNavigationForUser(user) in frontend/src/services/role-permissions.js narrows frontDesk to ['lab'] for any doctor/nurse whose department resolves to one of the lab disciplines. Fourteen department aliases are mapped (lab, laboratory, pathology, hematology, microbiology, clinical chemistry, biochemistry, blood bank, etc.). Blood bank / transfusion medicine departments additionally surface blood-bank in their clinical extras.

B.4 Lab page

frontend/src/pages/lab-page.js is the OpenELIS-style multi-tab parent. Nine workflow tabs (was seven):

  • Orders (lab-orders.js) — the queue of service_request rows where category=laboratory. Now shows the Lab column (current_lab_name) and carries a + Register Patient button (see §B.10).
  • Receiving (lab-receiving.js) — accession-number entry, specimen condition, rejection.
  • Workbench (lab-workbench.js) — per-order result entry against observations rows.
  • Verify (lab-verify.js) — second-tech sign-off (preliminary → verified).
  • Worksheets (lab-worksheets-view.js) — batch run of multiple results.
  • Lab Transfers (lab-transfers.js) — multi-lab handoff queue (Inbox / Outgoing / All sub-tabs). New.
  • Analytics (lab-analytics.js) — KPI dashboard. New.
  • Reports (lab-reports.js) — diagnostic_report aggregation + publication.
  • Catalog (lab-catalog.js) — test / panel catalog editor.

All patient-facing tabs (everything except Catalog, Transfers, Analytics) make the patient name clickable; clicks dispatch a window event picked up by the chart popup mounted at the page level.

B.5 Lab patient chart popup

frontend/src/components/lab-patient-chart.js is a sibling of the five ward charts. It’s mounted exactly once in lab-page.js — outside the tab-switch logic — so tab switches don’t re-mount it.

Cross-tab open mechanism: the chart’s connectedCallback() subscribes to a window event:

// In any lab sub-component, on patient name click:
window.dispatchEvent(new CustomEvent('lab-open-chart', {
  detail: { order }   // any record carrying patient_id + patient_name
}));

The chart’s handler calls this.show(order) which sets patientId / patientName / admissionId / contextOrder and fetches counts. This pattern avoids importing the chart in each sub-component.

Grid view (default after show() is called):

  • Header: name • MRN • gender • DOB · order_number · display. “All categories” button hidden.
  • Info bar: gender, DOB, blood type, phone, admission #, priority pill (colour-coded: STAT red / urgent orange / asap yellow / routine blue).
  • Body: 11 category cards. Each card has icon + label + hint + count badge.

Detail view (after a card click):

  • Lab Order renders as a single read-only snapshot card (test/panel, order #, code, priority, status, category, ordered date, specimen type, reason, notes). STAT orders get a red banner. A tip-banner reminds users that result entry happens on the Receiving / Workbench / Verify / Reports tabs.
  • The remaining 10 categories are identical to the five ward charts. The “Lab Orders” history tab highlights the current order with a “this order” badge.

B.6 Categories — list and create endpoints

CardList APIAdd APIEditable
Lab Orderpassed in via show(order)n/ano
VitalsGET /api/observations/patient/:id/vitalsPOST /api/observations category: vital-signsyes
AllergiesGET /api/allergies/patient/:idPOST /api/allergiesyes
ConditionsGET /api/conditions/patient/:idPOST /api/conditionsyes
Nursing Assess.GET /api/nursing-assessments/patient/:idPOST /api/nursing-assessmentsyes
MedicationsGET /api/prescriptions?patient_id=(placed by ordering doctor — read-only here)no
Lab OrdersGET /api/service-requests?patient_id= filter category ~ “lab”(placed by ordering doctor — read-only here)no
Imaging Orderssame, filter category ~ “rad|imag”(read-only)no
ResultsGET /api/diagnostic-reports?patient_id=(published from the Reports tab)no
Care PlansGET /api/care-plans?patient_id=(read-only)no
ConsentsGET /api/consents?patient_id=(read-only)no

For each editable category the chart’s submitAdd(e) method assembles the FHIR-shaped payload and POSTs it. On success it re-fetches the active category and re-runs loadCounts().

B.7 Vitals quick chips

Vitals add form has the standard seven chips (no lab-specific extras):

Chipcodedisplayunit
BP85354-9Blood PressuremmHg
HR8867-4Heart Ratebpm
Temp8310-5Body Temperature°C
SpO259408-5Oxygen Saturation%
RR9279-1Respiratory Rate/min
Wt29463-7Body Weightkg
Pain38208-5Pain Severity0-10

Click chip → code, display, unit pre-fill → user only types the value.

The Nursing Assess. dropdown is tuned for lab work: Phlebotomy difficulty, Bleeding risk, Pain (during draw), Other.

B.8 Where result entry actually happens

The chart popup is not where the lab clinician records a result — that flow lives in the dedicated tabs:

TabEndpointWhat it does
ReceivingPOST /api/lab-specimensAccessions a specimen; flips service_request.status to received.
WorkbenchPOST /api/observations (per result)Creates observations rows against the order, status preliminary.
VerifyPUT /api/observations/:id/verifyFlips preliminary → verified.
ReportsPOST /api/diagnostic-reportsAggregates verified observations into a diagnostic_report, status final.

The chart’s purpose is purely contextual — read patient state to interpret values — and to capture the rare bedside-style note that surfaces during specimen handling (an unrecorded latex allergy noticed during phlebotomy, etc.).

B.9 Lab-branch topology (schema)

Two new tables plus two columns on service_requests. Migrations in backend/internal/database/database.go; models in backend/internal/models/models.go.

-- Multi-lab topology: each row is a physical lab.
CREATE TABLE lab_branches (
  id BIGSERIAL PRIMARY KEY,
  code TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  location TEXT, phone TEXT, email TEXT, manager TEXT,
  specialty TEXT,                     -- general / microbiology / transfusion / molecular / …
  active BOOLEAN DEFAULT TRUE,
  notes TEXT,
  created_at TIMESTAMP, updated_at TIMESTAMP
);

-- Inter-lab handoff with full audit trail.
CREATE TABLE lab_transfers (
  id BIGSERIAL PRIMARY KEY,
  service_request_id INTEGER NOT NULL,
  specimen_id INTEGER,
  from_lab_id INTEGER NOT NULL,
  to_lab_id INTEGER NOT NULL,
  reason TEXT,
  priority TEXT DEFAULT 'routine',
  status TEXT NOT NULL DEFAULT 'pending',  -- pending | accepted | rejected | completed
  requested_by INTEGER, requested_by_name TEXT,
  decided_by INTEGER, decided_by_name TEXT,
  decided_at TIMESTAMP,
  decision_notes TEXT,
  notes TEXT,
  created_at TIMESTAMP
);

-- Routing pointers on the service request.
ALTER TABLE service_requests ADD COLUMN originating_lab_id INTEGER;  -- where placed
ALTER TABLE service_requests ADD COLUMN current_lab_id INTEGER;      -- where it lives now

Indexes: idx_lab_transfers_to(to_lab_id, status), idx_lab_transfers_from(from_lab_id, status), idx_lab_transfers_order(service_request_id, created_at DESC), idx_service_requests_current_lab(current_lab_id, category, status).

Seed function ensureLabBranchesSeed:

  1. Idempotent insert of three branches if lab_branches is empty: MAIN (Main Laboratory, general), MICRO (Microbiology Lab), BLOOD (Blood Bank, transfusion).
  2. Back-fill: any service_requests.category='laboratory' row with NULL current_lab_id gets pinned to the MAIN branch so the new transfer queue starts from a coherent state on a system that already has lab orders.

service_requests query update: service-requests.go introduces a serviceRequestSelect constant + scanServiceRequest helper. List/Get/PatientList queries now LEFT JOIN patients, lab_branches ob ON sr.originating_lab_id, lab_branches cb ON sr.current_lab_id so the response carries originating_lab_name and current_lab_name without follow-up requests.

CreateServiceRequest default routing: if the caller omits originating_lab_id for a category=laboratory order, the handler looks up the MAIN branch and uses it. current_lab_id defaults to the originating lab. This means existing kiosks (the ward Quick-Order widgets) keep working unchanged — orders just land on the Main Lab queue.

B.10 Lab Transfers (backend)

Routes in backend/cmd/server/main.go under the protected group:

MethodPathHandlerWhat it does
GET/api/lab-branchesListLabBranchesReturns each branch with derived open_order_count + pending_transfers.
GET/api/lab-branches/{id}GetLabBranch
POST/api/lab-branchesCreateLabBranchcode + name required.
PUT/api/lab-branches/{id}UpdateLabBranch
DELETE/api/lab-branches/{id}DeleteLabBranchSoft-archive (active=false) if any orders or transfers reference the branch; hard delete otherwise.
GET/api/lab-transfersListLabTransfersFilters: from_lab_id, to_lab_id, status, service_request_id.
POST/api/lab-transfersCreateLabTransferSets from_lab_id from the order’s current_lab_id if omitted; refuses if from == to. Notifies receiving lab.
POST/api/lab-transfers/{id}/acceptAcceptLabTransferFlips status pending → accepted and updates service_requests.current_lab_id. Idempotent: HTTP 409 if not pending.
POST/api/lab-transfers/{id}/rejectRejectLabTransferFlips status pending → rejected. Notifies sender at high priority.
POST/api/lab-transfers/{id}/completeCompleteLabTransferFlips status to completed once result returned.
DELETE/api/lab-transfers/{id}DeleteLabTransferOnly allowed while pending (HTTP 409 otherwise — decided rows are audit).

Notifications: lab_transfers.go uses notifyRole("nurse", …) and notifyRole("doctor", …) so any user with role nurse/doctor at the receiving lab gets a bell. STAT/urgent priority bumps to notify.PriorityHigh. Event types: lab_transfer_in, lab_transfer_accepted, lab_transfer_rejected. Action URL is #/lab so the bell deep-links to the kiosk.

Models in models.go: LabBranch (with derived counters), LabTransfer (with joined order + branch names).

B.11 Lab Analytics (backend)

Single endpoint, single handler: GET /api/lab-analytics?window_days=30lab_analytics.go. Window clamped to [1, 365], defaults 30.

Queries (all over service_requests where category='laboratory' AND created_at::DATE >= cutoff):

FieldQuery
total_ordersSELECT COUNT(*)
pending_ordersWHERE status IN ('draft','active','on-hold','pending')
completed_ordersWHERE status='completed'
cancelled_ordersWHERE status IN ('revoked','cancelled')
stat_ordersWHERE priority IN ('stat','urgent','asap')
top_referring_doctorsGROUP BY requester ORDER BY COUNT(*) DESC LIMIT 10
top_ordered_testsGROUP BY code ORDER BY COUNT(*) DESC LIMIT 10 (display via MAX(display))
usage_by_labLEFT JOIN service_requests sr ON sr.current_lab_id = b.id per-branch GROUP BY
orders_by_dayGROUP BY created_at::DATE ORDER BY day ASC (clamped to 14 days)
transfers_in/out/pendinglab_transfers count over the window plus a global status='pending'

Returned struct: LabAnalyticsDashboard plus four sub-types (LabDoctorStat, LabTestStat, LabBranchUsageStat, LabDailyStat) defined in models.go. All array fields are non-nil even when empty so the frontend doesn’t have to null-check.

Frontend (lab-analytics.js): one api.getLabAnalytics({ window_days }) call on mount; CSS percentage bars (no chart library), a sparkline rendered as flex children with height set to a per-bar percentage of the max. The window selector triggers a single re-fetch.

B.12 Walk-in patient registration

The + Register Patient flow is implemented entirely in lab-orders.js — no separate handler since reusing the existing POST /api/patients.

Three new properties on <lab-orders>: showRegisterModal, registerSearch, registerMatches. The search box fires a debounced (350ms) api.searchPatients(q) call; the first 6 results render in an orange “Possible matches” panel. Clicking a match closes the register modal and opens the order modal with formPatientId pre-set.

splitFullName(full): same single-field-name UX pattern used on the patients page. Splits a trimmed full name into first + remainder so the existing patients.first_name / last_name schema doesn’t need to change.

submitRegister(e): assembles a Patient payload (first/last from split, dob, gender, phone, national_id, email, address, clinical_status: 'stable', status: 'active'), POSTs to /api/patients, and on success refreshes this.patients and chains into openCreate() with formPatientId set to the new ID. The user lands directly in the order-placement modal — no extra navigation, no MRN re-typing.

RBAC: see §B.3 — RoleNurse was extended to ["GET", "POST", "PUT"] on patients so lab nurses can create. Doctors already had POST.

B.13 Build / verification

  • Backend: go build ./... — clean.
  • Frontend: npx vite build — 1.86 MB / 313 kB gzip in ~3s.

B.14 Known follow-ups (not done yet)

  • Critical-value back-alerts. When a verified result is flagged critical (e.g. K+ < 2.5 or > 6.5, glucose < 50, hemoglobin < 7), fire a high-priority notification to the ordering doctor and the patient’s current ward. Half-done — the order-route notification is in place; the result-route is not.
  • Department-aware backend RBAC. Today any doctor/nurse can GET /api/lab because backend permission is role-based; only the kiosk shell narrows this client-side. Ideally the lab routes check user.department ∈ lab disciplines server-side too. Same caveat for patients POST after the lab-walkin change.
  • Lab nurse seed user. No nurse is seeded — only the lab doctor. Add a lab.nurse@hospital.com for parity with how the other ward guides handle staffing.
  • Per-order timeline view. A small “from order → received → workbench → verified → published” stepper on the Lab Order card would make the chart even more useful for follow-up calls from the ordering doctor.
  • Six chart components share ~85% of their code. Refactor opportunity: extract a single ward-patient-chart component parameterised by a ward descriptor (snapshot card renderer, vital-chip preset, nursing-assess types). Today the six files duplicate ~3,000 lines.
  • Transfer SLA / escalation. A pending transfer has no deadline today. A tick-loop check (“any pending transfer older than X minutes for STAT / Y for routine fires an escalation alert to the receiving lab”) would close the operational gap.
  • Specimen-aware transfer. Today a transfer carries a specimen_id column but the UI doesn’t surface it. When transferring a microbiology culture from Main to Micro, recording which tube physically moved would help track chain of custody.
  • Branch-aware result return. When Microbiology completes a transferred order, the result lands under the current_lab_id (Microbiology). The ordering doctor sees it on their Results feed regardless, but a “Result returned from Microbiology” annotation on the report would be clearer.
  • Analytics export. The Analytics tab is read-only. CSV / PDF export of the leaderboards for the lab director’s monthly meeting is on the wishlist.
  • Per-doctor / per-test drilldown. Clicking a row on a leaderboard should open a filtered list of orders behind the count. Today it’s a static bar.
  • Branch admin UI. lab_branches CRUD endpoints exist but there’s no kiosk page to edit them — today you INSERT directly or use a tool. A small admin panel inside the lab kiosk is a one-modal job.

Last updated: 2026-04-29