add frontend

This commit is contained in:
voltsrage
2026-06-19 17:42:09 +08:00
parent 49973271e5
commit 5f69ce1a95
53 changed files with 6591 additions and 0 deletions
@@ -0,0 +1,62 @@
<script setup>
import { ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useAlertStore } from '@/stores/alerts'
import { useSettingsStore } from '@/stores/settings'
import { alertStatusToApiFilter } from '@/api/normalize'
import AlertCard from '@/components/alerts/AlertCard.vue'
import AlertFilters from '@/components/alerts/AlertFilters.vue'
import AcknowledgeModal from '@/components/alerts/AcknowledgeModal.vue'
import Skeleton from '@/components/ui/Skeleton.vue'
import EmptyState from '@/components/ui/EmptyState.vue'
const alertStore = useAlertStore()
const settings = useSettingsStore()
const { alerts, loading } = storeToRefs(alertStore)
const activeFilter = ref('Open')
const confirmingAlert = ref(null)
watch(activeFilter, (status) => {
alertStore.loadGlobalAlerts(alertStatusToApiFilter(status))
}, { immediate: true })
async function handleAcknowledge() {
if (!confirmingAlert.value) return
await alertStore.acknowledge(confirmingAlert.value.id, settings.clinicianId)
confirmingAlert.value = null
alertStore.loadGlobalAlerts(alertStatusToApiFilter(activeFilter.value))
}
async function handleResolve(alertId) {
await alertStore.resolve(alertId)
alertStore.loadGlobalAlerts(alertStatusToApiFilter(activeFilter.value))
}
</script>
<template>
<div>
<h1 class="mb-4 text-xl font-bold dark:text-white">Alert Center</h1>
<AlertFilters v-model="activeFilter" />
<Skeleton v-if="loading && alerts.length === 0" class="mt-4" :rows="4" />
<EmptyState v-else-if="alerts.length === 0" class="mt-4" message="No alerts for this status" />
<div v-else class="mt-4 space-y-3">
<AlertCard
v-for="alert in alerts"
:key="alert.id"
:alert="alert"
@acknowledge="confirmingAlert = alert"
@resolve="handleResolve(alert.id)"
/>
</div>
<AcknowledgeModal
:open="!!confirmingAlert"
:alert="confirmingAlert"
@confirm="handleAcknowledge"
@close="confirmingAlert = null"
/>
</div>
</template>
@@ -0,0 +1,71 @@
<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { usePolling } from '@/composables/usePolling'
import * as encountersApi from '@/api/encounters'
import * as clinicalApi from '@/api/clinical'
import VitalsPanel from '@/components/patient/VitalsPanel.vue'
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 Skeleton from '@/components/ui/Skeleton.vue'
const route = useRoute()
const encounter = ref(null)
const loading = ref(true)
const observations = ref([])
const news2 = ref(null)
const sepsisBundle = ref(null)
const orders = ref([])
async function loadAll() {
const id = route.params.encounterId
loading.value = true
try {
const [enc, obs, n2, bundle, ord] = await Promise.all([
encountersApi.fetchEncounter(id),
encountersApi.fetchObservations(id),
clinicalApi.fetchCurrentNews2(id).catch(() => null),
clinicalApi.fetchSepsisBundle(id).catch(() => null),
clinicalApi.fetchOrders(id).catch(() => ({ items: [] })),
])
encounter.value = enc
observations.value = obs
news2.value = n2
sepsisBundle.value = bundle
orders.value = ord.items ?? ord
} finally {
loading.value = false
}
}
usePolling(loadAll, 5_000)
watch(() => route.params.encounterId, loadAll)
</script>
<template>
<Skeleton v-if="loading && !encounter" :rows="6" />
<div v-else-if="encounter" class="space-y-6">
<div class="flex 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">
&larr; Ward
</RouterLink>
<h1 class="text-xl font-bold dark:text-white">
{{ encounter.patient?.firstName }} {{ encounter.patient?.lastName }}
</h1>
</div>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<ScoresPanel :news2="news2" :encounter="encounter" />
<VitalsPanel :observations="observations" />
<AlertsList :encounter-id="route.params.encounterId" />
</div>
<div class="grid gap-4 md:grid-cols-2">
<OrdersPanel :orders="orders" />
<SepsisBundlePanel v-if="sepsisBundle" :bundle="sepsisBundle" />
</div>
</div>
</template>
@@ -0,0 +1,28 @@
<script setup>
import { storeToRefs } from 'pinia'
import { useWardStore } from '@/stores/ward'
import { usePolling } from '@/composables/usePolling'
import WardTable from '@/components/ward/WardTable.vue'
import Skeleton from '@/components/ui/Skeleton.vue'
import EmptyState from '@/components/ui/EmptyState.vue'
const wardStore = useWardStore()
const { sortedByRisk, loading, error, criticalCount } = storeToRefs(wardStore)
usePolling(() => wardStore.loadEncounters(), 10_000)
</script>
<template>
<div>
<div class="mb-4 flex items-center justify-between">
<h1 class="text-xl font-bold dark:text-white">Virtual Ward</h1>
<Badge v-if="criticalCount > 0" variant="critical">
{{ criticalCount }} critical
</Badge>
</div>
<Skeleton v-if="loading && sortedByRisk.length === 0" :rows="5" />
<EmptyState v-else-if="sortedByRisk.length === 0" message="No active patients" />
<WardTable v-else :patients="sortedByRisk" />
</div>
</template>