feature: Improve dashboard functionality

This commit is contained in:
voltsrage
2026-06-23 20:19:32 +08:00
parent dd0fd88731
commit 79b6d9d85e
60 changed files with 2857 additions and 164 deletions
@@ -0,0 +1,67 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import AcknowledgeModal from '@/components/alerts/AcknowledgeModal.vue'
import { useAuthStore } from '@/stores/auth'
const alert = {
id: 'alert-1',
alertType: 'SofaSepsis',
severity: 'Critical',
status: 'Open',
details: 'SOFA delta >= 2',
}
describe('AcknowledgeModal', () => {
beforeEach(() => {
setActivePinia(createPinia())
const auth = useAuthStore()
auth.$patch({
user: {
userId: '11111111-1111-1111-1111-111111111111',
username: 'nurse.demo',
displayName: 'Demo Nurse',
role: 'NURSE',
},
})
})
it('showsAuthenticatedUserAndRoleContext', () => {
const wrapper = mount(AcknowledgeModal, {
props: { open: true, alert },
global: {
stubs: {
Modal: {
props: ['open', 'title'],
template: '<div v-if="open"><slot /></div>',
},
},
},
})
expect(wrapper.text()).toContain('Demo Nurse')
expect(wrapper.text()).toContain('Nurse')
expect(wrapper.text()).toContain('documenting awareness')
expect(wrapper.text()).toContain('[NURSE] Acknowledged by Demo Nurse.')
})
it('emitsOptionalNoteOnConfirm', async () => {
const wrapper = mount(AcknowledgeModal, {
props: { open: true, alert },
global: {
stubs: {
Modal: {
props: ['open', 'title'],
template: '<div v-if="open"><slot /></div>',
},
},
},
})
await wrapper.find('textarea').setValue('Will reassess in 30 minutes')
const ackButton = wrapper.findAll('button').find(button => button.text() === 'Acknowledge')
await ackButton.trigger('click')
expect(wrapper.emitted('confirm')).toEqual([['Will reassess in 30 minutes']])
})
})
@@ -36,6 +36,20 @@ describe('AlertCard', () => {
expect(wrapper.emitted('acknowledge')).toHaveLength(1)
})
it('showsAcknowledgedByWhenPresent', () => {
const wrapper = mount(AlertCard, {
props: {
alert: {
...openAlert,
status: 'Acknowledged',
acknowledgedBy: 'Demo Nurse (NURSE)',
acknowledgedAt: '2026-06-19T12:30:00Z',
},
},
})
expect(wrapper.text()).toContain('Acknowledged by Demo Nurse (Nurse)')
})
it('resolvedAlertHidesActions', () => {
const wrapper = mount(AlertCard, { props: { alert: resolvedAlert } })
const actionButtons = wrapper.findAll('button').filter(b => ['Acknowledge', 'Resolve'].includes(b.text()))
@@ -0,0 +1,35 @@
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import CriticalAlertBanner from '@/components/alerts/CriticalAlertBanner.vue'
import { useAlertStore } from '@/stores/alerts'
const { mockPush } = vi.hoisted(() => ({
mockPush: vi.fn(),
}))
vi.mock('vue-router', () => ({
useRouter: () => ({ push: mockPush }),
}))
describe('CriticalAlertBanner', () => {
it('rendersCriticalAlertsAndDismisses', async () => {
setActivePinia(createPinia())
const alertStore = useAlertStore()
alertStore.bannerAlerts = [{
id: 'alert-1',
alertType: 'SofaSepsis',
severity: 'Critical',
status: 'Open',
details: 'SOFA delta >= 2',
}]
const wrapper = mount(CriticalAlertBanner)
expect(wrapper.text()).toContain('Sepsis Alert (SOFA)')
expect(wrapper.text()).toContain('SOFA delta >= 2')
const dismissButton = wrapper.findAll('button').find(button => button.text() === 'Dismiss')
await dismissButton.trigger('click')
expect(alertStore.bannerAlerts).toEqual([])
})
})
@@ -0,0 +1,54 @@
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import DepartmentOverviewView from '@/views/DepartmentOverviewView.vue'
const { mockPush } = vi.hoisted(() => ({
mockPush: vi.fn(),
}))
vi.mock('vue-router', () => ({
useRouter: () => ({ push: mockPush }),
}))
vi.mock('@/composables/usePolling', () => ({
usePolling: (fn) => {
fn()
},
}))
vi.mock('@/api/encounters', () => ({
fetchAllActiveEncounters: vi.fn(() => Promise.resolve([
{
department: 'ICU',
news2Score: 8,
openAlertCount: 2,
sepsisActive: true,
sepsisBundleStatus: 'IN_PROGRESS',
},
{
department: 'GENERAL_MEDICINE',
news2Score: 3,
openAlertCount: 0,
sepsisActive: false,
},
])),
}))
vi.mock('@/api/analytics', () => ({
fetchAlertSummary: vi.fn(() => Promise.resolve({
summary: [{ department: 'ICU', total: 5 }],
})),
}))
describe('DepartmentOverviewView', () => {
it('rendersDepartmentCardsAndTotals', async () => {
setActivePinia(createPinia())
const wrapper = mount(DepartmentOverviewView)
await vi.waitFor(() => expect(wrapper.text()).toContain('1 active patient'))
expect(wrapper.text()).toContain('Department Overview')
expect(wrapper.text()).toContain('Critical (NEWS2 ≥ 7)')
expect(wrapper.text()).toContain('Alert volume')
})
})
@@ -0,0 +1,49 @@
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import SepsisBoardView from '@/views/SepsisBoardView.vue'
const { mockPush } = vi.hoisted(() => ({
mockPush: vi.fn(),
}))
vi.mock('vue-router', () => ({
useRouter: () => ({ push: mockPush }),
}))
vi.mock('@/composables/usePolling', () => ({
usePolling: () => {},
}))
const bundles = [
{
id: 'b1',
encounterId: 'enc-1',
firstName: 'Alice',
lastName: 'A',
mrn: 'M1',
department: 'Icu',
roomBed: '101',
recognizedAt: '2026-06-23T12:00:00Z',
deadlineAt: '2026-06-23T15:00:00Z',
complianceStatus: 'IN_PROGRESS',
elements: [
{ id: 'e1', elementType: 'BloodCultures', status: 'Completed' },
{ id: 'e2', elementType: 'SerumLactate', status: 'Pending' },
],
},
]
vi.mock('@/api/sepsis', () => ({
fetchSepsisBundles: vi.fn(() => Promise.resolve({ items: bundles, totalCount: 1 })),
}))
describe('SepsisBoardView', () => {
it('rendersBundleBoardWithSummary', async () => {
setActivePinia(createPinia())
const wrapper = mount(SepsisBoardView)
await vi.waitFor(() => expect(wrapper.text()).toContain('Alice A'))
expect(wrapper.text()).toContain('Sepsis Bundle Board')
expect(wrapper.text()).toContain('Serum lactate')
})
})
@@ -68,14 +68,19 @@ const patients = [
]
describe('WardTable', () => {
const sortProps = {
sortField: 'news2Score',
sortDirection: 'desc',
}
it('rendersAllPatientRows', () => {
const wrapper = mount(WardTable, { props: { patients } })
const wrapper = mount(WardTable, { props: { patients, ...sortProps } })
expect(wrapper.findAll('tbody tr')).toHaveLength(3)
})
it('emitsClickWithEncounterId', async () => {
mockPush.mockClear()
const wrapper = mount(WardTable, { props: { patients } })
const wrapper = mount(WardTable, { props: { patients, ...sortProps } })
await wrapper.findAll('tbody tr')[2].trigger('click')
expect(mockPush).toHaveBeenCalledWith({
name: 'PatientDetail',
@@ -84,18 +89,26 @@ describe('WardTable', () => {
})
it('showsCriticalBadgeForHighNews2', () => {
const wrapper = mount(WardTable, { props: { patients } })
const wrapper = mount(WardTable, { props: { patients, ...sortProps } })
const highRiskRow = wrapper.findAll('tbody tr')[2]
expect(highRiskRow.html()).toContain('text-severity-critical')
})
it('showsExtendedClinicalColumns', () => {
const wrapper = mount(WardTable, { props: { patients } })
const wrapper = mount(WardTable, { props: { patients, ...sortProps } })
const header = wrapper.find('thead').text()
expect(header).toContain('SOFA')
expect(header).toContain('GCS')
expect(header).toContain('Department')
expect(header).toContain('Last vitals')
expect(wrapper.text()).toContain('Dr. C')
expect(wrapper.text()).toContain('Δ+2')
})
it('emitsSortWhenHeaderClicked', async () => {
const wrapper = mount(WardTable, { props: { patients, ...sortProps } })
const roomHeader = wrapper.findAll('thead button').find(button => button.text().includes('Room'))
await roomHeader.trigger('click')
expect(wrapper.emitted('sort')).toEqual([['roomBed']])
})
})
@@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest'
import {
formatAcknowledgedByDisplay,
previewAcknowledgmentNote,
roleAcknowledgmentMessage,
} from '@/composables/alertAcknowledge'
describe('alertAcknowledge', () => {
it('buildsRoleAwareNotePreview', () => {
expect(previewAcknowledgmentNote('NURSE', 'Demo Nurse', 'Escalating to physician'))
.toBe('[NURSE] Acknowledged by Demo Nurse. Escalating to physician')
})
it('usesRoleSpecificAcknowledgmentMessage', () => {
expect(roleAcknowledgmentMessage('PHYSICIAN')).toContain('physician')
})
it('formatsAcknowledgedByDisplay', () => {
expect(formatAcknowledgedByDisplay('Demo Nurse (NURSE)')).toBe('Demo Nurse (Nurse)')
})
})
@@ -0,0 +1,48 @@
import { describe, it, expect } from 'vitest'
import { detectNewCriticalAlerts, isNotifiableCriticalAlert } from '@/composables/criticalAlertDetect'
const criticalOpen = {
id: 'a1',
severity: 'Critical',
status: 'Open',
alertType: 'SofaSepsis',
}
const criticalAcknowledged = {
id: 'a2',
severity: 'Critical',
status: 'Acknowledged',
alertType: 'News2Emergency',
}
const warningOpen = {
id: 'a3',
severity: 'Warning',
status: 'Open',
alertType: 'News2Warning',
}
describe('criticalAlertDetect', () => {
it('identifiesNotifiableCriticalAlerts', () => {
expect(isNotifiableCriticalAlert(criticalOpen)).toBe(true)
expect(isNotifiableCriticalAlert(criticalAcknowledged)).toBe(false)
expect(isNotifiableCriticalAlert(warningOpen)).toBe(false)
})
it('seedsSeenIdsOnFirstPoll', () => {
const result = detectNewCriticalAlerts([criticalOpen, warningOpen], [], false)
expect(result.newAlerts).toEqual([])
expect(result.nextSeenIds).toEqual(['a1'])
expect(result.seeded).toBe(true)
})
it('detectsNewCriticalAlertsAfterSeed', () => {
const seeded = detectNewCriticalAlerts([criticalOpen], [], false)
const next = detectNewCriticalAlerts(
[criticalOpen, { ...criticalOpen, id: 'a4', alertType: 'GcsCritical' }],
seeded.nextSeenIds,
seeded.seeded,
)
expect(next.newAlerts.map(alert => alert.id)).toEqual(['a4'])
})
})
@@ -0,0 +1,87 @@
import { describe, it, expect } from 'vitest'
import {
aggregateByDepartment,
news2AcuityTier,
normalizeDepartmentKey,
summarizeDepartments,
} from '@/composables/departmentFormat'
const encounters = [
{
department: 'ICU',
news2Score: 8,
openAlertCount: 2,
sepsisActive: true,
sepsisBundleStatus: 'IN_PROGRESS',
},
{
department: 'Icu',
news2Score: 5,
openAlertCount: 1,
sepsisActive: false,
sepsisBundleStatus: null,
},
{
department: 'GENERAL_MEDICINE',
news2Score: 2,
openAlertCount: 0,
sepsisActive: false,
sepsisBundleStatus: null,
},
{
department: 'Surgery',
news2Score: 6,
openAlertCount: 3,
sepsisActive: true,
sepsisBundleStatus: 'IN_PROGRESS',
},
]
describe('departmentFormat', () => {
it('normalizesDepartmentKeys', () => {
expect(normalizeDepartmentKey('Icu')).toBe('ICU')
expect(normalizeDepartmentKey('GeneralMedicine')).toBe('GENERAL_MEDICINE')
})
it('classifiesNews2Acuity', () => {
expect(news2AcuityTier(2)).toBe('low')
expect(news2AcuityTier(5)).toBe('medium')
expect(news2AcuityTier(7)).toBe('high')
})
it('aggregatesByDepartment', () => {
const departments = aggregateByDepartment(encounters, [
{ department: 'ICU', total: 10 },
{ department: 'SURGERY', total: 4 },
])
const icu = departments.find(d => d.key === 'ICU')
const surgery = departments.find(d => d.key === 'SURGERY')
const general = departments.find(d => d.key === 'GENERAL_MEDICINE')
expect(icu.patientCount).toBe(2)
expect(icu.acuity.high).toBe(1)
expect(icu.acuity.medium).toBe(1)
expect(icu.activeBundleCount).toBe(1)
expect(icu.openAlertCount).toBe(3)
expect(icu.averageNews2).toBe(6.5)
expect(icu.alertVolume).toBe(10)
expect(surgery.patientCount).toBe(1)
expect(surgery.activeBundleCount).toBe(1)
expect(surgery.alertVolume).toBe(4)
expect(general.patientCount).toBe(1)
expect(general.acuity.low).toBe(1)
})
it('summarizesTotals', () => {
const departments = aggregateByDepartment(encounters)
expect(summarizeDepartments(departments)).toEqual({
patientCount: 4,
criticalCount: 1,
activeBundleCount: 2,
openAlertCount: 6,
})
})
})
@@ -0,0 +1,56 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import {
bundleUrgency,
formatCountdown,
sortBundlesByUrgency,
summarizeBundles,
} from '@/composables/sepsisFormat'
const baseBundle = {
complianceStatus: 'IN_PROGRESS',
deadlineAt: '2026-06-23T15:00:00Z',
}
describe('sepsisFormat', () => {
const now = new Date('2026-06-23T14:00:00Z').getTime()
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(now)
})
afterEach(() => {
vi.useRealTimers()
})
it('classifiesUrgency', () => {
expect(bundleUrgency({ ...baseBundle, deadlineAt: '2026-06-23T15:00:00Z' }, now)).toBe('on_track')
expect(bundleUrgency({ ...baseBundle, deadlineAt: '2026-06-23T14:20:00Z' }, now)).toBe('at_risk')
expect(bundleUrgency({ ...baseBundle, deadlineAt: '2026-06-23T13:00:00Z' }, now)).toBe('overdue')
expect(bundleUrgency({ complianceStatus: 'NON_COMPLIANT', deadlineAt: '2026-06-23T15:00:00Z' }, now)).toBe('overdue')
})
it('formatsCountdown', () => {
expect(formatCountdown('2026-06-23T14:45:00Z', now)).toBe('45:00')
expect(formatCountdown('2026-06-23T13:00:00Z', now)).toBe('0:00')
})
it('sortsByUrgency', () => {
const bundles = [
{ id: 'a', complianceStatus: 'IN_PROGRESS', deadlineAt: '2026-06-23T15:00:00Z' },
{ id: 'b', complianceStatus: 'IN_PROGRESS', deadlineAt: '2026-06-23T13:00:00Z' },
{ id: 'c', complianceStatus: 'IN_PROGRESS', deadlineAt: '2026-06-23T14:20:00Z' },
]
const sorted = sortBundlesByUrgency(bundles, now)
expect(sorted.map(b => b.id)).toEqual(['b', 'c', 'a'])
})
it('summarizesBundles', () => {
const bundles = [
{ complianceStatus: 'IN_PROGRESS', deadlineAt: '2026-06-23T15:00:00Z' },
{ complianceStatus: 'IN_PROGRESS', deadlineAt: '2026-06-23T14:20:00Z' },
{ complianceStatus: 'IN_PROGRESS', deadlineAt: '2026-06-23T13:00:00Z' },
]
expect(summarizeBundles(bundles, now)).toEqual({ on_track: 1, at_risk: 1, overdue: 1 })
})
})
@@ -0,0 +1,39 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useAlertStore } from '@/stores/alerts'
describe('alert store notifications', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('queuesBannerAlertsForNewCriticalItems', () => {
const store = useAlertStore()
const critical = {
id: 'alert-1',
severity: 'Critical',
status: 'Open',
alertType: 'SofaSepsis',
}
expect(store.applyPollResults([critical])).toEqual([])
expect(store.bannerAlerts).toEqual([])
const next = {
id: 'alert-2',
severity: 'Critical',
status: 'Open',
alertType: 'GcsCritical',
}
const newAlerts = store.applyPollResults([critical, next])
expect(newAlerts.map(alert => alert.id)).toEqual(['alert-2'])
expect(store.bannerAlerts.map(alert => alert.id)).toEqual(['alert-2'])
})
it('dismissesBannerAlert', () => {
const store = useAlertStore()
store.bannerAlerts = [{ id: 'alert-1' }]
store.dismissBannerAlert('alert-1')
expect(store.bannerAlerts).toEqual([])
})
})
@@ -0,0 +1,49 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useSettingsStore } from '@/stores/settings'
import { useWardStore } from '@/stores/ward'
describe('ward store sort', () => {
beforeEach(() => {
localStorage.clear()
setActivePinia(createPinia())
})
it('persistsSortPreference', () => {
const settings = useSettingsStore()
const ward = useWardStore()
ward.setSort('name')
expect(settings.wardSortField).toBe('name')
expect(settings.wardSortDirection).toBe('asc')
expect(localStorage.getItem('wardSortField')).toBe('name')
ward.setSort('name')
expect(settings.wardSortDirection).toBe('desc')
expect(localStorage.getItem('wardSortDirection')).toBe('desc')
})
})
describe('ward store filters', () => {
beforeEach(() => {
localStorage.clear()
setActivePinia(createPinia())
})
it('filtersEncountersForDisplay', () => {
const ward = useWardStore()
ward.encounters = [
{ encounterId: '1', firstName: 'Alice', lastName: 'A', mrn: 'M1', news2Score: 8, openAlertCount: 1 },
{ encounterId: '2', firstName: 'Bob', lastName: 'B', mrn: 'M2', news2Score: 3, openAlertCount: 0 },
]
ward.toggleFilter('critical')
expect(ward.displayEncounters.map(e => e.encounterId)).toEqual(['1'])
ward.toggleFilter('hasAlerts')
expect(ward.displayEncounters.map(e => e.encounterId)).toEqual(['1'])
ward.clearFilters()
expect(ward.displayEncounters.map(e => e.encounterId)).toEqual(['1', '2'])
})
})
@@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest'
import { filterEncounters, matchesPatientSearch } from '@/composables/wardFilter'
const patients = [
{
encounterId: '1',
firstName: 'Alice',
lastName: 'Anderson',
mrn: 'MRN-100',
news2Score: 8,
openAlertCount: 2,
sepsisActive: true,
sepsisBundleStatus: 'IN_PROGRESS',
},
{
encounterId: '2',
firstName: 'Bob',
lastName: 'Baker',
mrn: 'MRN-200',
news2Score: 4,
openAlertCount: 0,
sepsisActive: false,
},
{
encounterId: '3',
firstName: 'Carol',
lastName: 'Clark',
mrn: 'MRN-300',
news2Score: 7,
openAlertCount: 1,
sepsisActive: false,
sepsisBundleStatus: 'IN_PROGRESS',
},
]
describe('wardFilter', () => {
it('matchesNameOrMrn', () => {
expect(matchesPatientSearch(patients[0], 'alice')).toBe(true)
expect(matchesPatientSearch(patients[0], 'MRN-100')).toBe(true)
expect(matchesPatientSearch(patients[0], 'anderson')).toBe(true)
expect(matchesPatientSearch(patients[0], 'xyz')).toBe(false)
})
it('filtersBySearch', () => {
const result = filterEncounters(patients, { search: 'baker' })
expect(result.map(p => p.encounterId)).toEqual(['2'])
})
it('filtersByHasAlerts', () => {
const result = filterEncounters(patients, { hasAlerts: true })
expect(result.map(p => p.encounterId)).toEqual(['1', '3'])
})
it('filtersBySepsisActive', () => {
const result = filterEncounters(patients, { sepsisActive: true })
expect(result.map(p => p.encounterId)).toEqual(['1', '3'])
})
it('filtersByCriticalNews2', () => {
const result = filterEncounters(patients, { critical: true })
expect(result.map(p => p.encounterId)).toEqual(['1', '3'])
})
it('combinesSearchAndFilters', () => {
const result = filterEncounters(patients, {
search: 'carol',
hasAlerts: true,
critical: true,
})
expect(result.map(p => p.encounterId)).toEqual(['3'])
})
})
@@ -0,0 +1,78 @@
import { describe, it, expect, beforeEach } from 'vitest'
import {
defaultSortDirection,
sortEncounters,
} from '@/composables/wardSort'
const patients = [
{
encounterId: '1',
firstName: 'Bob',
lastName: 'B',
roomBed: '202',
department: 'SURGERY',
news2Score: 5,
qsofaScore: 1,
sepsisActive: false,
openAlertCount: 1,
},
{
encounterId: '2',
firstName: 'Alice',
lastName: 'A',
roomBed: '101',
department: 'ICU',
news2Score: 8,
qsofaScore: 2,
sepsisActive: true,
sepsisBundleStatus: 'IN_PROGRESS',
openAlertCount: 3,
},
{
encounterId: '3',
firstName: 'Carol',
lastName: 'C',
roomBed: '103',
department: 'GENERAL_MEDICINE',
news2Score: 2,
qsofaScore: 0,
sepsisActive: false,
openAlertCount: 0,
},
]
describe('wardSort', () => {
beforeEach(() => {
localStorage.clear()
})
it('defaultsTextFieldsToAscending', () => {
expect(defaultSortDirection('name')).toBe('asc')
expect(defaultSortDirection('news2Score')).toBe('desc')
})
it('sortsByNews2DescendingByDefault', () => {
const sorted = sortEncounters(patients, 'news2Score', 'desc')
expect(sorted.map(p => p.encounterId)).toEqual(['2', '1', '3'])
})
it('sortsByPatientNameAscending', () => {
const sorted = sortEncounters(patients, 'name', 'asc')
expect(sorted.map(p => p.firstName)).toEqual(['Alice', 'Bob', 'Carol'])
})
it('sortsByRoomBedAscending', () => {
const sorted = sortEncounters(patients, 'roomBed', 'asc')
expect(sorted.map(p => p.roomBed)).toEqual(['101', '103', '202'])
})
it('sortsByOpenAlertCountDescending', () => {
const sorted = sortEncounters(patients, 'openAlertCount', 'desc')
expect(sorted.map(p => p.encounterId)).toEqual(['2', '1', '3'])
})
it('sortsBySepsisStatusDescending', () => {
const sorted = sortEncounters(patients, 'sepsis', 'desc')
expect(sorted[0].encounterId).toBe('2')
})
})