import React from 'react'; import type { Patient } from 'fhir/r4'; import { Video } from 'lucide-react'; import type { RowStatusTone } from '../../../components/ui/row-meta'; import type { ActionDescriptor, QueueItem } from './encounterLifecycle'; import { appointmentIsVirtual, encounterIsVirtual } from '../../../utils/encounterClass'; // Shared focus-panel display helpers - used by both EncounterFocusPanel (provider tier) and // SupportFocusPanel (support tier). Kept here so the two panels can't drift out of sync. /** Button-variant `btn` class mapping for a focus panel's action bar. */ export const BTN_CLASS: Record = { go: 'btn btn-green', primary: 'btn btn-blue', // Was hand-rolled Tailwind borders, which rendered as no border at all: `#...root .btn` sets // `border: 1px solid transparent` at (1,1,0) and a utility class scores (0,1,0), so the colour // silently lost and these actions drew as plain text. `.btn-blue-ghost` is ID-scoped like the // other variants, and blue-ghost is the standard for secondary acts since grey was retired. outline: 'btn btn-blue-ghost', amber: 'btn bg-amber-100 text-amber-800 border border-amber-300 hover:bg-amber-200', }; /** "48 - M" - age in years + sex initial, from a Patient (empty string when unknown). */ export const ageSex = (patient?: Patient): string => { const sex = patient?.gender ? patient.gender[0].toUpperCase() : ''; if (!patient?.birthDate) return sex; const age = Math.floor((Date.now() - new Date(patient.birthDate).getTime()) / (365.25 * 864e5)); return [String(age), sex].filter(Boolean).join(' · '); }; /** * WHICH KIND OF VISIT THIS IS, beside which stage it is at. * * A row that says only "In progress" does not tell a clinician whether the patient is in a room * down the corridor or waiting in a meeting window, and those are different next actions. Read from * the Encounter's own class rather than re-derived from the appointment's location, for the same * reason the video door is: the class is the settled answer and it is settled from two directions. * * Absent rather than "In person" when it is not virtual. Naming the ordinary case on every row buys * nothing and costs the exception its contrast, which is the whole job of this chip. */ export const ModalityBadge: React.FC<{ item: QueueItem; virtualLocationId?: string }> = ({ item, virtualLocationId }) => { if (!queueItemIsVirtual(item, virtualLocationId)) return null; return ( ); }; // A row with no Encounter yet is still a video visit when its booking names the virtual Location. export const queueItemIsVirtual = (item: QueueItem, virtualLocationId?: string): boolean => item.encounter ? encounterIsVirtual(item.encounter) : appointmentIsVirtual(item.appointment, virtualLocationId); const QUEUE_STATUS: Record = { 'in-progress': { label: 'In progress', tone: 'attention' }, onleave: { label: 'On leave', tone: 'plain' }, triaged: { label: 'Roomed · ready', tone: 'done' }, arrived: { label: 'Checked in', tone: 'done' }, finished: { label: 'Finished', tone: 'plain' }, scheduled: { label: 'Scheduled', tone: 'plain' }, }; /* The header pill's tone for each row tone - the same tones the working chart's header uses (b4030fa1d). */ const PILL_TONE: Record = { attention: 'ak-pill--warn', done: 'ak-pill--ok', ended: '', danger: 'ak-pill--bad', plain: '', }; /** * A visit's state in a focus panel header: the SHARED pill, as the deck draws it and as the working chart's * header already does (P06, Captain + Codex 2026-09-30). On leave reads as info there too. Queue rows keep * their coloured words (`RowStatus`); only the header changed. */ export const VisitStatus: React.FC<{ item: QueueItem }> = ({ item }) => { const { label, tone } = QUEUE_STATUS[item.status] ?? QUEUE_STATUS.scheduled; const toneClass = item.status === 'onleave' ? 'ak-pill--info' : PILL_TONE[tone]; return {label}; };