Skip to content

feat(pharmacy): add PharmacyInput, PharmacySearch and PharmacyField - #737

Open
norman-ma wants to merge 3 commits into
med-component-updatefrom
pharmacy-components
Open

norman-ma wants to merge 3 commits into
med-component-updatefrom
pharmacy-components

Conversation

@norman-ma

Copy link
Copy Markdown

What & why

Adds the e-prescribing pharmacy field to @mieweb/ui, re-implementing WebChart's PatPharmacyInput / PharmacySearch (MIEstache + jQuery) as React components for the prescription form. Stacked on #719 (med-component-update); retarget to main after it merges.

Components (src/components/Pharmacy):

Export Role
PharmacyInput Native <select> of the patient's likely pharmacies (preferred, then recent) + search button. Keeps WebChart's form contract: pharmacy / pharm_id fields, placeholder "0", and ncpdpid / npi / newrx option attributes for transmitOptionSet.
PharmacySearch Directory search modal: postal / state-city / phone / fax filters, Exclude NPI, Mail Only, name search, details panel, Use Once / Make Preferred.
PharmacyField Both of the above, wired to a host-supplied PharmacyDataSource (six async functions).
usePharmacyCandidates, usePharmacySearch The orchestration PharmacyField uses (debounce, abort, stale-result guard), for custom layouts.
pharmacy.ts The domain rules, implemented once: pharmId ⇄ (id, idType), classification, default selection, search params, role 5/6.

Problem. Every prescription form needs the patient's pharmacy, and the selection and search rules are subtle (N-prefixed NPI ids, override vs. preferred precedence, the 6-digit phone/fax rule). Implementing them once in the library keeps WebChart and any new host consistent.

Alternatives considered.

  • ProviderSearchBar / ProviderSearchFilters: provider-specific, with no candidate list or preferred-save flow.
  • UserPicker: people, not a directory with service levels.
  • CodeLookup: offline code shards, not a live directory.
  • Custom Select: the field must stay a native <select> for the legacy form and delegated jQuery change handlers.

The new components compose the existing Modal, Autocomplete, RadioGroup, Checkbox, Input, Tooltip and Button.

Relationships. All are declared in parameters.catalog on both pages and summarized in the Encounter & orders overview:

  • PharmacyInput ↔ PharmacySearch: composes with.
  • PharmacyInput ↔ PharmacyField: alternatives.
  • PharmacyField contains PharmacySearch.
  • PharmacyField ↔ MedicationList (MedicationEditor): composes with. The PharmacyField page shows the host wiring that chooses e-send or fax from serviceLevelFlags(pharmacy).newRx.

Boundaries (owned by the app or WebChart). These are documented in the story descriptions:

  • Endpoints, the candidates SQL and the server's multi-pass search.
  • City normalization, zip proximity and the staging directory.
  • Patient-level authorization on save.
  • Non-US state lists and persisting filter preferences.

Defects fixed relative to WebChart. Each is covered by a test:

  • Make Preferred no longer silently clears the mail-order slot when nothing is selected.
  • Only one candidate is selected when the patient has both a local and a mail-order preferred pharmacy.
  • "0" is treated as no prefill.
  • The 6-digit rule counts digits, not characters.
  • Save failures are shown and keep the modal open.
  • The modal closes once.

Also in this PR (separate commit): Autocomplete now stops Escape from propagating while its list is open, so Escape closes the list rather than the enclosing Modal. This also fixes the same bug in MedicationEditor's drug search.

Compatibility. Additive exports only, with no new dependencies. The Autocomplete change only affects Escape while the list is open.

Evidence.

  • 32 stories, including Empty, Loading, Error, Mobile, RTL (Arabic), save failure, and a composed field with a fake data source and a visible call log.
  • All story play functions were run in a browser.
  • New visual baselines: light, dark, mobile, the results list, and the details panel's service-level branches.
  • RTL matrix entries with geometry and :dir() assertions.
  • I checked brand switching (WebChart, BlueHive) and text expansion (long-name fixture) by hand.

Known gap: I haven't checked focus return in Safari. It uses returnFocusRef, because Safari and macOS Firefox don't focus a button when it's clicked.

Maintainer notes: src/components/Pharmacy/MAINTAINERS.md, registered in CONTRIBUTING.md.

Checklist

  • pnpm typecheck && pnpm lint && pnpm format && pnpm test pass
    • pnpm typecheck, pnpm lint, pnpm format: passed.
    • pnpm test: 2662 passed, 9 failed. All 9 failures are 5-second timeouts under full-suite load in files this PR doesn't touch (EncounterVisit, MedicationEditor, OrgChart, SuperChat, TextAreaRich). Run alone, those 7 files pass (165/165). Pharmacy and Autocomplete: 91/91.
  • Visual tests pass / baselines updated if rendering changed (pnpm test:visual)
    • Run in mcr.microsoft.com/playwright:v1.58.2-noble against storybook-static, because Playwright 1.58 won't install on the Ubuntu 26.04 dev host. The container reproduces the existing CI baselines exactly (40/40 RTL matrix).
    • tests/visual/pharmacy.spec.ts + tests/visual/rtl-matrix.spec.ts: 49 passed.
    • 9 new baselines. I reviewed the images by hand.

RTL — see CONTRIBUTING.md → RTL & direction-agnostic styling

  • No new physical-direction classes or inline styles (ml-, left-, marginLeft, left:, textAlign: 'left', …) — pnpm rtl:scan passes
  • Directional icons flip with rtl:-scale-x-100; genuinely physical usages annotated // rtl-ignore -- <reason>. There are no directional icons: search, phone, truck, bolt and shield have fixed meaning.
  • ArrowLeft/ArrowRight handlers are direction-aware (useDirection()). None were added; Autocomplete handles only Up and Down.
  • New class strings safelisted in both src/tailwind-preset.ts and src/tailwind-preset.cjs
  • Direction-sensitive rendering has RTL visual coverage: screenshot plus computed-style/geometry assertions
    • pharmacy-input: the search button sits at the inline end, with square joined corners.
    • pharmacy-info: labels are at the inline start, phone numbers are LTR islands, and street addresses aren't reordered (bdi matches :dir(ltr)).

norman-ma and others added 2 commits November 10, 2026 11:51
…e list

Modal listens for Escape on document, so Escape meant to close the result
list also closed the dialog (e.g. MedicationEditor's drug search). Stop
propagation while the list is open; a second Escape still closes the Modal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-implements WebChart's patient pharmacy input and pharmacy search for
the prescription form:

- pharmacy.ts: pharmId parsing, classification, default selection,
  search params and preferred role, implemented once
- PharmacyInput: native select + search button, keeping WebChart's form
  contract (pharmacy / pharm_id, "0" placeholder, ncpdpid/npi/newrx)
- PharmacySearch: directory search modal with Use Once / Make Preferred,
  details panel, focus return
- PharmacyField + usePharmacyCandidates / usePharmacySearch: wired to a
  host-supplied PharmacyDataSource with debounce and stale-result guard

Fixes WebChart defects: Make Preferred can no longer clear the mail-order
slot, only one preferred candidate is selected, "0" means no prefill,
and the 6-digit rule counts digits.

Includes stories, unit tests, visual and RTL baselines, condensed-density
rules, safelist entries and maintainer notes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings November 10, 2026 16:57
@norman-ma
norman-ma added this pull request to stack #738 November 10, 2026 16:58
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Deploying ui with  Cloudflare Pages  Cloudflare Pages

Latest commit: b53fed3
Status: ✅  Deploy successful!
Preview URL: https://2948ab9c.ui-6d0.pages.dev
Branch Preview URL: https://pharmacy-components.ui-6d0.pages.dev

View logs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Patient-specific pharmacy state can carry over to another patient, and late save completions can overwrite selections and close reopened dialogs.

6 open issues
What changed in this PR

Adds reusable pharmacy selection and directory search to @mieweb/ui prescription forms, preserving WebChart’s native form contract while leaving backend integration to the host application.

Changes:

  • Adds PharmacyInput, PharmacySearch, PharmacyField, shared domain rules, and data-source hooks.
  • Adds component documentation, fixtures, unit tests, visual coverage, RTL checks, and condensed styling.
  • Updates Autocomplete so Escape closes its open list without dismissing the enclosing modal.
File Description
tests/​visual/​rtl-matrix.spec.ts Adds pharmacy alignment and text-direction checks.
tests/​visual/​pharmacy.spec.ts Adds theme, mobile, results, and details screenshots.
src/​tailwind-preset.ts Safelists new pharmacy utilities.
src/​tailwind-preset.cjs Mirrors the pharmacy safelist additions.
src/​styles/​condensed-view.css Adds compact pharmacy styling.
src/​index.ts Exports the pharmacy module.
src/​components/​Pharmacy/​usePharmacyData.ts Adds loading, search, cancellation, and save hooks.
src/​components/​Pharmacy/​storyData.ts Supplies fictional fixtures and an in-memory data source.
src/​components/​Pharmacy/​PharmacyTypeIcon.tsx Displays pharmacy capability icons.
src/​components/​Pharmacy/​PharmacySearch.tsx Implements directory search and preferred selection.
src/​components/​Pharmacy/​PharmacySearch.test.tsx Tests search interactions, saving, and focus return.
src/​components/​Pharmacy/​PharmacySearch.stories.tsx Documents search usage and demonstrates states.
src/​components/​Pharmacy/​PharmacyInput.tsx Adds the native candidate select and search button.
src/​components/​Pharmacy/​PharmacyInput.test.tsx Tests selection rules and legacy form attributes.
src/​components/​Pharmacy/​PharmacyInput.stories.tsx Documents input usage and variants.
src/​components/​Pharmacy/​PharmacyInfo.tsx Displays pharmacy details and service capabilities.
src/​components/​Pharmacy/​PharmacyHelp.tsx Adds labelled help buttons with tooltips.
src/​components/​Pharmacy/​PharmacyField.tsx Connects the input and search to host data.
src/​components/​Pharmacy/​PharmacyField.test.tsx Tests loading, debounce, stale responses, and selection.
src/​components/​Pharmacy/​PharmacyField.stories.tsx Demonstrates connected usage and prescription integration.
src/​components/​Pharmacy/​pharmacy.ts Centralizes identifiers, classification, and selection rules.
src/​components/​Pharmacy/​pharmacy.test.ts Tests domain rules and display formatting.
src/​components/​Pharmacy/​MAINTAINERS.md Records architecture, invariants, and testing guidance.
src/​components/​Pharmacy/​index.ts Defines public pharmacy exports.
src/​components/​Pharmacy/​dataSource.ts Defines the host’s async data contract.
src/​components/​MedicationList/​MedicationList.stories.tsx Links medication editing to PharmacyField.
src/​components/​Autocomplete/​Autocomplete.tsx Consumes Escape while the list is open.
src/​components/​Autocomplete/​Autocomplete.test.tsx Adds nested-modal Escape regression coverage.
src/​catalog/​EncounterOrders.mdx Adds pharmacy selection guidance and data contracts.
CONTRIBUTING.md Registers pharmacy maintainer notes.

Review effort: Balanced

Comment on lines +205 to +207
(patient) => {
if (patient)
setInitialFilters(withOverrides(filtersFromLocation(patient)));
…earch

- reset override, selection and search when patId changes
- block dismissal while Make Preferred saves; ignore a save that settles
  after the dialog is gone
- required PharmacyInput fails validation on the "0" placeholder
- Enter in dialog fields no longer submits a host form
- hide superseded results while a search loads
- late initialFilters don't overwrite user-edited filters
- wait for web fonts before the details-panel screenshot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings November 12, 2026 18:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Changing patients can leave the host holding the previous patient’s pharmacy until loading succeeds, or indefinitely if it fails, risking use of the wrong pharmacy.

2 open issues
3 previously missed issues

In code that hasn't changed since last review

Medium severity Invalidate pending saves when the dialog session closes

src/​components/​Pharmacy/​PharmacySearch.tsx:254

With motion enabled, Modal keeps this body mounted during exit, so closing the dialog does not invalidate a pending save. If it settles during exit or after a quick reopen, it can publish the old choice and close the reopened search. Invalidate a dialog-session token on close, check it after the save settles, and cover animated close/reopen in the late-save test.

Medium severity Restore focus after the modal exit animation completes

src/​components/​Pharmacy/​PharmacySearch.tsx:610

Focus is returned as soon as open changes to false, but Modal keeps the dialog and focus guard active until its animated exit finishes (Modal.tsx:148–193). Focus then sits outside a still-visible modal, and Tab presses on the opener bypass the dialog's focus trap. Restore focus after the exit completes, using the immediate path only when there is no animated exit.

Medium severity Prevent Escape from dismissing the enclosing modal

src/​components/​Pharmacy/​PharmacySearch.tsx:670

When this search is inside another Modal, Escape with the result list closed reaches both document listeners and can dismiss the prescription editor too. The pending-save guard only blocks this search's dismissal, so the enclosing modal can still unmount it. Consume Escape inside the pharmacy dialog after Autocomplete handles its list, and use handleOpenChange to preserve the pending-save guard.

5 resolved since last review

Review effort: Balanced

const candidatesRef = React.useRef(list.candidates);
candidatesRef.current = list.candidates;
React.useEffect(() => {
if (list.loadCount === 0) return;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants