Nexus Docs Documentation for the Nexus hospital management system

Guides

Pharmacy

Prescriptions, drug-safety, dispensing, counselling, and multi-pharmacy stock — the seventh kiosk.

Two parts:

  • Part A — Business level. What pharmacy staff own day-to-day, what the kiosk looks like when they sign in, and how the day flows.
  • Part B — Technical level. Tables, handlers, RBAC, API surface, and component structure.

Companion docs: docs/LAB_GUIDE.md, docs/MATERNITY_GUIDE.md, docs/HR_GUIDE.md, docs/FINANCE_GUIDE.md, docs/EMERGENCY_GUIDE.md.

This is the seventh kiosk surface. It pins pharmacists to /pharmacy and surfaces the full medication lifecycle: prescriptions → drug-safety → dispensing → counselling → refills, plus multi-pharmacy stock movement and analytics.


Part A — Business level

A.1 What a pharmacist owns

ResponsibilityDetail
Pharmacy workbenchA single screen with six tabs — Prescriptions, Inventory, Transfers, Interactions, Counselling, Analytics.
Prescription queueEvery Rx placed by any doctor (clinic, ER, ward, OR, outpatient) lands on the Prescriptions tab the moment it’s created. STAT / urgent items bubble up via the bell.
Drug-safety checksThe system runs a drug-allergy check and a drug-drug interaction check at the moment a prescriber tries to create an Rx. Major / contraindicated pairs and any active-allergy match block the Rx unless an override is recorded in the audit log. Moderate / minor interactions pass through but are persisted as an active medication alert on the patient.
DispensingPer-item Dispense button on the Rx detail. Stock decrements at the prescription’s branch (not “any branch with the drug”). Out-of-stock is surfaced inline; the rest of the items can still go.
RefillsPrescriptions can carry an authorised refill count (0–11). When ≥1 refill remains, an Issue refill button appears on the Rx detail. Each refill decrements the remaining count, deducts stock, and writes a row in the refill log.
Inventory & drug masterPer-medication card with stock, reorder level, branch, controlled-substance flag + narcotic schedule, formulary status, refrigeration, barcode / batch / lot / storage location. Cards filter by branch and by status.
Stock transfersMulti-pharmacy hospitals route inventory between Main / ER / OPD / satellite branches via the Transfers tab. The destination Accepts (stock moves) or Rejects (no movement).
Drug-drug interactionsThe Interactions tab is the master editor for the rule set the safety check runs on. Pharmacy directors can extend or override the seeded list.
Counselling logPharmacist-patient education events go into the Counselling tab — topic (dosing / side-effects / adherence / lifestyle / drug-food / contraception / missed-dose / other), notes, and an optional follow-up date.
Walk-in patient registrationOutpatient or walk-in patients can be registered directly from the kiosk’s + Patient button — no need to bounce them to reception.
Operational analyticsThe Analytics tab is the head pharmacist’s daily readout: total / pending / dispensed counts, top-prescribed meds, top prescribers, per-branch usage, low-stock + expiring + active-alert counters, 14-day Rx volume sparkline.

A.2 What they cannot do

  • No ward boards (ICU / ER / Maternity / Operations / Ward / Lab / Radiology) — pharmacist is not a clinical-write role.
  • No HR, finance, cashier surfaces.
  • They CAN now register walk-in patients and create prescriptions, but they cannot admit, transfer between wards, or order labs / imaging.

A.3 What the screen looks like

┌────────────────────────────────────────────────────────────────────────────┐
│ M  Mando · Pharmacy        🔔   Pharmacist Omar Khaled ┄                    │
├────────────────────────────────────────────────────────────────────────────┤
│  💊 Pharmacy                                                                │
│     N pending prescriptions · M low stock alerts                            │
│                                       [+ Patient] [+ Med] [+ Prescription]  │
│                                                                             │
│  [Prescriptions] Inventory  Transfers  Interactions  Counselling  Analytics │
│                                                                             │
│  Quick Navigation                                                           │
│  [Patients] [Dispense] [Medications] [Doctors] [Emergency] [Operations] …   │
│                                                                             │
│  [search…] [All / pending / partially / dispensed]                          │
│  [All sources / emergency / operation / ward / clinic / outpatient]         │
│                                                                             │
│  Patient        Doctor         Date         Status         Source · Items   │
│  Sarah Hassan   Dr. Ahmad      2026-04-29   pending        clinic · 3       │
│  John Smith     Dr. Chen       2026-04-29   partially      ward · 5         │
│  …                                                                          │
└────────────────────────────────────────────────────────────────────────────┘

A.4 The day, end to end

  1. Sign in. The system bounces Pharmacist + department=Pharmacy users to /pharmacy. Other roles never see this kiosk.
  2. Watch the queue. Prescriptions is the default tab. Filter by status / source. Search by patient or doctor.
  3. Walk-in patient? Click + Patient in the banner. The modal runs a debounced search before letting you create — orange “Possible matches” panel surfaces existing records so you don’t double-MRN. After create, the system jumps you straight into a New Prescription with that patient pre-selected.
  4. Click a row to open the Rx detail. Patient / doctor / source / pharmacy branch / refills / diagnosis are all visible at a glance, plus a per-item table.
  5. Dispense. Click Dispense on any pending item. Stock is deducted at the prescription’s branch. If the branch is out of stock, the item flips to out-of-stock and the Dispense button gone — but other items on the same Rx are unaffected.
  6. Issue a refill. When the prescriber authorised refills, an Issue refill button appears in the Rx footer once at least one item is dispensed. One click writes a refill row, decrements refills_remaining, and re-deducts stock per item.
  7. Stock transfer. ER pharmacy is short on a drug? Open Transfers, pick the source branch on the picker, hit + Transfer Stock, choose the medication / destination / quantity / priority. The destination’s Inbox lights up; they Accept (stock moves) or Reject (no movement).
  8. Drug-drug interaction tuning. The seeded master ships with ~15 high-impact pairs (warfarin + aspirin, simvastatin + clarithromycin, sildenafil + nitrate, etc.). Pharmacy directors edit / disable / extend the list from the Interactions tab.
  9. Counselling. After dispensing a new chronic med (or any time the patient asks), open Counselling, log the session — topic, notes, optional follow-up date.
  10. End of shift. Open Analytics for the daily readout: pending Rx count, top prescribed drugs, top prescribers, per-branch volume, low-stock / out-of-stock / expiring (60d) / active-alert counters.

A.5 Walk-in patient registration

Outpatient pharmacy and ER pharmacy frequently get patients who aren’t on the system yet. Pharmacy staff register them directly:

  1. From any tab, click + Patient in the banner.
  2. Type 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 New Prescription modal opens with that patient pre-selected.
  4. If no match: fill the create form (full name required, plus DOB / gender / phone / national ID / email / address) and click Register & New Prescription. The system creates the patient, mints an MRN, refreshes the patient list, and jumps straight into the prescription modal with the new patient pre-selected.

Why search-first: a walk-in pharmacy visit is the most common path to accidentally creating a parallel record (no admission, no insurance lookup, no demographics double-check). The orange panel is the single guardrail.

A.6 Drug-safety workflow

Two checks run on every POST /api/prescriptions:

  1. Drug-allergy. Substring match (case-insensitive, bidirectional) between each prescribed item and the patient’s active allergens. Any match returns HTTP 409 with the conflict list.
  2. Drug-drug interaction. All pairwise combinations of (new items × new items) and (new items × patient’s active prescription items) checked against drug_interactions. Severity buckets:
    • Contraindicated / Major: HTTP 409 — Rx blocked. The kiosk surfaces a “Proceed anyway? (override is audited)” confirm. If the prescriber confirms, the kiosk retries with ?override=true and the override is recorded.
    • Moderate / Minor: Rx is created, but each hit is persisted as an active row in medication_alerts. The pharmacist sees them on the alert panel and can acknowledge.

Override is the explicit path — there’s no silent bypass.

A.7 Multi-pharmacy stock transfers

Three branches seed on a fresh boot: MAIN (Main Pharmacy), ER (ER Pharmacy), OPD (Outpatient Pharmacy). Add more from the API or extend the seed.

Branch picker (top of Transfers tab) is the “I am working at this pharmacy” selector. Defaults to the first active branch and persists in localStorage.

Sub-tabWhat it shows
InboxTransfers other branches sent to me, status pending. Accept / Reject buttons.
OutgoingTransfers I sent. Read-only — see whether they were accepted or rejected.
All activityInbox + Outgoing combined, sorted newest first.

The flow:

  1. Tech at Source clicks + Transfer Stock → picks medication, destination, quantity, priority, reason. Submit.
  2. Server verifies source actually has the stock. Refused if not.
  3. Transfer is queued pending. Receiving branch gets a notification.
  4. Destination Accepts → server re-checks stock at decision time (Create’s check could be stale), then decrements source and increments destination (cloning the source row’s master fields if the destination didn’t stock the drug yet).
  5. Destination Rejects → no stock movement. Sender notified at high priority.

Common pitfalls:

  • “My transfer was accepted but stock didn’t move.” Check the destination branch’s inventory — the row is created/incremented at to_branch_id, not at the source.
  • “Stock check failed at Accept time.” Someone dispensed against the source while your transfer was pending. Send a smaller transfer or wait for restock.

A.8 Counselling log

Each row is one pharmacist-patient education event. Topic taxonomy is a soft enum:

TopicUse
dosing“Take with food”, “every 6 hours”, “tapering schedule”
side_effectsWhat to expect / when to call back
adherenceWhy finishing the course matters / pillbox tips
lifestyleAlcohol / driving / breastfeeding caveats
drug_foodGrapefruit / tyramine / dairy interactions
contraceptionCycle gaps, alternative method during therapy
missed_doseCatch-up rules
otherAnything not above

Follow-up: tick the box + pick a date when the patient should come back. The Counselling tab has a “Follow-up needed only” filter so the pharmacy lead can scan the daily callback list.

A.9 Notifications

Inbound (the bell flashes):

  • New prescription placed by any doctor — fires rx_new.
  • Pharmacy stock transfer in (other branch routed work to this branch) — fires pharmacy_transfer_in. STAT/urgent bumps to high priority.
  • Pharmacy stock transfer accepted/rejected — fires back to the sender.

Outbound (the pharmacy fires automatically):

  • Drug-allergy override — recorded in audit log via the audit middleware.
  • Drug-interaction alert — written to medication_alerts for moderate/minor; blocked + audited for major/contraindicated.

A.10 Common pitfalls

  • “The dispense said out-of-stock but I have plenty.” Dispense pulls from the prescription’s branch, not “anywhere with stock”. Either transfer some stock to the prescription’s branch or change the prescription’s branch on the Rx form.
  • “The interaction alert keeps firing on a combination my service uses safely.” Open Interactions, find the rule, set Active=false (or change severity to minor). The seeded list is a starting point, not gospel.
  • “I issued a refill but the patient says they didn’t get one item.” Refills do per-item stock checks but don’t block when an item is out-of-stock — they continue with the rest. Open the Rx; out-of-stock items will be visible.
  • "+ Patient registered the same person 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.
  • “Transfer disappeared after acceptance.” Decided rows don’t show in the Inbox filter (which is status=pending). Switch to All activity.

Part B — Technical level

B.1 Identity & kiosk shell

A user lands on /pharmacy when:

  • users.role = 'pharmacist', or
  • users.role is doctor or nurse AND users.department resolves to a pharmacy discipline via WARD_NAV in role-permissions.js.

The page is <pharmacy-page> mounted in frontend/src/pages/pharmacy-page.js. Theming: purple gradient banner (#7c3aed → #5b21b6), uses CSS custom properties. Sub-tab components live in frontend/src/components/pharmacy/.

B.2 Schema additions

All in backend/internal/database/database.go.

New tables:

TablePurpose
pharmacy_branchesMulti-pharmacy topology (Main / ER / OPD / satellite). Seeded with three rows on a fresh boot.
pharmacy_transfersInter-branch stock movement audit. status walks pending → accepted → completed, or pending → rejected. Stock movement happens at Accept time, not Create time.
drug_interactionsDrug-drug interaction master. Seeded with ~15 high-impact pairs. Severity ∈ {minor, moderate, major, contraindicated}.
pharmacy_counsellingPharmacist-patient counselling log; optionally tied to a prescription. Topic enum is soft.
prescription_refillsRefill events against an originating Rx. One row per refill.

ALTERs on existing tables:

TableColumns added
pharmacy_inventorybranch_id, controlled_substance, narcotic_schedule, formulary_status, requires_refrigeration, barcode, batch_number, lot_number, storage_location
prescriptionsrefills_allowed, refills_remaining, branch_id
prescription_itemsdispensed_qty (tracks partial fills)

Seed functions:

  • ensurePharmacyBranchesSeed — inserts Main / ER / OPD branches if pharmacy_branches is empty; back-fills pharmacy_inventory.branch_id = MAIN.id for any rows without a branch so the new Transfers tab starts coherent.
  • ensureDrugInteractionsSeed — populates the most clinically-significant DDIs (warfarin/aspirin, simvastatin/clarithromycin, sildenafil/nitrate, sertraline/tramadol, methotrexate/trimethoprim, etc.). Real deployments swap this for an external feed.

B.3 Models

All in backend/internal/models/models.go:

  • Prescription — extended with RefillsAllowed, RefillsRemaining, BranchID, BranchName.
  • PrescriptionItem — extended with DispensedQty.
  • PharmacyInventory — extended with BranchID, BranchName, ControlledSubstance, NarcoticSchedule, FormularyStatus, RequiresRefrigeration, Barcode, BatchNumber, LotNumber, StorageLocation.
  • PharmacyBranch — id, code, name, location, phone, email, manager, pharmacy_type (central/satellite/outpatient), active, plus derived StockItems, PendingTransfers.
  • PharmacyTransfer — id, medication_name, batch_number, from/to branch, quantity, reason, priority, status, decided-by/at metadata, plus joined FromBranchName, ToBranchName.
  • DrugInteraction — drug_a, drug_b, severity, mechanism, clinical_effect, management, source, active.
  • PharmacyCounselling — patient_id, prescription_id, pharmacist_id/name, counselling_date, topic, notes, follow_up_needed/date.
  • PrescriptionRefill — prescription_id, refill_number, refill_date, dispensed_by/name, notes, status.
  • PharmacyAnalyticsDashboard — KPI struct + four sub-types (PharmacyMedStat, PharmacyDoctorStat, PharmacyBranchStat, PharmacyDailyStat).

B.4 Handlers

FileWhat it owns
pharmacy.goExisting prescriptions + inventory CRUD. Updated: CreatePrescription now also runs findInteractionConflicts; DispenseItem uses prescription’s branch when looking up stock; ListInventory takes branch_id + status query params and joins branch name.
pharmacy_branches.goCRUD for branches. Soft-archive on delete if any inventory or transfers reference the branch.
pharmacy_transfers.goList / Create / Accept / Reject / Delete. Stock movement happens in AcceptPharmacyTransfer; the DB row is created pending first so a rejected transfer leaves the source unchanged.
drug_interactions.goCRUD + the findInteractionConflicts(patientID, items) helper consumed by CreatePrescription. Match is bidirectional case-insensitive substring so brand vs. generic names still trigger. Patient’s currently-active prescriptions are checked too, not just new-vs-new pairs.
pharmacy_counselling.goCRUD with patient / topic / follow-up filters.
pharmacy_refills.goList + Create. Create decrements prescriptions.refills_remaining, deducts stock per item from the prescription’s branch (skipping out-of-stock items rather than blocking the whole refill), and writes one row in prescription_refills.
pharmacy_analytics.goSingle GET endpoint with ?window_days=N. Returns PharmacyAnalyticsDashboard with all aggregates.

B.5 Drug-safety check (interaction logic)

findInteractionConflicts(patientID, items []PrescriptionItem) in drug_interactions.go:

  1. Pulls drug_interactions rows where active=true once (avoids per-pair queries).
  2. Pulls patient’s active medications (prescriptions with status in {pending, partially-dispensed, dispensed}) once.
  3. For each pair: (item × item) and (item × active medication), substring-matches each side of the master row in either direction. A row keyed ("warfarin", "aspirin") will fire on ("Warfarin Sodium 5mg", "Aspirin 81mg") and vice versa.

CreatePrescription consumes this:

if hits := findInteractionConflicts(req.PatientID, req.Items); len(hits) > 0 {
    var blocking []InteractionConflict
    for _, h := range hits {
        if h.Severity == "major" || h.Severity == "contraindicated" {
            blocking = append(blocking, h)
        }
    }
    if len(blocking) > 0 {
        // 409 with override hint — frontend offers "Proceed anyway?"
        return
    }
    // Persist non-blocking hits as medication_alerts.
    for _, h := range hits { /* INSERT INTO medication_alerts ... */ }
}

The override path (?override=true) is the same flag the existing drug-allergy check uses — both are recorded by the audit middleware.

B.6 Routes

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

MethodPathHandler
GET/api/pharmacy-branchesListPharmacyBranches
POST/api/pharmacy-branchesCreatePharmacyBranch
PUT/api/pharmacy-branches/{id}UpdatePharmacyBranch
DELETE/api/pharmacy-branches/{id}DeletePharmacyBranch
GET/api/pharmacy-transfersListPharmacyTransfers (filters: from/to branch, status)
POST/api/pharmacy-transfersCreatePharmacyTransfer
POST/api/pharmacy-transfers/{id}/acceptAcceptPharmacyTransfer
POST/api/pharmacy-transfers/{id}/rejectRejectPharmacyTransfer
DELETE/api/pharmacy-transfers/{id}DeletePharmacyTransfer
GET / POST / PUT / DELETE/api/drug-interactions[/{id}]Drug Interactions CRUD (filters: severity, drug)
GET / POST / PUT / DELETE/api/pharmacy-counselling[/{id}]PharmacyCounselling CRUD (filters: patient_id, topic, follow_up)
GET/api/prescriptions/{id}/refillsListPrescriptionRefills
POST/api/prescriptions/{id}/refillsCreatePrescriptionRefill
DELETE/api/prescription-refills/{id}DeletePrescriptionRefill
GET/api/pharmacy-analytics?window_days=NGetPharmacyAnalyticsDashboard

B.7 RBAC

RolePharmacist in backend/internal/middleware/roles.go was extended with the new resources and patients: GET / POST / PUT so the kiosk can register walk-ins:

RolePharmacist:
  patients              GET, POST, PUT      ⬅ POST/PUT added for walk-in registration
  prescriptions         GET, POST, PUT, DELETE
  pharmacy              GET, POST, PUT, DELETE
  pharmacy-branches     GET, POST, PUT, DELETE
  pharmacy-transfers    GET, POST, PUT, DELETE
  pharmacy-counselling  GET, POST, PUT, DELETE
  pharmacy-analytics    GET
  drug-interactions     GET, POST, PUT, DELETE
  medication-alerts     GET, POST, PUT, DELETE
  medication-admin      GET, POST, PUT
  medication-dispense   GET, POST, PUT
  mar-schedules         GET, POST, PUT
  mar                   GET, POST, PUT
  allergies             GET
  conditions            GET
  vitals                GET
  observations          GET
  general-inventory     GET, POST, PUT
  records               GET
  patient-journey       GET

The frontend kiosk shell is the practical narrowing — only role=pharmacist (or doctor/nurse with department=Pharmacy) sees the kiosk in the first place.

B.8 Frontend components

ComponentFilePurpose
<pharmacy-page>pharmacy-page.jsTop-level shell. Six tabs, Rx detail modal, register-patient modal, refill modal, inventory + Rx CRUD modals.
<pharmacy-transfers>pharmacy/pharmacy-transfers.jsInter-branch transfer queue with branch picker + Inbox/Outgoing/All sub-tabs.
<pharmacy-analytics>pharmacy/pharmacy-analytics.jsKPI dashboard. CSS bars + sparkline; no chart library.
<pharmacy-counselling>pharmacy/pharmacy-counselling.jsCounselling log with topic + follow-up filters.
<drug-interactions>pharmacy/drug-interactions.jsDrug-drug interaction master editor.
pharmacy-styles.jspharmacy/pharmacy-styles.jsShared styles for the four expansion components. Mirrors lab-styles.js.

Walk-in registration in <pharmacy-page>: three properties (showRegisterModal, registerSearch, registerMatches), debounced (350ms) api.searchPatients(q), orange match panel, splitFullName(full) helper that splits a single full-name input into first_name + last_name so the existing schema doesn’t change. On create, the system refreshes the patient list and immediately opens the New Prescription modal with the new patient pre-selected.

Conflict-handling in handleRxSubmit: the API method api.createPrescription(payload, opts) accepts an optional opts.override = true. On 409 with conflicts or interactions arrays, the kiosk surfaces a confirm() with all hits listed; if the prescriber confirms, it retries with override=true.

B.9 API service methods

In frontend/src/services/api.js:

// Branches
getPharmacyBranches() / createPharmacyBranch(d) / updatePharmacyBranch(id,d) / deletePharmacyBranch(id)

// Transfers
getPharmacyTransfers({ from_branch_id, to_branch_id, status }) / createPharmacyTransfer(d)
acceptPharmacyTransfer(id, d) / rejectPharmacyTransfer(id, d) / deletePharmacyTransfer(id)

// Interactions
getDrugInteractions({ severity, drug }) / createDrugInteraction(d)
updateDrugInteraction(id, d) / deleteDrugInteraction(id)

// Counselling
getPharmacyCounselling({ patient_id, topic, follow_up }) / createPharmacyCounselling(d)
updatePharmacyCounselling(id, d) / deletePharmacyCounselling(id)

// Refills
getPrescriptionRefills(prescriptionId) / createPrescriptionRefill(prescriptionId, d)
deletePrescriptionRefill(id)

// Analytics
getPharmacyAnalytics({ window_days })

// createPrescription gained an opts.override flag for the conflict retry path.
createPrescription(data, { override: true })

B.10 Build / verification

  • Backend: go build ./... — clean.
  • Frontend: npx vite build — 1.91 MB / 323 kB gzip in ~2s.

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

  • Stock receiving from purchasing. Today inventory is updated manually or via transfers. Wiring goods_receipts into a pharmacy_inventory insert/increment would close the supply-chain loop.
  • Auto-PO generation. Low-stock alerts exist but don’t generate purchase orders automatically. A small “low-stock → draft PO to default vendor” cron would close that gap.
  • Cold-chain monitoring. requires_refrigeration flag exists; no temperature/humidity log table or sensor integration yet.
  • Compounding. No support for extemporaneous preparations (combine ingredient A + B + C in branch X, produce N units of compound Y at a custom expiry).
  • Insurance formulary check. Eligibility advisory is computed but not joined against formulary_status to surface “non-formulary — needs prior auth” at prescribing time.
  • Controlled-substance audit chain. controlled_substance and narcotic_schedule flags are stored but there’s no per-dispense witness signature or DEA-style ledger yet.
  • Refill auto-renewal request. Refill is pharmacist-issued only. A patient-initiated “request refill” flow that pings the prescriber for re-authorisation would help outpatient pharmacy.
  • Department-aware backend RBAC. Today any pharmacist sees /api/pharmacy-*. Tightening to user.department=Pharmacy server-side is on the list.
  • Transfer SLA / escalation. A pending transfer has no deadline. A tick-loop check (“STAT transfer older than 15 min fires escalation”) would close the operational gap.
  • Per-medication drilldown on Analytics. Clicking a leaderboard row should open a filtered Rx list for that drug. Today the bars are static.
  • Pharmacy chart popup. Lab and the wards have a patient chart popup with 11 cards (allergies, conditions, vitals, current meds, etc.). Pharmacy doesn’t yet — opening one from a Rx row would help with counselling and dispensing decisions.

Last updated: 2026-04-29