fix: Replay controls
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ref, watch, computed, onBeforeUnmount } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { usePolling } from '@/composables/usePolling'
|
||||
import { useReplayControls } from '@/composables/useReplayControls'
|
||||
import { useAlertStore } from '@/stores/alerts'
|
||||
import * as encountersApi from '@/api/encounters'
|
||||
import * as clinicalApi from '@/api/clinical'
|
||||
@@ -20,6 +21,7 @@ import Skeleton from '@/components/ui/Skeleton.vue'
|
||||
const route = useRoute()
|
||||
const alertStore = useAlertStore()
|
||||
const { alerts } = storeToRefs(alertStore)
|
||||
const replay = useReplayControls()
|
||||
|
||||
const encounter = ref(null)
|
||||
const loading = ref(true)
|
||||
@@ -36,6 +38,33 @@ const openAlerts = computed(() =>
|
||||
alerts.value.filter(a => a.status === 'Open' || a.status === 'Escalated'),
|
||||
)
|
||||
|
||||
const replayObservations = computed(() =>
|
||||
observations.value.filter(o => replay.isAtOrBefore(o.recordedAt)),
|
||||
)
|
||||
|
||||
const replayNews2History = computed(() =>
|
||||
news2History.value.filter(h => replay.isAtOrBefore(h.calculatedAt)),
|
||||
)
|
||||
|
||||
function collectScenarioTimes() {
|
||||
return [
|
||||
...observations.value.map(o => new Date(o.recordedAt).getTime()),
|
||||
...news2History.value.map(h => new Date(h.calculatedAt).getTime()),
|
||||
...alerts.value.map(a => new Date(a.triggeredAt).getTime()),
|
||||
].filter(Number.isFinite)
|
||||
}
|
||||
|
||||
function syncReplayBounds() {
|
||||
const times = collectScenarioTimes()
|
||||
if (!times.length) return
|
||||
|
||||
const atEnd = replay.currentMs.value >= replay.scenarioEndMs.value - 1_000
|
||||
replay.setScenarioBounds(Math.min(...times), Math.max(...times))
|
||||
if (atEnd || replay.scenarioEndMs.value === replay.scenarioStartMs.value) {
|
||||
replay.syncToEnd()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
const id = route.params.encounterId
|
||||
loading.value = true
|
||||
@@ -49,6 +78,7 @@ async function loadAll() {
|
||||
clinicalApi.fetchSepsisBundle(id).catch(() => null),
|
||||
clinicalApi.fetchOrders(id).catch(() => ({ items: [] })),
|
||||
])
|
||||
await alertStore.loadAlerts(id)
|
||||
encounter.value = enc
|
||||
observations.value = obs
|
||||
news2.value = n2
|
||||
@@ -56,6 +86,7 @@ async function loadAll() {
|
||||
medications.value = meds
|
||||
sepsisBundle.value = bundle
|
||||
orders.value = ord.items ?? ord
|
||||
syncReplayBounds()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -63,6 +94,7 @@ async function loadAll() {
|
||||
|
||||
function onSelectAlert(alert) {
|
||||
selectedAlert.value = alert
|
||||
replay.jumpToTimestamp(alert.triggeredAt)
|
||||
}
|
||||
|
||||
function jumpToNextAlert() {
|
||||
@@ -70,8 +102,13 @@ function jumpToNextAlert() {
|
||||
(a, b) => new Date(a.triggeredAt) - new Date(b.triggeredAt),
|
||||
)
|
||||
if (!sorted.length) return
|
||||
selectedAlert.value = sorted[nextAlertIndex % sorted.length]
|
||||
|
||||
const alert = sorted[nextAlertIndex % sorted.length]
|
||||
selectedAlert.value = alert
|
||||
nextAlertIndex++
|
||||
replay.jumpToTimestamp(alert.triggeredAt)
|
||||
|
||||
document.getElementById(`alert-row-${alert.id}`)?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
document.getElementById('clinical-review')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
|
||||
@@ -80,8 +117,20 @@ usePolling(loadAll, 5_000)
|
||||
watch(() => route.params.encounterId, () => {
|
||||
selectedAlert.value = null
|
||||
nextAlertIndex = 0
|
||||
replay.pause()
|
||||
loadAll()
|
||||
})
|
||||
|
||||
watch(openAlerts, (list) => {
|
||||
nextAlertIndex = 0
|
||||
if (selectedAlert.value && !list.some(a => a.id === selectedAlert.value.id)) {
|
||||
selectedAlert.value = null
|
||||
}
|
||||
})
|
||||
|
||||
watch([observations, news2History, alerts], syncReplayBounds, { deep: true })
|
||||
|
||||
onBeforeUnmount(() => replay.stopPlayback())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -98,7 +147,7 @@ watch(() => route.params.encounterId, () => {
|
||||
|
||||
<div class="grid min-w-0 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<ScoresPanel :news2="news2" :encounter="encounter" />
|
||||
<VitalsPanel :observations="observations" />
|
||||
<VitalsPanel :observations="replayObservations" />
|
||||
<AlertsList
|
||||
:encounter-id="route.params.encounterId"
|
||||
:selected-id="selectedAlert?.id"
|
||||
@@ -118,9 +167,19 @@ watch(() => route.params.encounterId, () => {
|
||||
</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" />
|
||||
<TrendsGrid :observations="replayObservations" />
|
||||
<News2History v-if="replayNews2History.length" :history="replayNews2History" />
|
||||
<ReplayControls
|
||||
:alerts="openAlerts"
|
||||
:is-paused="replay.isPaused"
|
||||
:progress="replay.progress"
|
||||
:formatted-time="replay.formattedTime"
|
||||
:speed="replay.speed"
|
||||
@pause="replay.pause()"
|
||||
@resume="replay.resume()"
|
||||
@set-speed="replay.setSpeed"
|
||||
@jump-to-alert="jumpToNextAlert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user