Queue Management Floater
Reusable FAB + side-panel for queue management on any page, with per-deptType defaults and per-row actions.
Status: Live Component:
web/src/common/components/queue-management/QueueManagementFloater.tsxUsed 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_queueschanges 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:
- FAB visible at right edge with badge showing count of
(ON_HOLD + recall_count > 0)rows. - Click FAB → panel slides in. FAB hides while panel is open (Zoom transition).
- Tabs show counts. Switching tabs filters the row list.
- Search narrows the list across HN/name/VN/queue#.
- 3-dot menu on each row shows actions appropriate for that status.
- Realtime — change a row in another tab/device, watch the panel update without manual refresh.
- Close → panel slides out, FAB returns.
9. Backend endpoints (optional but recommended)
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.
10. Related
web/src/hooks/useQueueRowActions.ts— per-ticket-id actions (recall/hold/release/complete/cancel) with backend + Supabase fallbackweb/src/common/components/queue-management/QueueManagementPanel.tsx— presentational panelweb/src/common/components/queue-management/QueueManagementFloater.tsx— FAB + panel + actions hook combinedweb/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 (coversdepartment_queueslifecycle)