update readme
do Ward table missing critical clinical columns
This commit is contained in:
@@ -16,9 +16,16 @@ const patients = [
|
||||
firstName: 'Alice',
|
||||
lastName: 'A',
|
||||
mrn: 'M1',
|
||||
room: '101',
|
||||
roomBed: '101',
|
||||
status: 'Active',
|
||||
news2Score: 3,
|
||||
sofaScore: 4,
|
||||
sofaDelta: 0,
|
||||
gcsScore: 15,
|
||||
qsofaScore: 0,
|
||||
attendingPhysician: 'Dr. A',
|
||||
admittedAt: '2026-06-21T10:00:00Z',
|
||||
lastObservationAt: '2026-06-23T13:00:00Z',
|
||||
sepsisActive: false,
|
||||
openAlertCount: 0,
|
||||
},
|
||||
@@ -27,9 +34,16 @@ const patients = [
|
||||
firstName: 'Bob',
|
||||
lastName: 'B',
|
||||
mrn: 'M2',
|
||||
room: '102',
|
||||
roomBed: '102',
|
||||
status: 'Active',
|
||||
news2Score: 5,
|
||||
sofaScore: 6,
|
||||
sofaDelta: 1,
|
||||
gcsScore: 12,
|
||||
qsofaScore: 1,
|
||||
attendingPhysician: 'Dr. B',
|
||||
admittedAt: '2026-06-22T10:00:00Z',
|
||||
lastObservationAt: '2026-06-23T10:00:00Z',
|
||||
sepsisActive: false,
|
||||
openAlertCount: 1,
|
||||
},
|
||||
@@ -38,9 +52,16 @@ const patients = [
|
||||
firstName: 'Carol',
|
||||
lastName: 'C',
|
||||
mrn: 'M3',
|
||||
room: '103',
|
||||
roomBed: '103',
|
||||
status: 'Active',
|
||||
news2Score: 8,
|
||||
sofaScore: 10,
|
||||
sofaDelta: 2,
|
||||
gcsScore: 8,
|
||||
qsofaScore: 2,
|
||||
attendingPhysician: 'Dr. C',
|
||||
admittedAt: '2026-06-20T10:00:00Z',
|
||||
lastObservationAt: '2026-06-23T08:00:00Z',
|
||||
sepsisActive: true,
|
||||
openAlertCount: 3,
|
||||
},
|
||||
@@ -67,4 +88,14 @@ describe('WardTable', () => {
|
||||
const highRiskRow = wrapper.findAll('tbody tr')[2]
|
||||
expect(highRiskRow.html()).toContain('text-severity-critical')
|
||||
})
|
||||
|
||||
it('showsExtendedClinicalColumns', () => {
|
||||
const wrapper = mount(WardTable, { props: { patients } })
|
||||
const header = wrapper.find('thead').text()
|
||||
expect(header).toContain('SOFA')
|
||||
expect(header).toContain('GCS')
|
||||
expect(header).toContain('Last vitals')
|
||||
expect(wrapper.text()).toContain('Dr. C')
|
||||
expect(wrapper.text()).toContain('Δ+2')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import {
|
||||
formatLengthOfStay,
|
||||
observationStaleness,
|
||||
patientRoom,
|
||||
stalenessClass,
|
||||
} from '@/composables/wardFormat'
|
||||
|
||||
describe('wardFormat', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date('2026-06-23T14:00:00Z'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('formatsLengthOfStay', () => {
|
||||
expect(formatLengthOfStay('2026-06-21T14:00:00Z')).toBe('2d')
|
||||
expect(formatLengthOfStay('2026-06-23T10:00:00Z')).toBe('4h')
|
||||
})
|
||||
|
||||
it('detectsStaleObservations', () => {
|
||||
expect(observationStaleness('2026-06-23T13:00:00Z', 'Active')).toBe('fresh')
|
||||
expect(observationStaleness('2026-06-23T10:00:00Z', 'Active')).toBe('stale')
|
||||
expect(observationStaleness('2026-06-23T08:00:00Z', 'Active')).toBe('critical')
|
||||
expect(observationStaleness(null, 'Active')).toBe('missing')
|
||||
expect(observationStaleness('2026-06-23T08:00:00Z', 'Discharged')).toBeNull()
|
||||
})
|
||||
|
||||
it('mapsStalenessClasses', () => {
|
||||
expect(stalenessClass('stale')).toContain('amber')
|
||||
expect(stalenessClass('critical')).toContain('red')
|
||||
})
|
||||
|
||||
it('usesRoomBedWithFallback', () => {
|
||||
expect(patientRoom({ roomBed: 'ICU-1' })).toBe('ICU-1')
|
||||
expect(patientRoom({ room: '101' })).toBe('101')
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,14 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Badge from '@/components/ui/Badge.vue'
|
||||
import {
|
||||
formatGcsClassification,
|
||||
formatLastObservation,
|
||||
formatLengthOfStay,
|
||||
observationStaleness,
|
||||
patientRoom,
|
||||
stalenessClass,
|
||||
} from '@/composables/wardFormat'
|
||||
|
||||
const props = defineProps({ patient: { type: Object, required: true } })
|
||||
|
||||
@@ -10,6 +18,10 @@ const riskVariant = computed(() => {
|
||||
if (score >= 5) return 'warning'
|
||||
return 'success'
|
||||
})
|
||||
|
||||
const vitalsStaleness = computed(() =>
|
||||
observationStaleness(props.patient.lastObservationAt, props.patient.status),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -20,7 +32,7 @@ const riskVariant = computed(() => {
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Room {{ patient.room ?? '—' }}
|
||||
Room {{ patientRoom(patient) }}
|
||||
</span>
|
||||
<Badge v-if="patient.sepsisActive" variant="critical" size="xs">Sepsis</Badge>
|
||||
</div>
|
||||
@@ -28,17 +40,50 @@ const riskVariant = computed(() => {
|
||||
{{ patient.firstName }} {{ patient.lastName }}
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ patient.mrn }}</p>
|
||||
<p v-if="patient.attendingPhysician" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ patient.attendingPhysician }}
|
||||
</p>
|
||||
</div>
|
||||
<Badge :variant="riskVariant">NEWS2 {{ patient.news2Score ?? '—' }}</Badge>
|
||||
</div>
|
||||
|
||||
<dl class="mt-4 grid grid-cols-2 gap-4 border-t border-gray-100 pt-4 dark:border-gray-800">
|
||||
<div>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">SOFA</dt>
|
||||
<dd class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ patient.sofaScore ?? '—' }}
|
||||
<span v-if="patient.sofaDelta != null && patient.sofaDelta > 0" class="text-xs text-amber-600 dark:text-amber-400">
|
||||
Δ+{{ patient.sofaDelta }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">GCS</dt>
|
||||
<dd class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ patient.gcsScore ?? '—' }}
|
||||
<span v-if="patient.gcsClassification" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ formatGcsClassification(patient.gcsClassification) }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">qSOFA</dt>
|
||||
<dd class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ patient.qsofaScore ?? '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">LOS</dt>
|
||||
<dd class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ formatLengthOfStay(patient.admittedAt) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">Last vitals</dt>
|
||||
<dd class="mt-2 text-sm font-medium" :class="stalenessClass(vitalsStaleness)">
|
||||
{{ formatLastObservation(patient.lastObservationAt) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">Open alerts</dt>
|
||||
<dd class="mt-2">
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Badge from '@/components/ui/Badge.vue'
|
||||
import {
|
||||
formatGcsClassification,
|
||||
formatLastObservation,
|
||||
formatLengthOfStay,
|
||||
observationStaleness,
|
||||
patientRoom,
|
||||
stalenessClass,
|
||||
} from '@/composables/wardFormat'
|
||||
|
||||
const props = defineProps({ patient: { type: Object, required: true } })
|
||||
|
||||
@@ -10,12 +18,31 @@ const riskVariant = computed(() => {
|
||||
if (score >= 5) return 'warning'
|
||||
return 'success'
|
||||
})
|
||||
|
||||
const sofaVariant = computed(() => {
|
||||
const delta = props.patient.sofaDelta
|
||||
if (delta != null && delta >= 2) return 'critical'
|
||||
if (delta != null && delta === 1) return 'warning'
|
||||
return 'info'
|
||||
})
|
||||
|
||||
const gcsVariant = computed(() => {
|
||||
const score = props.patient.gcsScore
|
||||
if (score == null) return 'info'
|
||||
if (score <= 8) return 'critical'
|
||||
if (score <= 12) return 'warning'
|
||||
return 'success'
|
||||
})
|
||||
|
||||
const vitalsStaleness = computed(() =>
|
||||
observationStaleness(props.patient.lastObservationAt, props.patient.status),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ patient.room ?? '—' }}
|
||||
{{ patientRoom(patient) }}
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
@@ -26,9 +53,34 @@ const riskVariant = computed(() => {
|
||||
<td class="whitespace-nowrap px-4 py-4 text-right">
|
||||
<Badge :variant="riskVariant">{{ patient.news2Score ?? '—' }}</Badge>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-4 text-right text-sm">
|
||||
<div class="flex flex-col items-end gap-1">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ patient.sofaScore ?? '—' }}</span>
|
||||
<Badge v-if="patient.sofaDelta != null && patient.sofaDelta > 0" :variant="sofaVariant" size="xs">
|
||||
Δ+{{ patient.sofaDelta }}
|
||||
</Badge>
|
||||
</div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-4 text-right text-sm">
|
||||
<div class="flex flex-col items-end gap-1">
|
||||
<Badge :variant="gcsVariant">{{ patient.gcsScore ?? '—' }}</Badge>
|
||||
<span v-if="patient.gcsClassification" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ formatGcsClassification(patient.gcsClassification) }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-4 text-right text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ patient.qsofaScore ?? '—' }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ patient.attendingPhysician ?? '—' }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ formatLengthOfStay(patient.admittedAt) }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-4 text-sm" :class="stalenessClass(vitalsStaleness)">
|
||||
{{ formatLastObservation(patient.lastObservationAt) }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-4">
|
||||
<Badge v-if="patient.sepsisActive" variant="critical">Active</Badge>
|
||||
<span v-else class="text-sm text-gray-400">No</span>
|
||||
@@ -40,4 +92,4 @@ const riskVariant = computed(() => {
|
||||
<span v-else class="text-sm text-gray-400">0</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -30,7 +30,12 @@ function goToPatient(encounterId) {
|
||||
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Room</th>
|
||||
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Patient</th>
|
||||
<th class="px-4 py-4 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">NEWS2</th>
|
||||
<th class="px-4 py-4 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">SOFA</th>
|
||||
<th class="px-4 py-4 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">GCS</th>
|
||||
<th class="px-4 py-4 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">qSOFA</th>
|
||||
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Attending</th>
|
||||
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">LOS</th>
|
||||
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Last vitals</th>
|
||||
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Sepsis</th>
|
||||
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Alerts</th>
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
const STALE_MS = 2 * 60 * 60 * 1000
|
||||
const CRITICAL_STALE_MS = 4 * 60 * 60 * 1000
|
||||
|
||||
export function formatLengthOfStay(admittedAt, asOf = new Date()) {
|
||||
if (!admittedAt) return '—'
|
||||
const start = new Date(admittedAt).getTime()
|
||||
if (Number.isNaN(start)) return '—'
|
||||
|
||||
const diffMs = Math.max(0, asOf.getTime() - start)
|
||||
const days = Math.floor(diffMs / (24 * 60 * 60 * 1000))
|
||||
if (days >= 1) return `${days}d`
|
||||
const hours = Math.floor(diffMs / (60 * 60 * 1000))
|
||||
if (hours >= 1) return `${hours}h`
|
||||
return '<1h'
|
||||
}
|
||||
|
||||
export function observationStaleness(lastObservationAt, status, asOf = new Date()) {
|
||||
if (status && status !== 'Active') return null
|
||||
if (!lastObservationAt) return 'missing'
|
||||
|
||||
const ageMs = asOf.getTime() - new Date(lastObservationAt).getTime()
|
||||
if (ageMs > CRITICAL_STALE_MS) return 'critical'
|
||||
if (ageMs > STALE_MS) return 'stale'
|
||||
return 'fresh'
|
||||
}
|
||||
|
||||
export function formatLastObservation(lastObservationAt) {
|
||||
if (!lastObservationAt) return 'No vitals'
|
||||
return new Date(lastObservationAt).toLocaleString([], {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
export function stalenessClass(level) {
|
||||
if (level === 'critical') return 'text-red-600 dark:text-red-400'
|
||||
if (level === 'stale') return 'text-amber-600 dark:text-amber-400'
|
||||
if (level === 'missing') return 'text-gray-400 dark:text-gray-500'
|
||||
return 'text-gray-700 dark:text-gray-300'
|
||||
}
|
||||
|
||||
export function formatGcsClassification(classification) {
|
||||
if (!classification) return ''
|
||||
const map = {
|
||||
MILD: 'Mild',
|
||||
MODERATE: 'Mod',
|
||||
SEVERE: 'Severe',
|
||||
}
|
||||
return map[classification] ?? classification
|
||||
}
|
||||
|
||||
export function patientRoom(patient) {
|
||||
return patient.roomBed ?? patient.room ?? '—'
|
||||
}
|
||||
Reference in New Issue
Block a user