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
| Responsibility | Detail |
|---|---|
| Pharmacy workbench | A single screen with six tabs — Prescriptions, Inventory, Transfers, Interactions, Counselling, Analytics. |
| Prescription queue | Every 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 checks | The 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. |
| Dispensing | Per-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. |
| Refills | Prescriptions 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 master | Per-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 transfers | Multi-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 interactions | The 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 log | Pharmacist-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 registration | Outpatient or walk-in patients can be registered directly from the kiosk’s + Patient button — no need to bounce them to reception. |
| Operational analytics | The 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
- Sign in. The system bounces Pharmacist + department=Pharmacy users to
/pharmacy. Other roles never see this kiosk. - Watch the queue. Prescriptions is the default tab. Filter by status / source. Search by patient or doctor.
- 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.
- 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.
- 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.
- 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. - 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).
- 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.
- Counselling. After dispensing a new chronic med (or any time the patient asks), open Counselling, log the session — topic, notes, optional follow-up date.
- 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:
- From any tab, click + Patient in the banner.
- 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.
- If a match appears: click it → the modal closes and the New Prescription modal opens with that patient pre-selected.
- 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:
- 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.
- 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=trueand 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.
- 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 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-tab | What it shows |
|---|---|
| Inbox | Transfers other branches sent to me, status pending. Accept / Reject buttons. |
| Outgoing | Transfers I sent. Read-only — see whether they were accepted or rejected. |
| All activity | Inbox + Outgoing combined, sorted newest first. |
The flow:
- Tech at Source clicks + Transfer Stock → picks medication, destination, quantity, priority, reason. Submit.
- Server verifies source actually has the stock. Refused if not.
- Transfer is queued
pending. Receiving branch gets a notification. - 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).
- 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:
| Topic | Use |
|---|---|
| dosing | “Take with food”, “every 6 hours”, “tapering schedule” |
| side_effects | What to expect / when to call back |
| adherence | Why finishing the course matters / pillbox tips |
| lifestyle | Alcohol / driving / breastfeeding caveats |
| drug_food | Grapefruit / tyramine / dairy interactions |
| contraception | Cycle gaps, alternative method during therapy |
| missed_dose | Catch-up rules |
| other | Anything 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_alertsfor 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', orusers.roleisdoctorornurseANDusers.departmentresolves to a pharmacy discipline viaWARD_NAVinrole-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:
| Table | Purpose |
|---|---|
pharmacy_branches | Multi-pharmacy topology (Main / ER / OPD / satellite). Seeded with three rows on a fresh boot. |
pharmacy_transfers | Inter-branch stock movement audit. status walks pending → accepted → completed, or pending → rejected. Stock movement happens at Accept time, not Create time. |
drug_interactions | Drug-drug interaction master. Seeded with ~15 high-impact pairs. Severity ∈ {minor, moderate, major, contraindicated}. |
pharmacy_counselling | Pharmacist-patient counselling log; optionally tied to a prescription. Topic enum is soft. |
prescription_refills | Refill events against an originating Rx. One row per refill. |
ALTERs on existing tables:
| Table | Columns added |
|---|---|
pharmacy_inventory | branch_id, controlled_substance, narcotic_schedule, formulary_status, requires_refrigeration, barcode, batch_number, lot_number, storage_location |
prescriptions | refills_allowed, refills_remaining, branch_id |
prescription_items | dispensed_qty (tracks partial fills) |
Seed functions:
ensurePharmacyBranchesSeed— inserts Main / ER / OPD branches ifpharmacy_branchesis empty; back-fillspharmacy_inventory.branch_id = MAIN.idfor 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 withRefillsAllowed,RefillsRemaining,BranchID,BranchName.PrescriptionItem— extended withDispensedQty.PharmacyInventory— extended withBranchID,BranchName,ControlledSubstance,NarcoticSchedule,FormularyStatus,RequiresRefrigeration,Barcode,BatchNumber,LotNumber,StorageLocation.PharmacyBranch— id, code, name, location, phone, email, manager, pharmacy_type (central/satellite/outpatient), active, plus derivedStockItems,PendingTransfers.PharmacyTransfer— id, medication_name, batch_number, from/to branch, quantity, reason, priority, status, decided-by/at metadata, plus joinedFromBranchName,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
| File | What it owns |
|---|---|
pharmacy.go | Existing 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.go | CRUD for branches. Soft-archive on delete if any inventory or transfers reference the branch. |
pharmacy_transfers.go | List / 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.go | CRUD + 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.go | CRUD with patient / topic / follow-up filters. |
pharmacy_refills.go | List + 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.go | Single 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:
- Pulls
drug_interactionsrows whereactive=trueonce (avoids per-pair queries). - Pulls patient’s active medications (prescriptions with status in {pending, partially-dispensed, dispensed}) once.
- 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:
| Method | Path | Handler |
|---|---|---|
| GET | /api/pharmacy-branches | ListPharmacyBranches |
| POST | /api/pharmacy-branches | CreatePharmacyBranch |
| PUT | /api/pharmacy-branches/{id} | UpdatePharmacyBranch |
| DELETE | /api/pharmacy-branches/{id} | DeletePharmacyBranch |
| GET | /api/pharmacy-transfers | ListPharmacyTransfers (filters: from/to branch, status) |
| POST | /api/pharmacy-transfers | CreatePharmacyTransfer |
| POST | /api/pharmacy-transfers/{id}/accept | AcceptPharmacyTransfer |
| POST | /api/pharmacy-transfers/{id}/reject | RejectPharmacyTransfer |
| 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}/refills | ListPrescriptionRefills |
| POST | /api/prescriptions/{id}/refills | CreatePrescriptionRefill |
| DELETE | /api/prescription-refills/{id} | DeletePrescriptionRefill |
| GET | /api/pharmacy-analytics?window_days=N | GetPharmacyAnalyticsDashboard |
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
| Component | File | Purpose |
|---|---|---|
<pharmacy-page> | pharmacy-page.js | Top-level shell. Six tabs, Rx detail modal, register-patient modal, refill modal, inventory + Rx CRUD modals. |
<pharmacy-transfers> | pharmacy/pharmacy-transfers.js | Inter-branch transfer queue with branch picker + Inbox/Outgoing/All sub-tabs. |
<pharmacy-analytics> | pharmacy/pharmacy-analytics.js | KPI dashboard. CSS bars + sparkline; no chart library. |
<pharmacy-counselling> | pharmacy/pharmacy-counselling.js | Counselling log with topic + follow-up filters. |
<drug-interactions> | pharmacy/drug-interactions.js | Drug-drug interaction master editor. |
pharmacy-styles.js | pharmacy/pharmacy-styles.js | Shared 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_receiptsinto apharmacy_inventoryinsert/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_refrigerationflag 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_statusto surface “non-formulary — needs prior auth” at prescribing time. - Controlled-substance audit chain.
controlled_substanceandnarcotic_scheduleflags 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 touser.department=Pharmacyserver-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