Triage Queue Dashboard
blockhealthcareEmergency Department (ED) and Urgent Care patient acuity triage queue and room allocation board: live unit header, 4 ESI overview cards with target wait thresholds, searchable patient queue table with vitals snapshot, assigned room, attending care team, and quick clinical actions.
Installation
$pnpm dlx uipkge-ng@latest add triage-queue-dashboard$npx uipkge-ng@latest add triage-queue-dashboard$yarn dlx uipkge-ng@latest add triage-queue-dashboard$bunx uipkge-ng@latest add triage-queue-dashboardManifest:
https://uipkge.dev/r/angular/triage-queue-dashboard.jsonInstalls to:src/app/components/blocks/triage-queue-dashboard/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
PatientVitalsinterface PatientVitals {
bp: string
hr: number
spo2: number
temp: string
respRate: number
}Caregiverinterface Caregiver {
name: string
role: 'RN' | 'MD' | 'PA' | 'NP'
initials: string
avatar?: string
}TriagePatientinterface TriagePatient {
id: string
mrn: string
name: string
age: number
gender: 'M' | 'F' | 'Other'
acuity: EsiLevel
acuityCode: string
acuityLabel: string
chiefComplaint: string
symptoms: string
waitTime: string
waitMinutes: number
vitals: PatientVitals
assignedRoom: string
roomType: TriageRoomType
primaryNurse: Caregiver
attendingPhysician: Caregiver
clinicalStatus: string
}AcuitySummaryCardinterface AcuitySummaryCard {
level: EsiLevel
title: string
subtitle: string
activeCount: number
targetWait: string
targetWindow: string
badgeVariantClass: string
indicatorDot?: string
pulse: boolean
description: string
}npm dependencies
Files installed (2)
src/app/components/blocks/triage-queue-dashboard/triage-queue-dashboard.component.ts34.6 kB
// Emergency Department (ED) triage queue and room allocation board: live unit header, // 4 clickable ESI overview cards with target wait thresholds, searchable patient queue // table with vitals snapshot, assigned room, attending care team, and quick clinical // actions. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { Bed, Clock, DoorOpen, HeartPulse, LucideAngularModule, MoreHorizontal, Plus, RefreshCw, Search, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, } from '@/ui/dropdown-menu/dropdown-menu.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export type EsiLevel = 'esi-1' | 'esi-2' | 'esi-3' | 'esi-4' export type TriageRoomType = 'trauma' | 'acute' | 'fast_track' | 'waiting' export type TriageSortBy = 'wait-desc' | 'wait-asc' | 'acuity' export interface PatientVitals { bp: string hr: number spo2: number temp: string respRate: number } export interface Caregiver { name: string role: 'RN' | 'MD' | 'PA' | 'NP' initials: string avatar?: string } export interface TriagePatient { id: string mrn: string name: string age: number gender: 'M' | 'F' | 'Other' acuity: EsiLevel acuityCode: string acuityLabel: string chiefComplaint: string symptoms: string waitTime: string waitMinutes: number vitals: PatientVitals assignedRoom: string roomType: TriageRoomType primaryNurse: Caregiver attendingPhysician: Caregiver clinicalStatus: string } export interface AcuitySummaryCard { level: EsiLevel title: string subtitle: string activeCount: number targetWait: string targetWindow: string badgeVariantClass: string indicatorDot?: string pulse: boolean description: string } const acuityCards: AcuitySummaryCard[] = [ { level: 'esi-1', title: 'ESI 1 · Resuscitation', subtitle: 'Immediate Life Threat', activeCount: 1, targetWait: 'Immediate', targetWindow: '0 min wait', badgeVariantClass: 'border-destructive/30 bg-destructive/10 text-destructive', indicatorDot: 'bg-destructive', pulse: true, description: 'Hemodynamic instability, active airway compromise', }, { level: 'esi-2', title: 'ESI 2 · Emergent', subtitle: 'High Risk / Severe Pain', activeCount: 3, targetWait: '< 15m wait', targetWindow: 'Max 15 min', badgeVariantClass: 'border-orange-500/30 bg-orange-500/10 text-orange-700 dark:text-orange-400', indicatorDot: 'bg-orange-500', pulse: false, description: 'Confused, lethargic, severe respiratory distress', }, { level: 'esi-3', title: 'ESI 3 · Urgent', subtitle: 'Multiple Resources Needed', activeCount: 8, targetWait: '< 30m wait', targetWindow: 'Max 30 min', badgeVariantClass: 'border-warning/30 bg-warning/10 text-warning', indicatorDot: 'bg-warning', pulse: false, description: 'Stable vitals, 2+ diagnostic or lab orders needed', }, { level: 'esi-4', title: 'ESI 4/5 · Non-Urgent', subtitle: 'Low Resource / Fast Track', activeCount: 6, targetWait: '< 60m wait', targetWindow: 'Max 60 min', badgeVariantClass: 'border-success/30 bg-success/10 text-success', indicatorDot: 'bg-success', pulse: false, description: 'Simple lacerations, routine medication re-eval', }, ] const defaultPatients: TriagePatient[] = [ { id: 'pt-1', mrn: 'MRN-94021', name: 'James Wilson', age: 62, gender: 'M', acuity: 'esi-1', acuityCode: 'ESI 1', acuityLabel: 'ESI 1 - Resuscitation', chiefComplaint: 'Severe Crushing Chest Pain', symptoms: 'Diaphoresis, radiating left jaw pain, dyspnea. STAT STEMI protocol initiated.', waitTime: '2 min', waitMinutes: 2, vitals: { bp: '88/54', hr: 124, spo2: 91, temp: '98.4°F', respRate: 28 }, assignedRoom: 'Trauma Bay 2', roomType: 'trauma', primaryNurse: { name: 'Sarah Jenkins, RN', role: 'RN', initials: 'SJ', avatar: 'https://images.unsplash.com/photo-1559839734-2b71ea197ec2?w=120&auto=format&fit=crop&q=80', }, attendingPhysician: { name: 'Dr. Marcus Chen, MD', role: 'MD', initials: 'MC', avatar: 'https://images.unsplash.com/photo-1622253692010-333f2da6031d?w=120&auto=format&fit=crop&q=80', }, clinicalStatus: 'Cath Lab Alert · In Bay', }, { id: 'pt-2', mrn: 'MRN-88319', name: 'Maria Garcia', age: 34, gender: 'F', acuity: 'esi-2', acuityCode: 'ESI 2', acuityLabel: 'ESI 2 - Emergent', chiefComplaint: 'Compound Tibia Fracture', symptoms: 'Post-motorcycle collision, visible bone deformity, uncontrolled bleeding controlled by tourniquet.', waitTime: '9 min', waitMinutes: 9, vitals: { bp: '138/86', hr: 106, spo2: 97, temp: '99.1°F', respRate: 22 }, assignedRoom: 'Trauma Bay 4', roomType: 'trauma', primaryNurse: { name: 'Kevin Vance, RN', role: 'RN', initials: 'KV', avatar: 'https://images.unsplash.com/photo-1537368910025-700350fe46c7?w=120&auto=format&fit=crop&q=80', }, attendingPhysician: { name: 'Dr. Lisa Thorne, MD', role: 'MD', initials: 'LT', avatar: 'https://images.unsplash.com/photo-1594824813637-e54e44e26210?w=120&auto=format&fit=crop&q=80', }, clinicalStatus: 'Ortho Paged · IV Analgesia', }, { id: 'pt-3', mrn: 'MRN-72915', name: 'Noah Miller', age: 8, gender: 'M', acuity: 'esi-2', acuityCode: 'ESI 2', acuityLabel: 'ESI 2 - Emergent', chiefComplaint: 'Acute Asthma Exacerbation', symptoms: 'Inspiratory and expiratory stridor, intercostal retractions, refractory to home albuterol.', waitTime: '14 min', waitMinutes: 14, vitals: { bp: '104/68', hr: 132, spo2: 92, temp: '99.8°F', respRate: 34 }, assignedRoom: 'Bed 06 (Peds Acute)', roomType: 'acute', primaryNurse: { name: 'Angela Ray, RN', role: 'RN', initials: 'AR', avatar: 'https://images.unsplash.com/photo-1582750433449-648ed127bb54?w=120&auto=format&fit=crop&q=80', }, attendingPhysician: { name: 'Dr. Marcus Chen, MD', role: 'MD', initials: 'MC', avatar: 'https://images.unsplash.com/photo-1622253692010-333f2da6031d?w=120&auto=format&fit=crop&q=80', }, clinicalStatus: 'Duoneb #2 · Continuous O2', }, { id: 'pt-4', mrn: 'MRN-51829', name: 'Sarah Lee', age: 45, gender: 'F', acuity: 'esi-3', acuityCode: 'ESI 3', acuityLabel: 'ESI 3 - Urgent', chiefComplaint: 'Right Lower Quadrant Abdominal Pain', symptoms: 'Severe pain x 14h, focal McBurney point tenderness, nausea, low-grade fever.', waitTime: '28 min', waitMinutes: 28, vitals: { bp: '126/82', hr: 92, spo2: 98, temp: '101.4°F', respRate: 18 }, assignedRoom: 'Bed 14', roomType: 'acute', primaryNurse: { name: 'David Park, RN', role: 'RN', initials: 'DP', avatar: 'https://images.unsplash.com/photo-1612349317150-e413f6a5b16d?w=120&auto=format&fit=crop&q=80', }, attendingPhysician: { name: 'Dr. Sophia Adams, MD', role: 'MD', initials: 'SA', avatar: 'https://images.unsplash.com/photo-1559839734-2b71ea197ec2?w=120&auto=format&fit=crop&q=80', }, clinicalStatus: 'CT Scan Queued · NPO', }, { id: 'pt-5', mrn: 'MRN-63910', name: 'Robert Chen', age: 58, gender: 'M', acuity: 'esi-3', acuityCode: 'ESI 3', acuityLabel: 'ESI 3 - Urgent', chiefComplaint: 'Suspected Renal Colic & Flank Pain', symptoms: 'Sudden onset severe right flank pain radiating to groin, gross hematuria, pain score 8/10.', waitTime: '42 min', waitMinutes: 42, vitals: { bp: '146/94', hr: 88, spo2: 99, temp: '98.4°F', respRate: 16 }, assignedRoom: 'Bed 18', roomType: 'acute', primaryNurse: { name: 'Rachel Torres, RN', role: 'RN', initials: 'RT', avatar: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=120&auto=format&fit=crop&q=80', }, attendingPhysician: { name: 'Dr. Sophia Adams, MD', role: 'MD', initials: 'SA', avatar: 'https://images.unsplash.com/photo-1559839734-2b71ea197ec2?w=120&auto=format&fit=crop&q=80', }, clinicalStatus: 'Ultrasound Ordered · IV Fluids', }, { id: 'pt-6', mrn: 'MRN-90423', name: 'Marcus Vance', age: 22, gender: 'M', acuity: 'esi-4', acuityCode: 'ESI 4', acuityLabel: 'ESI 4 - Non-Urgent', chiefComplaint: 'Forearm Deep Glass Laceration', symptoms: '4cm clean linear laceration, hemostasis achieved, neurovascular sensation intact distally.', waitTime: '54 min', waitMinutes: 54, vitals: { bp: '120/78', hr: 74, spo2: 100, temp: '98.6°F', respRate: 14 }, assignedRoom: 'Fast Track 02', roomType: 'fast_track', primaryNurse: { name: 'Kevin Vance, RN', role: 'RN', initials: 'KV', avatar: 'https://images.unsplash.com/photo-1537368910025-700350fe46c7?w=120&auto=format&fit=crop&q=80', }, attendingPhysician: { name: 'Dr. Emily Watson, MD', role: 'MD', initials: 'EW', avatar: 'https://images.unsplash.com/photo-1594824813637-e54e44e26210?w=120&auto=format&fit=crop&q=80', }, clinicalStatus: 'Wound Irrigated · Suture Tray Ready', }, ] const acuityOrder: Record<EsiLevel, number> = { 'esi-1': 1, 'esi-2': 2, 'esi-3': 3, 'esi-4': 4 } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-triage-queue-dashboard, [ui-triage-queue-dashboard]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, UiInputComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiSeparatorComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="triage-queue-dashboard" [class]="rootClass"> <!-- Live ED Header --> <header class="bg-card border-border rounded-xl border p-5 shadow-xs sm:p-6"> <div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2.5"> <div class="bg-primary/10 text-primary flex size-9 items-center justify-center rounded-lg"> <lucide-icon [img]="HeartPulse" class="size-5" /> </div> <div> <h1 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl"> Emergency Department Triage Queue </h1> <p class="text-muted-foreground text-xs font-medium sm:text-sm">{{ facilityName }}</p> </div> </div> <div class="flex flex-wrap items-center gap-2 pt-1 text-xs"> <span ui-badge variant="outline" class="border-border bg-muted/50 text-foreground gap-1.5 font-medium"> <lucide-icon [img]="Bed" class="text-primary size-3.5" /> {{ unitName }} </span> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1.5 font-medium"> <span class="bg-success size-1.5 animate-pulse rounded-full"></span> 18 In Unit · 4 Awaiting Triage </span> <span class="text-muted-foreground hidden items-center gap-1 sm:inline-flex"> <lucide-icon [img]="Clock" class="size-3" /> Live Sync · Updated seconds ago </span> </div> </div> <div class="flex flex-wrap items-center gap-2.5"> <button ui-button variant="outline" size="sm" class="gap-1.5 text-xs font-medium"> <lucide-icon [img]="RefreshCw" class="text-muted-foreground size-3.5" /> Refresh Queue </button> <button ui-button size="sm" class="gap-1.5 text-xs font-semibold shadow-xs"> <lucide-icon [img]="Plus" class="size-4" /> Admit New Patient </button> </div> </div> </header> <!-- 4 Acuity Overview Cards (ESI 1 to ESI 4/5) --> <section aria-label="Acuity Overview Cards" class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> @for (card of acuityCards; track card.level) { <div ui-card [class]="acuityCardClass(card)" (click)="handleAcuityCardClick(card.level)"> <div ui-card-content class="p-4 sm:p-5"> <div class="flex items-start justify-between gap-2"> <div class="space-y-1"> <div class="flex items-center gap-1.5"> <span [class]="acuityDotClass(card)"></span> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> {{ card.subtitle }} </span> </div> <h2 class="text-foreground text-sm font-semibold sm:text-base">{{ card.title }}</h2> </div> <span ui-badge variant="outline" [class]="'gap-1 px-2 py-0.5 text-xs font-semibold ' + card.badgeVariantClass" > @if (card.pulse) { <span class="bg-destructive size-1.5 animate-pulse rounded-full"></span> } {{ card.targetWait }} </span> </div> <div class="mt-4 flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl"> {{ card.activeCount }} </span> <span class="text-muted-foreground text-xs font-medium">active in queue</span> </div> <span class="text-muted-foreground text-xs tabular-nums">{{ card.targetWindow }}</span> </div> <p class="text-muted-foreground mt-2 text-xs leading-relaxed">{{ card.description }}</p> </div> </div> } </section> <!-- Patient Queue Table Card --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="border-border border-b p-4 sm:p-5"> <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <div> <div class="flex items-center gap-2"> <h3 ui-card-title class="text-foreground text-base font-semibold sm:text-lg"> Patient Triage & Allocation Board </h3> <span ui-badge variant="secondary" class="font-mono text-xs tabular-nums"> {{ filteredPatients().length }} Patients </span> </div> <p ui-card-description class="text-muted-foreground mt-0.5 text-xs"> Continuous monitoring of emergency severity index, vital signs, and room throughput. </p> </div> <div class="flex flex-wrap items-center gap-2.5"> <div class="relative w-full min-w-[200px] sm:w-60"> <lucide-icon [img]="Search" class="text-muted-foreground absolute top-1/2 left-2.5 size-4 -translate-y-1/2" /> <ui-input [value]="search()" (valueChange)="search.set($event)" type="text" placeholder="Search patient, MRN, bay..." class="h-8 pl-8 text-xs sm:text-sm" /> </div> <div class="w-36 sm:w-40"> <ui-select [value]="acuityFilter()" (valueChange)="setAcuityFilter($event)"> <button ui-select-trigger class="h-8 text-xs"> <ui-select-value placeholder="All Acuities" /> </button> <ui-select-content> <ui-select-item value="all">All Acuities</ui-select-item> <ui-select-item value="esi-1">ESI 1 - Resuscitation</ui-select-item> <ui-select-item value="esi-2">ESI 2 - Emergent</ui-select-item> <ui-select-item value="esi-3">ESI 3 - Urgent</ui-select-item> <ui-select-item value="esi-4">ESI 4/5 - Non-Urgent</ui-select-item> </ui-select-content> </ui-select> </div> <div class="w-32 sm:w-36"> <ui-select [value]="locationFilter()" (valueChange)="setLocationFilter($event)"> <button ui-select-trigger class="h-8 text-xs"> <ui-select-value placeholder="All Locations" /> </button> <ui-select-content> <ui-select-item value="all">All Locations</ui-select-item> <ui-select-item value="trauma">Trauma Bays</ui-select-item> <ui-select-item value="acute">Acute Beds</ui-select-item> <ui-select-item value="fast_track">Fast Track</ui-select-item> </ui-select-content> </ui-select> </div> @if (isFiltered()) { <button ui-button variant="ghost" size="sm" class="h-8 px-2 text-xs" (click)="resetFilters()"> Clear </button> } </div> </div> </div> <div ui-card-content class="p-0"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row class="hover:bg-transparent"> <th ui-table-head class="w-[140px] text-xs font-semibold">Acuity (ESI)</th> <th ui-table-head class="min-w-[200px] text-xs font-semibold">Patient Demographics</th> <th ui-table-head class="min-w-[240px] text-xs font-semibold">Chief Complaint & Symptoms</th> <th ui-table-head class="w-[110px] text-xs font-semibold">Wait Time</th> <th ui-table-head class="min-w-[190px] text-xs font-semibold">Vitals Snapshot</th> <th ui-table-head class="w-[150px] text-xs font-semibold">Bed / Room</th> <th ui-table-head class="min-w-[160px] text-xs font-semibold">Attending Staff</th> <th ui-table-head class="w-[60px] text-right text-xs font-semibold"> <span class="sr-only">Actions</span> </th> </tr> </thead> <tbody ui-table-body> @if (filteredPatients().length === 0) { <tr ui-table-row> <td ui-table-cell [attr.colspan]="8" class="h-48 text-center"> <div class="flex flex-col items-center justify-center gap-2"> <div class="bg-muted flex size-10 items-center justify-center rounded-full"> <lucide-icon [img]="Search" class="text-muted-foreground size-5" /> </div> <p class="text-foreground text-sm font-semibold">No patients match current filters</p> <p class="text-muted-foreground text-xs"> Try clearing your search query or selecting "All Acuities". </p> <button ui-button variant="outline" size="sm" class="mt-2 text-xs" (click)="resetFilters()"> Reset Filters </button> </div> </td> </tr> } @for (pt of filteredPatients(); track pt.id) { <tr ui-table-row [class]="patientRowClass(pt)"> <td ui-table-cell class="align-top font-medium"> <div class="flex flex-col items-start gap-1"> <span ui-badge variant="outline" [class]="acuityBadgeClass(pt)"> @if (pt.acuity === 'esi-1') { <span class="bg-destructive size-1.5 animate-pulse rounded-full"></span> } {{ pt.acuityCode }} </span> <span class="text-muted-foreground text-xs font-medium">{{ acuityLabel(pt.acuity) }}</span> </div> </td> <td ui-table-cell class="align-top"> <div class="space-y-1"> <div class="flex items-center gap-1.5"> <span class="text-foreground text-sm font-semibold tracking-tight">{{ pt.name }}</span> <span class="text-muted-foreground text-xs font-medium">({{ pt.age }}{{ pt.gender }})</span> </div> <div class="text-muted-foreground flex items-center gap-2 font-mono text-xs"> <span>{{ pt.mrn }}</span> </div> </div> </td> <td ui-table-cell class="align-top"> <div class="space-y-1"> <p class="text-foreground text-xs font-semibold">{{ pt.chiefComplaint }}</p> <p class="text-muted-foreground text-xs leading-relaxed">{{ pt.symptoms }}</p> </div> </td> <td ui-table-cell class="align-top"> <div class="flex items-center gap-1.5 pt-0.5"> <lucide-icon [img]="Clock" [class]="waitIconClass(pt)" /> <span [class]="waitTextClass(pt)">{{ pt.waitTime }}</span> </div> </td> <td ui-table-cell class="align-top"> <div class="grid grid-cols-2 gap-x-3 gap-y-1 text-xs"> <div class="flex items-center justify-between gap-1 font-mono"> <span class="text-muted-foreground">BP:</span> <span [class]="bpClass(pt)">{{ pt.vitals.bp }}</span> </div> <div class="flex items-center justify-between gap-1 font-mono"> <span class="text-muted-foreground">HR:</span> <span [class]="hrClass(pt)">{{ pt.vitals.hr }} bpm</span> </div> <div class="flex items-center justify-between gap-1 font-mono"> <span class="text-muted-foreground">SpO2:</span> <span [class]="spo2Class(pt)">{{ pt.vitals.spo2 }}%</span> </div> <div class="flex items-center justify-between gap-1 font-mono"> <span class="text-muted-foreground">Temp:</span> <span class="text-foreground font-semibold tabular-nums">{{ pt.vitals.temp }}</span> </div> </div> </td> <td ui-table-cell class="align-top"> <div class="space-y-1"> <span ui-badge variant="outline" class="border-border bg-muted/40 text-foreground gap-1 text-xs font-medium" > <lucide-icon [img]="Bed" class="text-primary size-3" /> {{ pt.assignedRoom }} </span> <p class="text-muted-foreground text-xs">{{ pt.clinicalStatus }}</p> </div> </td> <td ui-table-cell class="align-top"> <div class="space-y-1.5"> <div class="flex items-center gap-2"> <ui-avatar class="size-6"> <ui-avatar-image [src]="pt.primaryNurse.avatar" [alt]="pt.primaryNurse.name" /> <ui-avatar-fallback class="bg-primary/10 text-primary text-xs font-medium"> {{ pt.primaryNurse.initials }} </ui-avatar-fallback> </ui-avatar> <span class="text-foreground max-w-[130px] truncate text-xs leading-none font-medium" [attr.title]="pt.primaryNurse.name" > {{ pt.primaryNurse.name }} </span> </div> <div class="flex items-center gap-2"> <ui-avatar class="size-6"> <ui-avatar-image [src]="pt.attendingPhysician.avatar" [alt]="pt.attendingPhysician.name" /> <ui-avatar-fallback class="bg-secondary text-secondary-foreground text-xs font-medium"> {{ pt.attendingPhysician.initials }} </ui-avatar-fallback> </ui-avatar> <span class="text-muted-foreground max-w-[130px] truncate text-xs leading-none" [attr.title]="pt.attendingPhysician.name" > {{ pt.attendingPhysician.name }} </span> </div> </div> </td> <td ui-table-cell class="text-right align-top"> <ui-dropdown-menu> <button ui-button ui-dropdown-menu-trigger variant="ghost" size="icon-sm" class="size-8"> <lucide-icon [img]="MoreHorizontal" class="size-4" /> <span class="sr-only">Patient actions</span> </button> <ui-dropdown-menu-content align="end" class="w-48"> <ui-dropdown-menu-label>Triage Actions</ui-dropdown-menu-label> <ui-dropdown-menu-item> <lucide-icon [img]="Bed" class="size-4" /> <span>Assign Room</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item> <lucide-icon [img]="HeartPulse" class="size-4" /> <span>Re-triage Acuity</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item> <lucide-icon [img]="Zap" class="size-4" /> <span>Order STAT Labs</span> </ui-dropdown-menu-item> <ui-dropdown-menu-separator /> <ui-dropdown-menu-item> <lucide-icon [img]="DoorOpen" class="size-4" /> <span>Discharge / Transfer</span> </ui-dropdown-menu-item> </ui-dropdown-menu-content> </ui-dropdown-menu> </td> </tr> } </tbody> </table> </div> <div class="border-border bg-muted/20 text-muted-foreground flex flex-wrap items-center justify-between gap-3 border-t px-4 py-3 text-xs" > <div class="flex items-center gap-3 font-medium"> <span>Level 1 Trauma Alert Protocol Active</span> <div ui-separator orientation="vertical" class="h-3.5"></div> <span class="tabular-nums">Average Door-to-Triage: 4.2 min</span> </div> <div class="flex items-center gap-2"> <span class="text-foreground font-medium">ED Occupancy:</span> <span class="text-foreground font-semibold tabular-nums">88% (22/25 Beds)</span> </div> </div> </div> </div> </div> `, }) export class UiTriageQueueDashboardComponent { protected readonly Bed = Bed protected readonly Clock = Clock protected readonly DoorOpen = DoorOpen protected readonly HeartPulse = HeartPulse protected readonly MoreHorizontal = MoreHorizontal protected readonly Plus = Plus protected readonly RefreshCw = RefreshCw protected readonly Search = Search protected readonly Zap = Zap protected readonly acuityCards = acuityCards @Input('class') className?: string @Input() unitName = 'Level 1 Trauma Center · Bay 4' @Input() facilityName = 'St. Jude Metropolitan Academic Medical Center' @Input() set initialSearch(v: string | undefined) { this.search.set(v ?? '') } @Input() set initialAcuity(v: string | undefined) { if (v) this.acuityFilter.set(v) } @Input() set initialLocation(v: string | undefined) { if (v) this.locationFilter.set(v) } @Input() set initialPatients(v: TriagePatient[] | undefined) { if (v) this.patients.set(v) } readonly patients = signal<TriagePatient[]>(defaultPatients.map((p) => ({ ...p }))) readonly search = signal('') readonly acuityFilter = signal<string>('all') readonly locationFilter = signal<string>('all') readonly sortBy = signal<TriageSortBy>('acuity') readonly filteredPatients = computed(() => { const q = this.search().trim().toLowerCase() const acuity = this.acuityFilter() const location = this.locationFilter() const sort = this.sortBy() return this.patients() .filter((pt) => { if (q) { const matchesName = pt.name.toLowerCase().includes(q) const matchesMrn = pt.mrn.toLowerCase().includes(q) const matchesComplaint = pt.chiefComplaint.toLowerCase().includes(q) const matchesRoom = pt.assignedRoom.toLowerCase().includes(q) if (!matchesName && !matchesMrn && !matchesComplaint && !matchesRoom) return false } if (acuity !== 'all' && pt.acuity !== acuity) return false if (location !== 'all' && pt.roomType !== location) return false return true }) .sort((a, b) => { if (sort === 'wait-desc') return b.waitMinutes - a.waitMinutes if (sort === 'wait-asc') return a.waitMinutes - b.waitMinutes return acuityOrder[a.acuity] - acuityOrder[b.acuity] }) }) readonly isFiltered = computed( () => !!this.search() || this.acuityFilter() !== 'all' || this.locationFilter() !== 'all', ) get rootClass(): string { return cn('bg-background text-foreground flex w-full flex-col gap-6 font-sans', this.className) } acuityCardClass(card: AcuitySummaryCard): string { return cn( 'border-border relative cursor-pointer transition-colors duration-150 select-none hover:shadow-sm', this.acuityFilter() === card.level && 'ring-primary/40 bg-accent/40 ring-2', ) } acuityDotClass(card: AcuitySummaryCard): string { return cn('size-2 shrink-0 rounded-full', card.indicatorDot, card.pulse && 'animate-pulse') } acuityBadgeClass(pt: TriagePatient): string { return cn( 'gap-1.5 px-2.5 py-1 text-xs font-semibold whitespace-nowrap', pt.acuity === 'esi-1' && 'border-destructive/30 bg-destructive/10 text-destructive', pt.acuity === 'esi-2' && 'border-orange-500/30 bg-orange-500/10 text-orange-700 dark:text-orange-400', pt.acuity === 'esi-3' && 'border-warning/30 bg-warning/10 text-warning', pt.acuity === 'esi-4' && 'border-success/30 bg-success/10 text-success', ) } acuityLabel(level: EsiLevel): string { if (level === 'esi-1') return 'Resuscitation' if (level === 'esi-2') return 'Emergent' if (level === 'esi-3') return 'Urgent' return 'Non-Urgent' } patientRowClass(pt: TriagePatient): string { return cn('transition-colors', pt.acuity === 'esi-1' && 'bg-destructive/[0.03] dark:bg-destructive/10]') } waitIconClass(pt: TriagePatient): string { return cn('size-3.5 shrink-0', pt.waitMinutes > 30 ? 'text-warning' : 'text-muted-foreground') } waitTextClass(pt: TriagePatient): string { return cn('text-xs font-semibold tabular-nums', pt.waitMinutes > 40 ? 'text-warning' : 'text-foreground') } bpClass(pt: TriagePatient): string { return cn('font-semibold tabular-nums', pt.acuity === 'esi-1' ? 'text-destructive' : 'text-foreground') } hrClass(pt: TriagePatient): string { return cn('font-semibold tabular-nums', pt.vitals.hr > 110 ? 'text-destructive' : 'text-foreground') } spo2Class(pt: TriagePatient): string { return cn('font-semibold tabular-nums', pt.vitals.spo2 < 95 ? 'text-destructive' : 'text-foreground') } handleAcuityCardClick(level: EsiLevel): void { this.acuityFilter.set(this.acuityFilter() === level ? 'all' : level) } setAcuityFilter(value: string): void { this.acuityFilter.set(value) } setLocationFilter(value: string): void { this.locationFilter.set(value) } setSortBy(value: TriageSortBy): void { this.sortBy.set(value) } resetFilters(): void { this.search.set('') this.acuityFilter.set('all') this.locationFilter.set('all') this.sortBy.set('acuity') } }src/app/components/blocks/triage-queue-dashboard/index.ts0.2 kB
export { UiTriageQueueDashboardComponent, type AcuitySummaryCard, type Caregiver, type EsiLevel, type PatientVitals, type TriagePatient, type TriageRoomType, type TriageSortBy, } from './triage-queue-dashboard.component'
Raw manifest:https://uipkge.dev/r/angular/triage-queue-dashboard.json