Skip to content

New Schedule wizard polish, CAD QR scan, patient card labels, Director/Programme Manager roles - #18

Merged
Rl0007 merged 57 commits into
bwhtech:mainfrom
dadsena01:schedule-and-roles-followup
Aug 27, 2026
Merged

Rl0007 merged 57 commits into
bwhtech:mainfrom
dadsena01:schedule-and-roles-followup

Conversation

@dadsena01

@dadsena01 dadsena01 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Reworked the New Schedule wizard - fields now go Project > Site > Clinic > Unit and each one narrows the next based on what's actually been scheduled together before. Made Project, Unit and all 4 Who fields required since people were skipping them. Swapped the plain progress bar for an actual stepper, next dates now show as a timeline, and moved the assigned-elsewhere clash warning out of that tiny sidebar into a proper table that only shows up on the Who step. Added a next-4-weeks table on the last step too.

Added camera QR scanning on the CAD page using Frappe's built in scanner, so staff without a USB scanner can just point a phone at the card. Tested it with an actual QR code through a fake camera and it correctly found the patient. Also wired up district autocomplete which existed on the backend but was never hooked up.

Patient card was showing raw values with no labels (just "Male 11-08-2026 9797646461") so added labels to everything and also added the ABHA ID which was missing.

Last thing - there was no Director or Programme Manager role anywhere so the dashboard and reports were stuck at System Manager only. Added a patch for both roles. Turned out just adding them to the workspace wasn't enough, they also needed read access on Bandhu Clinic Session or the dashboard wouldn't even show up in their sidebar. Also cleaned up custom_encounter_status, it was dead - nothing ever wrote to it so it just showed "Registered" on every encounter.

123/123 tests passing, checked everything live.

Same base as #10 since none of that is merged yet, so this has all of that plus today's commits.

dadsena01 and others added 16 commits August 11, 2026 16:03
- Add System Manager staff onboarding page: creates User + Healthcare
  Practitioner in one call instead of four manual Desk steps
- Convert CAD/Doctor/Nurse from website pages to Desk Pages with full
  Desk chrome (sidebar, search, notifications)
- Grant Doctor/Nurse/CAD roles read access on Page doctype
- Gate patient queueing on clinic session status = In Progress, so a
  patient can no longer reach the doctor before the nurse starts session
- Add Details/Order Test/Prescribe Medicine dialogs for doctor, and
  Enter Results/Dispense dialogs for nurse, carrying real structured
  clinical data instead of a bare state flip
- Add doctor get_session_status/session bar for parity with CAD/Nurse
- Add CAD/Doctor/Nurse/Onboard Staff shortcuts to the Admin workspace
Restrict the CAD, Doctor and Nurse workspaces to their own role plus
System Manager, and the remaining workspaces to System Manager only.

Framework, Quality and Marley Health are Desktop Icon records owned by
frappe, erpnext and healthcare, so they cannot be restricted by editing
those apps. desk_visibility runs on after_migrate instead, because each
app re-syncs its own icon fixture on every migrate and would otherwise
wipe the restriction.

Clicking a CAD, Doctor or Nurse workspace icon now routes straight to the
working page rather than an icon grid. The include path carries an
explicit version, since app_include_js raw paths are not cache-busted by
the framework and browsers keep serving the previous file.
Clinic ID is now LSG(2) + Unit(1) + Year(2) + Serial(5), replacing the
BMC-##### series for newly registered patients. IDs already issued are
left untouched, and both formats stay searchable.

The serial resets per calendar year and runs global across every LSG and
unit. Scoping it per LSG or unit would tie a permanent identifier to
attributes that get corrected and reorganised, forcing an issued ID to
either change or start lying. It is drawn from the Series counter rather
than max()+1, which races when two front desks register at once, and it
throws instead of widening past five digits.

lsg_code and unit_code hold text like LSG-EKM-KLM, so the numeric
components live in new fields alongside them. Both become immutable once
any ID has been issued, because every printed card embeds them.

Bandhu Clinic Session gains a unit link. Nothing in the registration path
could resolve a unit before; deriving it from Unit.cad breaks as soon as
one CAD covers two units. register_patient now takes the session and is
gated on session access rather than role alone, since the session decides
which codes land in a permanent ID.

The QR encoded an API URL, so scanning a card produced a JSON page. It
now carries the bare Clinic ID, which is what a USB barcode scanner needs
and what a phone camera can show a human. A patch reissues the images for
existing patients.

New Bandhu Patient Card print format at CR80 size, reachable from the CAD
screen through a whitelisted endpoint: the role holds no Patient print
permission, so /printview refuses it. Scanning a card jumps straight to
the patient on an exact match, and every queue row carries the grouped
Clinic ID and its own print action.
Captures work that had accumulated uncommitted across several sessions,
plus this round's security and correctness fixes.

Scheduling
- Bandhu Session Schedule (+ weekday child, Bandhu Settings single) and
  utils/session_schedule.py generate recurring camps weekly, fortnightly
  or monthly. Idempotency keys on (schedule, date), so a camp cancelled
  for a holiday is not regenerated by the nightly job.
- page/new_schedule: guided Where/When/Who/Check wizard, with clash
  warnings when a doctor, nurse or vehicle is already committed.
- page/my_schedule: field staff see the camps they are on the team for,
  drivers included. Cancelled camps are shown and badged, not hidden.
- First use of scheduler_events in this app.

Dashboard
- Six number cards and a weekly chart on the Dashboard workspace,
  following the Number Card + Dashboard Chart pattern used elsewhere in
  the org rather than a custom page.

Security and correctness
- Escape patient_name, mobile and the clinic ID in the patient card
  print format. Frappe's Jinja environment has autoescape off, and the
  card is rendered into a same-origin window, so a name entered at
  registration could run as script in the session of whoever printed it.
- register_patient now requires a running camp, sharing one gate with
  create_encounter. The camp resolves the LSG and unit codes baked into
  a permanent, printed Clinic ID.
- Nurse start/end camp is now a state machine: no reopening a closed
  camp, no opening one dated another day, nothing on a cancelled camp.
- sync_to_queue survives two front desks registering the same patient at
  once. Patient Queue.patient is unique and DuplicateEntryError is not a
  ValidationError, so the existing handler never caught it and the whole
  registration rolled back.

Display
- Ages read 52y / 8mo / 12d instead of Healthcare's three-line
  "52 Year(s) 4 Month(s) 16 Day(s)".
- Camp site shows its name, not its record id.
- Queue tables no longer clip the last patient behind a 360px box.

106 tests pass.
Whitelisted methods no longer return {"success": True} — the absence of an
error is the success signal, so the client stops branching on a flag that only
ever held one value. frappe.throw's explicit frappe.ValidationError argument
goes with it; the default is the same exception.

Each Desk page's styles move out of the JS template string into a sibling
.css file.
First of the eight reports in the scope doc. One row per camp for a period:
where and when, the team, opening hours, patients seen split into new and
repeat, tests ordered vs done, medicines prescribed vs dispensed. Summary
band, per-camp chart, and a shortcut on the Dashboard workspace.

Roles are System Manager only. A Script Report runs raw SQL and bypasses
permissions entirely, so the Report record's roles table is the only gate.

Counts come from the encounter's own child tables. Nothing in the clinic loop
writes Test Result, Bandhu Medication Dispense or Referral — the rows in those
doctypes are pre-handoff seed data pointing at session ids, so joining them
returned zero for every camp while looking correct.

Test Instructions.result_type had no blank first option, so Frappe filled every
newly ordered test with the first Select value: a malaria test nobody had run
yet read as Positive on the boards and in the report. The options now start
blank, and the patch clears the rows the old schema mislabelled — only where
the encounter is still Awaiting Test, so a nurse's real result is untouched.
Tests Report is one row per test: result and value alongside the patient's
Clinic ID, sex, age group and native state, and the camp it was run at. The
Pending filter matches the absence of a result, so it is applied in Python
rather than as a SQL equality.

Clinic Report aggregates the same camps by clinic, project, unit, LSG or site,
with camps scheduled vs held vs cancelled and patients per camp. The grouping
picks a field, so it is validated against a whitelist rather than trusted.

The four per-camp count queries move to utils/clinic_stats.py, now that Session
and Clinic Report both run them. age_group() lands in utils/patient.py with
bands we chose — the scope doc asks for age-group breakdowns without defining
them, so CMID still has to confirm.
Patients Per Camp, Tests By Result and Patients By Clinic render the
existing Session, Tests and Clinic reports rather than re-querying, so a
change to a report's logic cannot drift from its chart.

Each carries a rolling 30-day window as a dynamic filter, since a fixed
from_date would freeze the moment the JSON was written.

The quick list and the camps chart move to full width. At half width the
three new charts sat in a ragged two-column grid with unreadable
category labels.
…ests By Result series

- Patients By Clinic threw 'Unknown grouping: clinic' — the chart's
  filters_json passed lowercase 'clinic' but Bandhu Clinic Report's
  GROUP_BY_FIELD keys are capitalized ('Clinic').
- Patients Per Camp drew one bar per session, so several camps sharing
  a date truncated the x-axis into unreadable stubs. Now grouped by
  day.
- Tests By Result always drew a 'Positive' series even when nothing
  was positive, showing an empty legend entry. Now stacks whichever
  result types actually occur (Positive/Negative/Value/Pending) and
  drops series with no data.
…ields

- Where > When > Who > Check fields reordered to the real hierarchy
  (Project > Site > Clinic > Unit); Site/Clinic/Unit narrow to what has
  actually been scheduled together before, falling back to the full list
  when there is no history yet instead of blocking a new combination.
- Project and Unit are now required, matching Site and Clinic; Project
  is derived server-side from the selected Clinic in the DocType's own
  validate() so the Desk form and any other caller get it too, not just
  this wizard's JS.
- Who step (Doctor/Nurse/Driver/Vehicle) is now required before Next.
- Top progress bar replaced with a real numbered stepper.
- 'Next dates' renders as a timeline instead of a flat list.
- 'Already assigned elsewhere' moved out of the narrow sidebar into a
  full-width table below the wizard, shown only on the Who step where
  it is relevant, with Role/Name/Site/Date columns and a thin separator
  under the header row.
- Check step adds a Next 4 Weeks table (Date/Site/Clinic/Unit/Time).
- Wizard buttons/pills/day-chips now use Frappe's own button CSS
  variables instead of hardcoded colors.
- Add a Scan button next to patient search that opens Frappe's built-in
  Scanner (html5-qrcode, already bundled in Desk) to decode a patient
  card's QR with the device camera and run the same lookup a typed or
  USB-scanned Clinic ID already goes through. Guards on
  navigator.mediaDevices being available so a browser/origin that can't
  do camera access gets a clear message instead of a silent failure.
- Native District now offers suggestions from get_districts(), filtered
  by the selected Native State, via a datalist on the existing free-text
  field -- no change to it staying free text.
Sex/DOB/Mobile were printed as bare values with no label, unreadable at
a glance. Every field now reads Label: value, including a new ABHA ID
line (shown only when the patient has one). All still pass through the
existing | e escaping this print format depends on.
…status

No Director or Programme Manager role existed anywhere in the system, so
the Dashboard workspace and all 3 scope reports were System Manager only.
New patch creates both roles and grants them read on Bandhu Clinic
Session -- the real gate: a Workspace's own roles list only narrows
visibility after the user already has read permission on some doctype in
that module (Workspace.is_permitted() in Frappe core runs after a module
check), so without this the Dashboard workspace never showed up in the
sidebar for either role and /app/dashboard fell through to the core
Dashboard DocType's list view instead. Added to the Dashboard workspace
and Session/Tests/Clinic report roles.

custom_encounter_status was never written by any app code -- only its own
Select default filled it, so all 41 live encounters read 'Registered'
regardless of their actual state. Removed the field definition and fixed
the two dangling insert_after references; a patch drops the Custom Field
record and the now-orphaned column.
The page stylesheets referenced --border-radius-full, --border-radius-md
and --border-radius, none of which frappe v17 defines. An undefined custom
property makes the whole declaration invalid, so every badge and card fell
back to square corners.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UcoFSie72rSSkiKmPJFySt
Rl0007 and others added 5 commits August 26, 2026 12:04
…ent card

Both are attribute-context values in a template whose Jinja env has autoescape
off; one bad quote in custom_bandhu_id would break out of the alt attribute.
Every patient in the DB is readable through the CAD form's search and printable
card. That stays — a CAD legitimately meets patients registered at another site,
and narrowing the search would break repeat-visit lookup. What was missing is any
record of who looked at whom, so both endpoints now write through Frappe's own
frappe.core.doctype.access_log.make_access_log, which defers the insert and so
costs the request nothing.

The card render is logged with the patient as the reference document: it names one
patient and puts their details on paper. The search is logged with the term,
because it is not keystroke-driven — cad_form.js fires it on Enter or the search
button only (cad_form.js:322-327) — so it is one row per deliberate lookup, not a
flood. Log Settings clears Access Log on its own retention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It was whitelisted with no guard, so any authenticated user could call it. The
data is a static district list, so the fix is the gate the rest of the form
already uses, not a redesign. Its only caller is the CAD registration form's
native-state change handler (cad_form.js:511), which require_cad_access() lets
through unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Patient Queue.patient is unique and sync_to_queue overwrites the row on every
visit, so one row per patient ever registered sat on the board for the life of the
site with nothing to remove it. Patient Queue is a projection — Patient Encounter
holds the clinical record and sync_to_queue rebuilds the row from it on the next
save — so a finished row can be dropped and nothing is lost.

Implemented as the clear_old_logs interface Frappe's Log Settings already looks
for (frappe/core/doctype/log_settings), registered via
default_log_clearing_doctypes, rather than as a scheduled job of our own. Only
Done rows past the retention window are touched; Active rows stay whatever their
age, so an unclosed camp keeps its board.

The Data-typed patient/clinic_session columns are left alone — changing them to
Link touches a unique index and is a schema change, not this cleanup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sync_to_queue's savepoint recovery caught frappe.DuplicateEntryError, which
base_document only raises for a primary-key collision (base_document.py:837). A
unique *field* goes through show_unique_validation_message() and raises
frappe.UniqueValidationError instead (base_document.py:917), and Patient Queue
keys its unique index on `patient`, not on the name — so the loser of a
concurrent registration escaped the handler entirely, propagated out of the
on_update hook and out of encounter.insert(), and rolled the whole registration
back.

Both classes are caught now: the row is also named by a dated series two
same-second inserts can still collide on. The recovery clears the framework's
queued "must be unique" msgprint, which would otherwise report a failure the
front desk did not have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Rl0007

Rl0007 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Consolidation note (2026-08-26)#10, #11, #12, #13, #14, #15, #16 and #17 have been closed as superseded by this PR, with the reason recorded on each one.

Why: this branch was opened on the same base as #10 and already contains every commit from all eight of those PRs, unchanged and by the same SHA, plus five newer ones. They were eight windows onto code this PR already carries — separate review meant reviewing the same diff twice, and merging them in any order would have conflicted against this branch. Each closure was verified with git merge-base --is-ancestor <commit> 0442d66 first; no commit was dropped.

So this PR is now the single review surface for the whole stack. #9 (README contributing section) is unrelated and was left open.

Follow-up work sits on fix/audit-consolidated, branched off this PR's head, closing the remaining findings in AUDIT_FINDINGS.md (F1 escaping leftovers, the F3 exception class, F6 access logging, F8 role gate, F11 queue retention) — it will be raised separately once this merges, so nothing here needs to change.

@Rl0007

Rl0007 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Follow-up branch is now up as #19 (draft) — it is stacked on this branch, so it should merge after this one. It closes the remaining AUDIT_FINDINGS.md items and fixes four bugs a full browser pass turned up, two of which touch code introduced here:

Nothing here needs changing — #19 carries all of it.

Rl0007 and others added 25 commits August 26, 2026 13:39
A Time read from the DB is a timedelta, but one arriving from a client save is a
string, and "13:30:00" <= "8:00:00" is True lexicographically - so every
schedule starting before 10am was unsavable from the Desk form.
/desk renders the Desktop Icon grid, and Frappe seeds icons from workspaces only
in after_app_install - which ran before these workspaces declared their app, so
field staff had no icon for their own board. The old restrict list named three
icons by hand, missed every ERPNext one, and named one that does not exist here.
link_to resolves against Workspace Sidebar, not Workspace, and the two sync
separately - from after_migrate an unresolvable link took the whole migrate
down.
is_icon_permitted resolves a link icon through bootinfo.workspace_sidebar_item,
which is keyed by app and never by workspace name, so per-workspace icons were
created and filtered straight back out. The app tile is the route; the sidebar
behind it is already role-filtered.
Frappe's standard.css sets .table { margin: 20px 0 }; these files zeroed only
margin-bottom, so the surviving 20px showed as an empty band inside the bordered
wrap above every header row. Page containers also never gained the bottom
padding that removing the .table-wrap height cap implied.
A sticky offset resolves against the scroll container's content box, so top:0
parked the clash header 15px inside .clash's padding and rows scrolled through
the gap - which is what read as the header breaking apart.
…e wizard

One bullet per person per date is unreadable at forty rows, and re-warning about
clashes the wizard already showed is how real warnings get ignored.
Every line carried the same weight, so the one thing the row is opened to answer
- where am I going - read no louder than the nearest PHC.
The chrome generator ignores @page (preferCSSPageSize=false) and reads geometry
off .print-format, so the card printed in the corner of a blank A4; the browser
print path needed @page margins to suppress Chrome's own header furniture.
The five tests were hardcoded in four places and had already drifted apart.
They are now records: the doctor's checkbox grid, order_test validation and
the Tests Report filter all read the master, and Test Instructions.test_name
is a Link to it.

The master carries the result shape (Positive/Negative vs a measured Value,
with its unit), so the shape stops being knowledge each form has to hold —
that split is what let an untested test read "Positive" until 2026-08-20.

Seeded via after_install for new sites and an idempotent patch for the two
existing ones; a retired test keeps enabled = 0 across re-seeds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
IntegrationTestCase rolls back once the class finishes, not per test, so a
test that retires a master was still visible to the next one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MultiCheck re-sorts its options alphabetically unless sort_options is false,
so display_order never reached the screen. Caught in the browser, not by the
tests, which assert the server's order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
order_test stamps the doctor's one ordering note onto every row it appends, so the
Patient Details dialog printed the same sentence once per test. shared_test_note
reports that note once when every row agrees, leaving the per-row notes in the data
for the rows that genuinely carry their own.

attach_test_shapes carries result_shape and unit down from the Bandhu Test master in
one query for the whole batch. A row cannot say which shape it is — an ordered but
untested row has no result_type at all — so the boards could not tell a measurement
from an indicator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…markup

Nine full-width label/value rows separated by rules is heavier ruling than anything
Desk draws. The dialog now uses Desk's own read-only field shape — a small muted
control-label above its value, in the bootstrap grid and espresso typography the desk
bundle already ships — so it follows Desk across an upgrade instead of drifting from
it. Height, weight and BMI collapse into one Vitals field rather than three more rows.

Two display bugs go with it:

- Date of Birth printed the stored 1992-02-14. It now goes through
  frappe.datetime.str_to_user, like every other date in this app.
- Tests read as a bullet list with a literal double dash and the status as plain
  text ("Hb -- pending"), and the ordering note dangled under every row. They now
  read as rows — name left, result right — with es-badge indicators: Positive is the
  only solid badge, because it is the one a nurse must not walk past. A value test
  prints its reading and the master's unit (11.4 g/dL) instead of the word "Value",
  and a per-row note is labelled rather than left hanging under its test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.detail-row and .detail-heading had no callers left once the dialog moved onto Desk's
own classes, and page rules for a frappe.ui.Dialog are exactly what drifts from Desk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Font Awesome is not in this bench's Desk bundle (`grep -c "fa-calendar-o"
sites/assets/frappe/dist/css/desk.bundle.*.css` returns 0), so every
`<i class="fa fa-...">` this app shipped rendered as an empty element.
Eight were left after the earlier conversion pass: Start/End Session on
the nurse page, the QR-scan and register buttons on CAD, and the
retry-screen warning, clinic, site and session-status glyphs in the
shared session UI. All now go through `frappe.utils.icon()` against ids
verified to exist in frappe/public/icons/lucide/icons.svg.

The session status was the worst of them: with the dot invisible,
"In Progress" and "Completed" differed only by the word. It is now an
`.es-badge` rather than a restored dot — a dot carries its meaning only
in colour, which fails a colour-blind reader and a phone screen in
sunlight, and these boards are used outdoors.

Two things the swap exposed. Desk's own `.icon` carries `margin: 0 auto`,
which inside the flex session bar let the icons absorb the free space and
drift away from their labels, so the bar pins it to 0. And
`.load-error-icon` sized its glyph with `font-size`, which does nothing
to an SVG; it now sets width/height.
Healthcare Practitioner declares title_field: practitioner_name but never sets
show_title_field_in_link, so every Desk Link to a practitioner showed
HLC-PRAC-2026-00004. Shipped as a Property Setter, since editing the healthcare
app's JSON would be lost on the next bench update.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five top-level custom buttons crowded the form header and truncated the
breadcrumb to "Ban /". The three that act on generated camps move under a
Sessions group, Copy This Schedule under Create; Preview Next Dates stays
top-level as the only read-only check and the only button on an unsaved doc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Desk leaves 27px under a workspace's last widget — --padding-sm on
.layout-main-section plus the 20px floor it forces on editor.js's
redactor — so the Dashboard's last row of shortcuts sat flush against
the viewport. Adds the same --padding-2xl footer the Bandhu Desk pages
carry, scoped per workspace so the other installed apps keep Desk's
own spacing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A quick list, a link field and the form header all read Bandhu Clinic Session's
title field, which was the raw `site` link -- so every camp showed as SITE-0002.
The new fetched `site_name` carries the readable name; a patch backfills the
existing rows, since fetch_from only fires on save.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five test names against four result series answered no question anyone asks of
the dashboard; the Tests Report already carries the same breakdown with filters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Desk's .icon is an inline-block whose baseline is its bottom edge, so an icon
set beside a label rode above the label's optical centre on every Bandhu page.
The rule centres them once for all six page roots; its gap replaces the literal
space the markup carried.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cards led with the visit count and buried the one thing a doctor sorts by:
whether this patient is theirs to act on or is sitting with the nurse. Each card
now leads with that state, carries the Clinic ID and a readable clinical line
("Tests: awaiting Hb, Malaria") instead of "0/2 test(s) done", and keeps a fixed
action rail -- so a patient with nothing to do renders a reason, not a card with
a hole where the buttons would be. Completed Today drops to a table: no row
there is actionable, and a card each cost a screen per camp.

`group_clinic_id` moves into session_ui so both boards read one copy, and the
Clinic ID rides down in the query that already resolved each patient's age.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A patient who leaves before being seen stayed on the doctor's and nurse's boards
for the rest of the camp with no way to clear them. Cancelled becomes a real
workflow state, reachable from every live state and exiting to none, and the
front desk gets the action against a running session it owns.

Also in this pass: the schedule rail is drawn per item so it starts at the first
bullet and stops at the last, and cad_form picks up the shared Clinic ID
formatter now that it lives in session_ui.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Rl0007
Rl0007 merged commit 3199cac into bwhtech:main Aug 27, 2026
1 of 3 checks passed
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.

3 participants