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
| Responsibility | Detail |
|---|---|
| Lab workbench | The single screen they sign in to: nine workflow tabs — Orders, Receiving, Workbench, Verify, Worksheets, Lab Transfers, Analytics, Reports, Catalog. |
| Order intake | Orders 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 receipt | When a tube arrives, Receiving records the accession number, specimen type, condition, and rejection reason if any. |
| Result entry | Workbench 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. |
| Worksheets | A batch view that lets the bench run multiple specimens in one go and sweep results back onto orders. |
| Lab-to-Lab transfers | When 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 analytics | The 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 registration | Lab 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 reporting | Reports publishes the verified results as DiagnosticReport resources — visible to the ordering doctor under Results on their patient chart. |
| Catalog | The test/panel catalog itself (codes, reference ranges, units, specimen requirements) — usually edited by the lab director. |
| Patient context | Click 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 observations | The 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
- Sign in. The system bounces them straight to
/lab. No dashboard. - Watch the queue. Orders is the default tab. Each row shows the order #, patient name, test/panel, lab branch, priority, status, and date.
- 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.
- 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.
- 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.
- 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.
- Click “Conditions” / “Medications” to inform interpretation — e.g. an elevated glucose is expected on a patient with active diabetes who’s on insulin.
- Add a vital / allergy / condition / nursing note if the chart turned up something the bedside team missed.
- 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
DiagnosticReportand published.
- 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.
- 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.
- 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:
- From Orders, click + Register Patient.
- 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.
- 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.
- 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-tab | What it shows |
|---|---|
| Inbox | Transfers other labs sent to me, status pending. The badge on the tab is the count. Each row has Accept / Reject buttons. |
| Outgoing | Transfers I sent. Read-only — I can see whether they were accepted, rejected, or completed. |
| All activity | Inbox + Outgoing combined, sorted newest first. The full audit trail. |
How the workflow walks:
- 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.
- 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. - The destination tech opens Lab Transfers, sees the row in their Inbox, and either:
- Accept — status flips to
accepted, the order’scurrent_lab_idis 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
rejectedwith a required note (why). The order stays at the sending lab. The sender is notified at high priority.
- Accept — status flips to
- 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
pendingtransfers can be Accepted or Rejected (HTTP 409 otherwise). - Only
pendingtransfers 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_newon the Laboratory department feed. - STAT priority orders fire at high priority; routine orders fire normal.
- New transfer in —
lab_transfer_infires when another lab routes work to this branch. STAT/urgent priority bumps to high. - Transfer accepted —
lab_transfer_acceptedfires back to the sender. - Transfer rejected —
lab_transfer_rejectedfires 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.roleisdoctorornurse.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:
- Sets the post-login home to
#/<wardRoute>(skips/dashboard). - Bounces any
#/dashboardhit back to the ward route. - Renders
renderWardShell(wardKey)— for lab, the slot hosts<lab-page>(the existing OpenELIS-style multi-tab workbench). The route is/lab. 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
doctorstable):Clinical Pathology - role:
doctor
Idempotency: users.email UNIQUE → ON 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:
| Resource | Doctor | Nurse | Why |
|---|---|---|---|
lab-branches | GET/POST/PUT/DELETE | GET/POST/PUT | Branch catalog (CRUD admin lives here so the lab director can edit from the kiosk). |
lab-transfers | GET/POST/PUT/DELETE | GET/POST/PUT | Transfer queue. Doctor / nurse parity, since either can hand off a sample. |
lab-analytics | GET | GET | Dashboard read-only. |
patients | GET/POST/PUT | GET/POST/PUT ⬅ changed | Lab 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_requestrows wherecategory=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
observationsrows. - 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_reportaggregation + 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
| Card | List API | Add API | Editable |
|---|---|---|---|
| Lab Order | passed in via show(order) | n/a | no |
| Vitals | GET /api/observations/patient/:id/vitals | POST /api/observations category: vital-signs | yes |
| Allergies | GET /api/allergies/patient/:id | POST /api/allergies | yes |
| Conditions | GET /api/conditions/patient/:id | POST /api/conditions | yes |
| Nursing Assess. | GET /api/nursing-assessments/patient/:id | POST /api/nursing-assessments | yes |
| Medications | GET /api/prescriptions?patient_id= | (placed by ordering doctor — read-only here) | no |
| Lab Orders | GET /api/service-requests?patient_id= filter category ~ “lab” | (placed by ordering doctor — read-only here) | no |
| Imaging Orders | same, filter category ~ “rad|imag” | (read-only) | no |
| Results | GET /api/diagnostic-reports?patient_id= | (published from the Reports tab) | no |
| Care Plans | GET /api/care-plans?patient_id= | (read-only) | no |
| Consents | GET /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):
| Chip | code | display | unit |
|---|---|---|---|
| BP | 85354-9 | Blood Pressure | mmHg |
| HR | 8867-4 | Heart Rate | bpm |
| Temp | 8310-5 | Body Temperature | °C |
| SpO2 | 59408-5 | Oxygen Saturation | % |
| RR | 9279-1 | Respiratory Rate | /min |
| Wt | 29463-7 | Body Weight | kg |
| Pain | 38208-5 | Pain Severity | 0-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:
| Tab | Endpoint | What it does |
|---|---|---|
| Receiving | POST /api/lab-specimens | Accessions a specimen; flips service_request.status to received. |
| Workbench | POST /api/observations (per result) | Creates observations rows against the order, status preliminary. |
| Verify | PUT /api/observations/:id/verify | Flips preliminary → verified. |
| Reports | POST /api/diagnostic-reports | Aggregates 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:
- Idempotent insert of three branches if
lab_branchesis empty:MAIN(Main Laboratory, general),MICRO(Microbiology Lab),BLOOD(Blood Bank, transfusion). - Back-fill: any
service_requests.category='laboratory'row with NULLcurrent_lab_idgets 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:
| Method | Path | Handler | What it does |
|---|---|---|---|
| GET | /api/lab-branches | ListLabBranches | Returns each branch with derived open_order_count + pending_transfers. |
| GET | /api/lab-branches/{id} | GetLabBranch | |
| POST | /api/lab-branches | CreateLabBranch | code + name required. |
| PUT | /api/lab-branches/{id} | UpdateLabBranch | |
| DELETE | /api/lab-branches/{id} | DeleteLabBranch | Soft-archive (active=false) if any orders or transfers reference the branch; hard delete otherwise. |
| GET | /api/lab-transfers | ListLabTransfers | Filters: from_lab_id, to_lab_id, status, service_request_id. |
| POST | /api/lab-transfers | CreateLabTransfer | Sets from_lab_id from the order’s current_lab_id if omitted; refuses if from == to. Notifies receiving lab. |
| POST | /api/lab-transfers/{id}/accept | AcceptLabTransfer | Flips status pending → accepted and updates service_requests.current_lab_id. Idempotent: HTTP 409 if not pending. |
| POST | /api/lab-transfers/{id}/reject | RejectLabTransfer | Flips status pending → rejected. Notifies sender at high priority. |
| POST | /api/lab-transfers/{id}/complete | CompleteLabTransfer | Flips status to completed once result returned. |
| DELETE | /api/lab-transfers/{id} | DeleteLabTransfer | Only 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=30 →
lab_analytics.go. Window clamped to [1, 365], defaults 30.
Queries (all over service_requests where category='laboratory' AND created_at::DATE >= cutoff):
| Field | Query |
|---|---|
total_orders | SELECT COUNT(*) |
pending_orders | WHERE status IN ('draft','active','on-hold','pending') |
completed_orders | WHERE status='completed' |
cancelled_orders | WHERE status IN ('revoked','cancelled') |
stat_orders | WHERE priority IN ('stat','urgent','asap') |
top_referring_doctors | GROUP BY requester ORDER BY COUNT(*) DESC LIMIT 10 |
top_ordered_tests | GROUP BY code ORDER BY COUNT(*) DESC LIMIT 10 (display via MAX(display)) |
usage_by_lab | LEFT JOIN service_requests sr ON sr.current_lab_id = b.id per-branch GROUP BY |
orders_by_day | GROUP BY created_at::DATE ORDER BY day ASC (clamped to 14 days) |
transfers_in/out/pending | lab_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/labbecause backend permission is role-based; only the kiosk shell narrows this client-side. Ideally the lab routes checkuser.department ∈ lab disciplinesserver-side too. Same caveat forpatientsPOST after the lab-walkin change. - Lab nurse seed user. No nurse is seeded — only the lab doctor. Add a
lab.nurse@hospital.comfor 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-chartcomponent 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_idcolumn 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_branchesCRUD endpoints exist but there’s no kiosk page to edit them — today youINSERTdirectly or use a tool. A small admin panel inside the lab kiosk is a one-modal job.
Last updated: 2026-04-29