feature: add handoff report

This commit is contained in:
voltsrage
2026-06-23 20:32:33 +08:00
parent 79b6d9d85e
commit c6b98b5f7c
14 changed files with 906 additions and 7 deletions
@@ -0,0 +1,67 @@
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import HandoffReport from '@/components/ward/HandoffReport.vue'
const mockReport = {
generatedAt: '2026-06-23T08:00:00Z',
generatedBy: 'Test Nurse',
summary: {
department: 'ICU',
patientCount: 1,
criticalCount: 1,
alertCount: 2,
activeBundles: 1,
},
patients: [
{
encounterId: 'enc-1',
name: 'Jane Doe',
mrn: 'MRN001',
room: 'ICU-3',
department: 'ICU',
attending: 'Dr Smith',
news2: 8,
sofa: 6,
sofaDelta: 2,
gcs: 14,
qsofa: 2,
vitalsSummary: 'HR 110 bpm',
alertsSummary: 'NEWS2 Emergency',
pendingOrders: 'Blood cultures',
sbar: {
situation: 'Sepsis workup',
background: 'Allergies: Penicillin',
assessment: 'NEWS2 8',
recommendation: 'Blood cultures',
},
},
],
}
vi.mock('@/composables/handoffReport', async importOriginal => {
const actual = await importOriginal()
return {
...actual,
buildHandoffReport: vi.fn(() => Promise.resolve(mockReport)),
}
})
describe('HandoffReport', () => {
it('rendersWardSummaryAndPatientRows', async () => {
const wrapper = mount(HandoffReport, {
props: {
encounters: [{ encounterId: 'enc-1' }],
department: 'ICU',
generatedBy: 'Test Nurse',
},
attachTo: document.body,
})
await vi.waitFor(() => expect(document.body.textContent).toContain('Ward Summary'))
expect(document.body.textContent).toContain('Jane Doe')
expect(document.body.textContent).toContain('SBAR')
expect(document.body.textContent).toContain('Sepsis workup')
wrapper.unmount()
document.body.innerHTML = ''
})
})
@@ -0,0 +1,34 @@
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import VitalsEntryForm from '@/components/patient/VitalsEntryForm.vue'
describe('VitalsEntryForm', () => {
it('rendersVitalSignFields', () => {
const wrapper = mount(VitalsEntryForm)
expect(wrapper.text()).toContain('Heart Rate')
expect(wrapper.text()).toContain('SpO₂')
expect(wrapper.text()).toContain('AVPU')
})
it('emitsBatchObservationsOnSubmit', async () => {
const wrapper = mount(VitalsEntryForm)
await wrapper.findAll('input[type="number"]')[0].setValue('90')
await wrapper.find('select').setValue('0')
await wrapper.find('button').trigger('click')
const payload = wrapper.emitted('submit')?.[0]?.[0]
expect(payload).toHaveLength(2)
expect(payload).toEqual(expect.arrayContaining([
expect.objectContaining({ observationCode: 'HEART_RATE', value: 90 }),
expect.objectContaining({ observationCode: 'AVPU', value: 0 }),
]))
})
it('showsPlausibilityError', async () => {
const wrapper = mount(VitalsEntryForm)
await wrapper.findAll('input[type="number"]')[0].setValue('999')
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('submit')).toBeUndefined()
expect(wrapper.text()).toContain('Value must be between 1 and 300')
})
})
@@ -0,0 +1,93 @@
import { describe, it, expect } from 'vitest'
import {
buildPatientReport,
buildSbar,
buildWardSummary,
extractLatestVitals,
formatAlertsSummary,
formatVitalsSummary,
} from '@/composables/handoffReport'
const wardPatient = {
encounterId: 'enc-1',
firstName: 'Jane',
lastName: 'Doe',
mrn: 'MRN001',
roomBed: 'ICU-3',
department: 'ICU',
attendingPhysician: 'Dr Smith',
news2Score: 8,
sofaScore: 6,
sofaDelta: 2,
gcsScore: 14,
qsofaScore: 2,
openAlertCount: 1,
}
const enrichment = {
encounter: {
admissionReason: 'Sepsis workup',
patient: { allergies: 'Penicillin' },
},
openAlerts: [{ alertType: 'News2Emergency' }],
pendingOrders: [{ description: 'Blood cultures', status: 'Pending' }],
vitals: extractLatestVitals([
{ observationCode: 'HEART_RATE', value: 110, unit: 'bpm', recordedAt: '2026-06-23T10:00:00Z' },
{ observationCode: 'SPO2', value: 94, unit: '%', recordedAt: '2026-06-23T10:00:00Z' },
]),
bundle: null,
}
describe('handoffReport', () => {
it('buildsWardSummary', () => {
const summary = buildWardSummary(
[
wardPatient,
{ news2Score: 3, openAlertCount: 0, sepsisActive: true },
],
'ICU',
)
expect(summary).toMatchObject({
department: 'ICU',
patientCount: 2,
criticalCount: 1,
alertCount: 1,
activeBundles: 1,
})
})
it('formatsLatestVitals', () => {
expect(formatVitalsSummary(enrichment.vitals)).toContain('HR 110 bpm')
expect(formatVitalsSummary(enrichment.vitals)).toContain('SpO₂ 94%')
})
it('formatsAlertSummary', () => {
expect(formatAlertsSummary(enrichment.openAlerts)).toContain('NEWS2 Emergency')
})
it('buildsPatientReportWithSbar', () => {
const report = buildPatientReport(wardPatient, enrichment)
expect(report.name).toBe('Jane Doe')
expect(report.pendingOrders).toContain('Blood cultures')
expect(report.sbar.situation).toBe('Sepsis workup')
expect(report.sbar.background).toContain('Penicillin')
expect(report.sbar.assessment).toContain('NEWS2 8')
expect(report.sbar.recommendation).toContain('Blood cultures')
})
it('includesBundleInRecommendation', () => {
const sbar = buildSbar(wardPatient, enrichment.encounter, {
...enrichment,
pendingOrders: [],
bundle: {
complianceStatus: 'IN_PROGRESS',
elements: [
{ elementType: 'BloodCultures', status: 'Pending' },
{ elementType: 'SerumLactate', status: 'Completed' },
],
},
})
expect(sbar.recommendation).toContain('Sepsis bundle')
expect(sbar.recommendation).toContain('Blood cultures')
})
})
@@ -0,0 +1,51 @@
import { describe, it, expect } from 'vitest'
import {
buildVitalsObservations,
isPlausibleValue,
validateVitalsForm,
} from '@/composables/vitalsForm'
describe('vitalsForm', () => {
it('validatesPlausibleRanges', () => {
expect(isPlausibleValue('HEART_RATE', 78)).toBe(true)
expect(isPlausibleValue('HEART_RATE', 350)).toBe(false)
expect(isPlausibleValue('TEMP_C', 37.2)).toBe(true)
expect(isPlausibleValue('TEMP_C', 10)).toBe(false)
})
it('requiresAtLeastOneValue', () => {
const result = validateVitalsForm({
heartRate: '',
respRate: '',
systolicBp: '',
diastolicBp: '',
spo2: '',
tempC: '',
avpu: '',
})
expect(result.valid).toBe(false)
expect(result.errors._form).toBeTruthy()
})
it('buildsBatchObservations', () => {
const observations = buildVitalsObservations({
heartRate: '88',
respRate: '18',
systolicBp: '120',
diastolicBp: '80',
spo2: '97',
tempC: '37.1',
avpu: '0',
}, '2026-06-23T12:00:00Z')
expect(observations).toHaveLength(7)
expect(observations[0]).toMatchObject({
observationCode: 'HEART_RATE',
value: 88,
unit: 'bpm',
source: 'Manual',
recordedAt: '2026-06-23T12:00:00Z',
})
expect(observations.find(obs => obs.observationCode === 'AVPU')?.value).toBe(0)
})
})