feature: Clinical Review Mode: Charts, Replay Controls & Alert Reasoning
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { usePolling } from '@/composables/usePolling'
|
||||
import { useAlertStore } from '@/stores/alerts'
|
||||
import * as encountersApi from '@/api/encounters'
|
||||
import * as clinicalApi from '@/api/clinical'
|
||||
import VitalsPanel from '@/components/patient/VitalsPanel.vue'
|
||||
@@ -9,30 +11,49 @@ import ScoresPanel from '@/components/patient/ScoresPanel.vue'
|
||||
import AlertsList from '@/components/patient/AlertsList.vue'
|
||||
import OrdersPanel from '@/components/patient/OrdersPanel.vue'
|
||||
import SepsisBundlePanel from '@/components/patient/SepsisBundlePanel.vue'
|
||||
import TrendsGrid from '@/components/charts/TrendsGrid.vue'
|
||||
import News2History from '@/components/charts/News2History.vue'
|
||||
import ReplayControls from '@/components/replay/ReplayControls.vue'
|
||||
import AlertReasoning from '@/components/alerts/AlertReasoning.vue'
|
||||
import Skeleton from '@/components/ui/Skeleton.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const alertStore = useAlertStore()
|
||||
const { alerts } = storeToRefs(alertStore)
|
||||
|
||||
const encounter = ref(null)
|
||||
const loading = ref(true)
|
||||
const observations = ref([])
|
||||
const news2 = ref(null)
|
||||
const news2History = ref([])
|
||||
const medications = ref([])
|
||||
const sepsisBundle = ref(null)
|
||||
const orders = ref([])
|
||||
const selectedAlert = ref(null)
|
||||
let nextAlertIndex = 0
|
||||
|
||||
const openAlerts = computed(() =>
|
||||
alerts.value.filter(a => a.status === 'Open' || a.status === 'Escalated'),
|
||||
)
|
||||
|
||||
async function loadAll() {
|
||||
const id = route.params.encounterId
|
||||
loading.value = true
|
||||
try {
|
||||
const [enc, obs, n2, bundle, ord] = await Promise.all([
|
||||
const [enc, obs, n2, history, meds, bundle, ord] = await Promise.all([
|
||||
encountersApi.fetchEncounter(id),
|
||||
encountersApi.fetchObservations(id),
|
||||
clinicalApi.fetchCurrentNews2(id).catch(() => null),
|
||||
clinicalApi.fetchNews2History(id).catch(() => []),
|
||||
clinicalApi.fetchMedications(id).catch(() => []),
|
||||
clinicalApi.fetchSepsisBundle(id).catch(() => null),
|
||||
clinicalApi.fetchOrders(id).catch(() => ({ items: [] })),
|
||||
])
|
||||
encounter.value = enc
|
||||
observations.value = obs
|
||||
news2.value = n2
|
||||
news2History.value = history
|
||||
medications.value = meds
|
||||
sepsisBundle.value = bundle
|
||||
orders.value = ord.items ?? ord
|
||||
} finally {
|
||||
@@ -40,14 +61,32 @@ async function loadAll() {
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectAlert(alert) {
|
||||
selectedAlert.value = alert
|
||||
}
|
||||
|
||||
function jumpToNextAlert() {
|
||||
const sorted = [...openAlerts.value].sort(
|
||||
(a, b) => new Date(a.triggeredAt) - new Date(b.triggeredAt),
|
||||
)
|
||||
if (!sorted.length) return
|
||||
selectedAlert.value = sorted[nextAlertIndex % sorted.length]
|
||||
nextAlertIndex++
|
||||
document.getElementById('clinical-review')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
|
||||
usePolling(loadAll, 5_000)
|
||||
|
||||
watch(() => route.params.encounterId, loadAll)
|
||||
watch(() => route.params.encounterId, () => {
|
||||
selectedAlert.value = null
|
||||
nextAlertIndex = 0
|
||||
loadAll()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Skeleton v-if="loading && !encounter" :rows="6" />
|
||||
<div v-else-if="encounter" class="space-y-8">
|
||||
<div v-else-if="encounter" class="w-full min-w-0 space-y-8">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<RouterLink to="/ward" class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
← Ward
|
||||
@@ -57,15 +96,31 @@ watch(() => route.params.encounterId, loadAll)
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="grid min-w-0 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<ScoresPanel :news2="news2" :encounter="encounter" />
|
||||
<VitalsPanel :observations="observations" />
|
||||
<AlertsList :encounter-id="route.params.encounterId" />
|
||||
<AlertsList
|
||||
:encounter-id="route.params.encounterId"
|
||||
:selected-id="selectedAlert?.id"
|
||||
@select="onSelectAlert"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<AlertReasoning
|
||||
v-if="selectedAlert"
|
||||
:alert="selectedAlert"
|
||||
:medications="medications"
|
||||
/>
|
||||
|
||||
<div class="grid min-w-0 gap-4 lg:grid-cols-2">
|
||||
<OrdersPanel :orders="orders" />
|
||||
<SepsisBundlePanel v-if="sepsisBundle" :bundle="sepsisBundle" />
|
||||
</div>
|
||||
|
||||
<div id="clinical-review" class="w-full min-w-0 space-y-8">
|
||||
<TrendsGrid :observations="observations" />
|
||||
<News2History v-if="news2History.length" :history="news2History" />
|
||||
<ReplayControls :alerts="openAlerts" @jump-to-alert="jumpToNextAlert" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user