medOS ultra

Queue Management Floater

Reusable FAB + side-panel for queue management on any page, with per-deptType defaults and per-row actions.

7 min read diagramsUpdated 2026-04-28docs/architecture/queue-management-floater.md

Status: Live Component: web/src/common/components/queue-management/QueueManagementFloater.tsx Used by: /cashier (dept_type=billing). Designed to drop into any queue-driven page.

1. What it is

A floating action button + side panel that gives any page full queue management capability with one line of JSX:

<QueueManagementFloater deptType="billing" onPrimary={(row) => openPaymentDialog(row)} />

The FAB sits on the right edge of the viewport (configurable). Click → 420px-wide side panel slides in with:

  • 6 tabs: Active / On Hold / Waiting / Recalled / Completed / History (with live counts)
  • Per-row 3-dot menu: primary action / view detail / recall / hold / release-hold / complete / cancel
  • Search across HN, name, VN, queue number
  • Realtime: subscribes to department_queues changes so the panel always reflects truth
  • Pending count badge on the FAB: shows how many tickets are on hold or have been recalled

2. Architecture

┌─────────────────────────────────────────────────────────┐
│  Page (e.g. /cashier, /medical-worklist, /laboratory)   │
│                                                          │
│    <QueueManagementFloater                              │
│      deptType="..."                                      │
│      onPrimary={(row) => ...}                           │
│    />                                                    │
└──────────────┬──────────────────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────────────────┐
│  QueueManagementFloater (FAB + state + actions hook)    │
│    • renders <Fab> with badge                           │
│    • mounts <QueueManagementPanel> when open            │
│    • dispatches actions via useQueueRowActions hook     │
└──────────────┬──────────────────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────────────────┐
│  QueueManagementPanel (presentation only)               │
│    • reads department_queues via Supabase realtime      │
│    • per-deptType defaults (title, primary label,       │
│      accent color) from DEPT_DEFAULTS                   │
│    • emits onAction(action, row) — caller dispatches    │
└─────────────────────────────────────────────────────────┘

3. Per-deptType defaults

DEPT_DEFAULTS in QueueManagementPanel.tsx provides language-aware defaults for each department. The FAB’s tooltip, accent color, and the primary action label all come from here:

dept_type Default title (TH) Default primary action Accent color
billing การจัดการคิวการเงิน ชำระเงิน #1976D2 (blue)
lab การจัดการคิวห้องปฏิบัติการ รับสิ่งส่งตรวจ #9C27B0 (purple)
imaging การจัดการคิวรังสีวิทยา รับ #00838F (teal)
pharmacy การจัดการคิวเภสัชกรรม จ่ายยา #388E3C (green)
consultation การจัดการคิวตรวจ เรียกตรวจ #1565C0 (deep blue)
screening การจัดการคิวคัดกรอง คัดกรอง #5E35B1 (deep purple)
medical_coder การจัดการคิวรหัสโรค รหัสโรค #6D4C41 (brown)

Override any of these via title, primaryActionLabel, or accentColor props.

4. Drop-in examples

4.1 Cashier (live)

import { QueueManagementFloater, type QueueManagementRow } from '@/common/components/queue-management';

<QueueManagementFloater
  deptType="billing"
  locationId={selectLocation?._id}
  userId={userId}
  language={i18n.language as 'th' | 'en'}
  onPrimary={(row) => openPaymentDialog(row)}
/>

4.2 Medical Worklist (consultation queue)

<QueueManagementFloater
  deptType="consultation"
  deptTypes={['consultation', 'screening']}  // broaden because orchestrator
                                              // doesn't always promote screening
                                              // to consultation
  userId={userId}
  onPrimary={(row) => navigate(`/patient-profile/${row.patient_id}`)}
/>

4.3 Laboratory

<QueueManagementFloater
  deptType="lab"
  onPrimary={(row) => openSpecimenReceive(row)}
  primaryActionLabel={{ th: 'รับสิ่งส่งตรวจ', en: 'Receive Specimen' }}
/>

4.4 Imaging / Diagnostics

<QueueManagementFloater
  deptType="imaging"
  onPrimary={(row) => openImagingAcknowledge(row)}
/>

4.5 Pharmacy

<QueueManagementFloater
  deptType="pharmacy"
  onPrimary={(row) => openDispenseDialog(row)}
/>

4.6 Custom dept_type with full overrides

<QueueManagementFloater
  deptType="bloodbank"  // not in DEPT_DEFAULTS — provide your own
  title={{ th: 'การจัดการคิวธนาคารเลือด', en: 'Blood Bank Queue' }}
  primaryActionLabel={{ th: 'จ่ายโลหิต', en: 'Issue Blood' }}
  accentColor="#D32F2F"
  onPrimary={(row) => openBloodIssueDialog(row)}
/>

4.7 External trigger (hideFab)

If your page has its own button to open the panel:

const [open, setOpen] = useState(false);

<Button onClick={() => setOpen(true)}>My Custom Trigger</Button>

<QueueManagementFloater
  deptType="lab"
  hideFab
  open={open}
  onClose={() => setOpen(false)}
  onPrimary={(row) => ...}
/>

5. Actions

The panel emits actions via the onAction callback. The floater handles the routine actions itself by calling useQueueRowActions, and routes primary / view-detail back to the caller via the onPrimary / onViewDetail props.

5.1 Caller-handled actions (you wire these)

Action ID What it does Who handles
primary Whatever your page wants — open a dialog, navigate, etc. Caller via onPrimary
view-detail Show row details (read-only) Caller via onViewDetail, falls back to onPrimary

5.2 Built-in lifecycle actions

Action ID What it does Status filter
recall recall_count++, set called_at ACTIVE only
hold status = ON_HOLD, store reason in metadata ACTIVE only
release-hold status = WAITING ON_HOLD only
complete status = COMPLETED ACTIVE only
cancel status = CANCELLED (destructive) WAITING / ON_HOLD

5.3 Queue manipulation actions (the “nurse cuts the queue” features)

These cover the three modes the cockpit/floater supports — inject to row (act on a specific patient), not inject (just call-next from the cockpit), and other:

Action ID What it does Status filter Use case
call-this Cut queue — set status = ACTIVE, assigned_to = userId, called_at = NOW(). Jumps this specific patient ahead of the call-next ordering. WAITING / ON_HOLD “Call patient H7 right now even though H1 is next” — cut queue
skip Push to back of queue — reset created_at = NOW() so this ticket sorts last under same-priority peers. Doesn’t deactivate or hold. WAITING / ACTIVE / ON_HOLD Patient stepped out briefly, others should move ahead
bump-stat priority = 'STAT' — top sort not COMPLETED/CANCELLED, current priority < STAT Emergency override
bump-urgent priority = 'URGENT' not COMPLETED/CANCELLED, current priority < URGENT Soft priority bump
bump-routine priority = 'ROUTINE' currently STAT or URGENT Reset after the urgency clears

Built-in actions try the backend /v2/medication/encounterJourney/queue/{action} endpoint first (calling call-specific, skip, bump-priority for the new ones), falling back to direct Supabase UPDATE if the endpoint isn’t reachable. The panel works even when the backend endpoints don’t exist yet.

6. Position presets

<QueueManagementFloater position="left-center" />   // ⭐ default — vertically centered, left edge
<QueueManagementFloater position="left-bottom" />   // bottom-left corner
<QueueManagementFloater position="left-top" />      // top-left (below app bar)
<QueueManagementFloater position="right-center" />  // right edge — only use when no per-row action buttons live there
<QueueManagementFloater position="right-bottom" />  // bottom-right corner (Material default)
<QueueManagementFloater position="right-top" />     // top-right

Why left-center is the default: in medOS the per-row action buttons (รายละเอียด / ชำระเงิน / Process Payment / etc.) live on the right side of every workstation table. A right-anchored FAB overlaps those buttons at the vertical mid-point of the table. Left-center keeps both surfaces clickable.

7. What it reads from Supabase

SELECT * FROM department_queues
 WHERE dept_type = $1                 -- or IN (deptTypes[])
   AND location_id = $2 (if provided)
 ORDER BY created_at DESC
 LIMIT 200;

Plus a real-time subscription to department_queues filtered by dept_type=eq.{deptType}.

The component never writes to encounter_journey_cache — it stays out of the manifest layer. The triggers (trg_sync_billing_queue, trg_propagate_patient_context) keep department_queues in sync.

8. Testing

Drop the floater into any page and check:

  1. FAB visible at right edge with badge showing count of (ON_HOLD + recall_count > 0) rows.
  2. Click FAB → panel slides in. FAB hides while panel is open (Zoom transition).
  3. Tabs show counts. Switching tabs filters the row list.
  4. Search narrows the list across HN/name/VN/queue#.
  5. 3-dot menu on each row shows actions appropriate for that status.
  6. Realtime — change a row in another tab/device, watch the panel update without manual refresh.
  7. Close → panel slides out, FAB returns.

The built-in actions (recall/hold/release/complete) work via Supabase fallback today. For a proper audit trail (who recalled, when, with what reason), the backend should expose:

POST /v2/medication/encounterJourney/queue/recall
POST /v2/medication/encounterJourney/queue/hold
POST /v2/medication/encounterJourney/queue/release-hold
POST /v2/medication/encounterJourney/queue/complete
POST /v2/medication/encounterJourney/queue/cancel

Each endpoint takes { ticket_id, user_id, service_channel, reason? } and emits a manifest.queue.workflow_transition event (which the orchestrator consumes to update clinical_context.queue_history).

When these land, no frontend changes needed — useQueueRowActions already prefers the backend path.

  • web/src/hooks/useQueueRowActions.ts — per-ticket-id actions (recall/hold/release/complete/cancel) with backend + Supabase fallback
  • web/src/common/components/queue-management/QueueManagementPanel.tsx — presentational panel
  • web/src/common/components/queue-management/QueueManagementFloater.tsx — FAB + panel + actions hook combined
  • web/packages/medical-kit/src/medical-worklist/workflow-config/components/QueueCockpit.tsx — the inline call-next/recall/hold cockpit (per-page, single active patient)
  • docs/architecture/encounter-orchestrator-triggers.md — master trigger reference (covers department_queues lifecycle)
Ask Anything