fix styling to be more responsive and clean
This commit is contained in:
@@ -4,7 +4,16 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>vigilcare-dashboard</title>
|
||||
<title>VigilCare Clinical</title>
|
||||
<script>
|
||||
;(function () {
|
||||
try {
|
||||
if (localStorage.getItem('darkMode') === 'true') {
|
||||
document.documentElement.classList.add('dark')
|
||||
}
|
||||
} catch (_) {}
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -34,7 +34,7 @@ function formatTime(iso) {
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Badge :variant="severityVariant(alert.severity)">{{ alert.severity }}</Badge>
|
||||
@@ -43,7 +43,7 @@ function formatTime(iso) {
|
||||
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ alertTypeLabel(alert.alertType) }}
|
||||
</h3>
|
||||
<p v-if="alert.details" class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p v-if="alert.details" class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ alert.details }}
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-500">
|
||||
@@ -51,7 +51,7 @@ function formatTime(iso) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="showActions(alert.status)" class="flex shrink-0 gap-2">
|
||||
<div v-if="showActions(alert.status)" class="flex w-full shrink-0 gap-2 sm:w-auto">
|
||||
<Button
|
||||
v-if="canAcknowledge(alert.status)"
|
||||
size="sm"
|
||||
|
||||
@@ -10,12 +10,13 @@ const tabs = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="-mx-4 overflow-x-auto px-4 sm:mx-0 sm:px-0">
|
||||
<div class="flex min-w-max gap-2 border-b border-gray-200 dark:border-gray-700 sm:min-w-0 sm:flex-wrap">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
type="button"
|
||||
class="border-b-2 px-4 py-2 text-sm font-medium transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||
class="shrink-0 border-b-2 px-4 py-2 text-sm font-medium transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||
:class="activeFilter === tab.value
|
||||
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
|
||||
@@ -24,4 +25,5 @@ const tabs = [
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,94 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useWardStore } from '@/stores/ward'
|
||||
import { useDarkMode } from '@/composables/useDarkMode'
|
||||
|
||||
const route = useRoute()
|
||||
const wardStore = useWardStore()
|
||||
const { department } = storeToRefs(wardStore)
|
||||
const { darkMode, toggle } = useDarkMode()
|
||||
|
||||
const pageTitle = computed(() => route.meta.title ?? 'VigilCare')
|
||||
|
||||
const departments = [
|
||||
{ value: '', label: 'All departments' },
|
||||
{ value: 'ICU', label: 'ICU' },
|
||||
{ value: 'GENERAL_MEDICINE', label: 'General Medicine' },
|
||||
{ value: 'SURGERY', label: 'Surgery' },
|
||||
]
|
||||
|
||||
function onDepartmentChange(event) {
|
||||
const value = event.target.value || null
|
||||
wardStore.setDepartment(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="flex h-14 items-center border-b border-gray-200 bg-white px-4 dark:border-gray-800 dark:bg-gray-900">
|
||||
<h1 class="text-lg font-semibold text-gray-900 dark:text-gray-100">VigilCare Clinical</h1>
|
||||
<header class="sticky top-0 z-30 border-b border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="flex h-16 items-center gap-4 px-4 lg:px-8">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
VigilCare Clinical
|
||||
</p>
|
||||
<h1 class="truncate text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2 sm:gap-4">
|
||||
<label v-if="route.name === 'WardDashboard'" class="hidden sm:block">
|
||||
<span class="sr-only">Department filter</span>
|
||||
<select
|
||||
:value="department ?? ''"
|
||||
class="rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
@change="onDepartmentChange"
|
||||
>
|
||||
<option v-for="dept in departments" :key="dept.value" :value="dept.value">
|
||||
{{ dept.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-600 transition duration-200 hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
:aria-label="darkMode ? 'Switch to light mode' : 'Switch to dark mode'"
|
||||
@click="toggle"
|
||||
>
|
||||
<svg
|
||||
v-if="darkMode"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<script setup>
|
||||
import AppHeader from './AppHeader.vue'
|
||||
import AppSidebar from './AppSidebar.vue'
|
||||
import MobileNav from './MobileNav.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-screen bg-gray-50 dark:bg-gray-950">
|
||||
<div class="flex min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||
<AppSidebar />
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<AppHeader />
|
||||
<main class="flex-1 overflow-y-auto p-4 lg:p-6">
|
||||
<main class="flex-1 overflow-y-auto p-4 pb-24 lg:p-8 lg:pb-8">
|
||||
<div class="mx-auto w-full max-w-7xl">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<MobileNav />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,13 +1,68 @@
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const links = [
|
||||
{ to: '/ward', label: 'Virtual Ward', icon: 'ward' },
|
||||
{ to: '/alerts', label: 'Alert Center', icon: 'alerts' },
|
||||
]
|
||||
|
||||
function linkClasses(path) {
|
||||
const active = route.path.startsWith(path)
|
||||
return active
|
||||
? 'bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-300'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="hidden w-64 border-r border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900 lg:block">
|
||||
<div class="flex h-14 items-center px-4">
|
||||
<span class="text-lg font-bold text-gray-900 dark:text-gray-100">VC</span>
|
||||
<aside class="hidden w-64 shrink-0 border-r border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900 lg:flex lg:flex-col">
|
||||
<div class="flex h-16 items-center px-8">
|
||||
<span class="text-lg font-bold text-gray-900 dark:text-gray-100">VigilCare</span>
|
||||
</div>
|
||||
<nav class="px-2 py-4">
|
||||
<slot />
|
||||
<nav class="flex-1 px-4 py-4" aria-label="Main navigation">
|
||||
<ul class="space-y-2">
|
||||
<li v-for="link in links" :key="link.to">
|
||||
<RouterLink
|
||||
:to="link.to"
|
||||
class="flex items-center gap-4 rounded-lg px-4 py-2 text-sm font-medium transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||
:class="linkClasses(link.to)"
|
||||
>
|
||||
<svg
|
||||
v-if="link.icon === 'ward'"
|
||||
class="h-6 w-6 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 10h16M4 14h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-6 w-6 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
{{ link.label }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const links = [
|
||||
{ to: '/ward', label: 'Ward', icon: 'ward' },
|
||||
{ to: '/alerts', label: 'Alerts', icon: 'alerts' },
|
||||
]
|
||||
|
||||
const activePath = computed(() => route.path)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
class="fixed inset-x-0 bottom-0 z-40 border-t border-gray-200 bg-white lg:hidden dark:border-gray-800 dark:bg-gray-900"
|
||||
aria-label="Mobile navigation"
|
||||
>
|
||||
<ul class="flex h-16 items-stretch">
|
||||
<li v-for="link in links" :key="link.to" class="flex-1">
|
||||
<RouterLink
|
||||
:to="link.to"
|
||||
class="flex h-full flex-col items-center justify-center gap-2 px-4 py-2 text-xs font-medium transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500"
|
||||
:class="activePath.startsWith(link.to)
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
|
||||
>
|
||||
<svg
|
||||
v-if="link.icon === 'ward'"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 10h16M4 14h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
{{ link.label }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -41,7 +41,7 @@ async function resolve(alertId) {
|
||||
<template>
|
||||
<Card>
|
||||
<template #header>
|
||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Active Alerts
|
||||
</h2>
|
||||
</template>
|
||||
@@ -51,7 +51,7 @@ async function resolve(alertId) {
|
||||
<li
|
||||
v-for="alert in alerts"
|
||||
:key="alert.id"
|
||||
class="flex items-start justify-between gap-3 py-3 first:pt-0 last:pb-0"
|
||||
class="flex flex-col gap-4 py-4 first:pt-0 last:pb-0 sm:flex-row sm:items-start sm:justify-between"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@@ -60,7 +60,7 @@ async function resolve(alertId) {
|
||||
{{ alertTypeLabel(alert.alertType) }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="alert.details" class="mt-1 truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
<p v-if="alert.details" class="mt-2 truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ alert.details }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ function statusVariant(status) {
|
||||
<template>
|
||||
<Card>
|
||||
<template #header>
|
||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Orders
|
||||
</h2>
|
||||
</template>
|
||||
@@ -40,7 +40,7 @@ function statusVariant(status) {
|
||||
<li
|
||||
v-for="order in pendingOrders"
|
||||
:key="order.id"
|
||||
class="flex items-center justify-between gap-3 py-2 first:pt-0"
|
||||
class="flex items-center justify-between gap-4 py-2 first:pt-0"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm text-gray-900 dark:text-white">{{ order.description }}</div>
|
||||
@@ -59,7 +59,7 @@ function statusVariant(status) {
|
||||
:key="order.id"
|
||||
class="py-2 first:pt-0"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm text-gray-900 dark:text-white">{{ order.description }}</div>
|
||||
<div v-if="order.resultSummary" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
|
||||
@@ -40,7 +40,7 @@ const qsofaVariant = computed(() => {
|
||||
<template>
|
||||
<Card>
|
||||
<template #header>
|
||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Clinical Scores
|
||||
</h2>
|
||||
</template>
|
||||
@@ -48,7 +48,7 @@ const qsofaVariant = computed(() => {
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">NEWS2</div>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<Badge :variant="news2Variant">{{ news2?.totalScore ?? '—' }}</Badge>
|
||||
<span v-if="news2?.riskLevel" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ news2.riskLevel }}
|
||||
@@ -57,7 +57,7 @@ const qsofaVariant = computed(() => {
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">qSOFA</div>
|
||||
<div class="mt-1">
|
||||
<div class="mt-2">
|
||||
<Badge :variant="qsofaVariant">{{ qsofa?.activeCriteria ?? '—' }}</Badge>
|
||||
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">/ 3 criteria</span>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@ function elementComplete(element) {
|
||||
<template>
|
||||
<Card>
|
||||
<template #header>
|
||||
<div class="mb-3 flex items-center justify-between gap-2">
|
||||
<div class="mb-4 flex flex-wrap items-center justify-between gap-4">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Sepsis Bundle
|
||||
</h2>
|
||||
@@ -56,7 +56,7 @@ function elementComplete(element) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="mb-4 flex items-center justify-between rounded-lg bg-gray-50 p-3 dark:bg-gray-800">
|
||||
<div class="mb-4 flex items-center justify-between rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300">Time to deadline</span>
|
||||
<span
|
||||
class="font-mono text-lg font-semibold"
|
||||
@@ -70,10 +70,10 @@ function elementComplete(element) {
|
||||
<li
|
||||
v-for="element in bundle.elements"
|
||||
:key="element.id"
|
||||
class="flex items-center gap-3 rounded-lg border border-gray-200 p-3 dark:border-gray-700"
|
||||
class="flex items-center gap-4 rounded-lg border border-gray-200 p-4 dark:border-gray-700"
|
||||
>
|
||||
<span
|
||||
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-xs"
|
||||
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs"
|
||||
:class="elementComplete(element)
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
|
||||
: 'bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500'"
|
||||
|
||||
@@ -24,16 +24,16 @@ const latestByCode = computed(() => {
|
||||
<template>
|
||||
<Card>
|
||||
<template #header>
|
||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Latest Vitals
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div v-if="latestByCode.length" class="grid grid-cols-2 gap-3">
|
||||
<div v-if="latestByCode.length" class="grid grid-cols-2 gap-4 sm:grid-cols-3">
|
||||
<div
|
||||
v-for="obs in latestByCode"
|
||||
:key="obs.id ?? obs.observationCode"
|
||||
class="rounded-lg bg-gray-50 p-3 dark:bg-gray-800"
|
||||
class="rounded-lg bg-gray-50 p-4 dark:bg-gray-800"
|
||||
>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ observationCodeLabel(obs.observationCode) }}
|
||||
|
||||
@@ -9,7 +9,7 @@ const props = defineProps({
|
||||
|
||||
const classes = computed(() => {
|
||||
const base = 'inline-flex items-center font-medium rounded-full'
|
||||
const sizes = { xs: 'px-1.5 py-0.5 text-xs', sm: 'px-2 py-0.5 text-xs', md: 'px-2.5 py-1 text-sm' }
|
||||
const sizes = { xs: 'px-2 py-2 text-xs', sm: 'px-2 py-2 text-xs', md: 'px-4 py-2 text-sm' }
|
||||
const variants = {
|
||||
critical: 'bg-severity-critical/10 text-severity-critical dark:bg-severity-critical-dark/20 dark:text-red-300',
|
||||
warning: 'bg-severity-warning/10 text-severity-warning dark:bg-severity-warning-dark/20 dark:text-amber-300',
|
||||
|
||||
@@ -25,9 +25,9 @@ const classes = computed(() => {
|
||||
const base =
|
||||
'inline-flex items-center justify-center font-medium rounded-lg transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none'
|
||||
const sizes = {
|
||||
sm: 'px-3 py-1.5 text-xs',
|
||||
sm: 'px-4 py-2 text-xs',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-5 py-2.5 text-base',
|
||||
lg: 'px-6 py-2 text-base',
|
||||
}
|
||||
const variants = {
|
||||
primary:
|
||||
|
||||
@@ -15,9 +15,9 @@ const classes = computed(() => {
|
||||
'rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-900'
|
||||
const paddings = {
|
||||
none: '',
|
||||
sm: 'p-3',
|
||||
sm: 'p-4',
|
||||
md: 'p-4',
|
||||
lg: 'p-6',
|
||||
lg: 'p-8',
|
||||
}
|
||||
return twMerge(base, paddings[props.padding])
|
||||
})
|
||||
|
||||
@@ -15,10 +15,10 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div v-if="open" class="fixed inset-0 z-50 flex items-end justify-center p-4 sm:items-center">
|
||||
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" @click="$emit('close')" />
|
||||
<div ref="modalRef" role="dialog" aria-modal="true"
|
||||
class="relative z-10 w-full max-w-md rounded-lg bg-white p-6 shadow-lg dark:bg-gray-800">
|
||||
class="relative z-10 max-h-[calc(100svh-2rem)] w-full max-w-md overflow-y-auto rounded-lg bg-white p-8 shadow-lg dark:bg-gray-800">
|
||||
<h2 v-if="title" class="mb-4 text-lg font-semibold dark:text-white">{{ title }}</h2>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="animate-pulse space-y-3" role="status" aria-label="Loading">
|
||||
<div class="animate-pulse space-y-4" role="status" aria-label="Loading">
|
||||
<div
|
||||
v-for="n in rows"
|
||||
:key="n"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Badge from '@/components/ui/Badge.vue'
|
||||
|
||||
const props = defineProps({ patient: { type: Object, required: true } })
|
||||
|
||||
const riskVariant = computed(() => {
|
||||
const score = props.patient.news2Score ?? 0
|
||||
if (score >= 7) return 'critical'
|
||||
if (score >= 5) return 'warning'
|
||||
return 'success'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="cursor-pointer rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition duration-200 hover:border-gray-300 hover:shadow-md active:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 dark:hover:border-gray-600 dark:active:bg-gray-800"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<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 ?? '—' }}
|
||||
</span>
|
||||
<Badge v-if="patient.sepsisActive" variant="critical" size="xs">Sepsis</Badge>
|
||||
</div>
|
||||
<h3 class="mt-2 truncate text-base font-semibold text-gray-900 dark:text-white">
|
||||
{{ patient.firstName }} {{ patient.lastName }}
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ patient.mrn }}</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">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">Open alerts</dt>
|
||||
<dd class="mt-2">
|
||||
<Badge v-if="patient.openAlertCount > 0" :variant="patient.openAlertCount > 2 ? 'critical' : 'warning'">
|
||||
{{ patient.openAlertCount }}
|
||||
</Badge>
|
||||
<span v-else class="text-sm text-gray-400">0</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
</template>
|
||||
@@ -14,26 +14,26 @@ const riskVariant = computed(() => {
|
||||
|
||||
<template>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
|
||||
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ patient.room ?? '—' }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<td class="px-4 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ patient.firstName }} {{ patient.lastName }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ patient.mrn }}</div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-right">
|
||||
<td class="whitespace-nowrap px-4 py-4 text-right">
|
||||
<Badge :variant="riskVariant">{{ patient.news2Score ?? '—' }}</Badge>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-right text-sm text-gray-700 dark:text-gray-300">
|
||||
<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-3">
|
||||
<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>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3">
|
||||
<td class="whitespace-nowrap px-4 py-4">
|
||||
<Badge v-if="patient.openAlertCount > 0" :variant="patient.openAlertCount > 2 ? 'critical' : 'warning'">
|
||||
{{ patient.openAlertCount }}
|
||||
</Badge>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import PatientRow from './PatientRow.vue'
|
||||
import PatientCard from './PatientCard.vue'
|
||||
|
||||
defineProps({ patients: { type: Array, required: true } })
|
||||
const router = useRouter()
|
||||
@@ -11,16 +12,27 @@ function goToPatient(encounterId) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<!-- Mobile: card list -->
|
||||
<div class="space-y-4 md:hidden">
|
||||
<PatientCard
|
||||
v-for="patient in patients"
|
||||
:key="patient.encounterId"
|
||||
:patient="patient"
|
||||
@click="goToPatient(patient.encounterId)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Desktop: scrollable table -->
|
||||
<div class="hidden overflow-x-auto rounded-lg border border-gray-200 md:block dark:border-gray-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="sticky top-0 bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Room</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Patient</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">NEWS2</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">qSOFA</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Sepsis</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Alerts</th>
|
||||
<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">qSOFA</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>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
|
||||
@@ -28,7 +40,7 @@ function goToPatient(encounterId) {
|
||||
v-for="patient in patients"
|
||||
:key="patient.encounterId"
|
||||
:patient="patient"
|
||||
class="cursor-pointer transition duration-150 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
class="cursor-pointer transition duration-200 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
@click="goToPatient(patient.encounterId)"
|
||||
/>
|
||||
</tbody>
|
||||
|
||||
@@ -1,298 +1,18 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
--bg: #fff;
|
||||
--border: #e5e4e7;
|
||||
--code-bg: #f4f3ec;
|
||||
--accent: #aa3bff;
|
||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||
--accent-border: rgba(170, 59, 255, 0.5);
|
||||
--social-bg: rgba(244, 243, 236, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||
|
||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--mono: ui-monospace, Consolas, monospace;
|
||||
|
||||
font: 18px/145% var(--sans);
|
||||
letter-spacing: 0.18px;
|
||||
color-scheme: light dark;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
/* 8px spacing rhythm — use Tailwind scale steps 2, 4, 6, 8, 12, 16 (8, 16, 24, 32, 48, 64 px) */
|
||||
@layer base {
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #9ca3af;
|
||||
--text-h: #f3f4f6;
|
||||
--bg: #16171d;
|
||||
--border: #2e303a;
|
||||
--code-bg: #1f2028;
|
||||
--accent: #c084fc;
|
||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||
--accent-border: rgba(192, 132, 252, 0.5);
|
||||
--social-bg: rgba(47, 48, 58, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||
}
|
||||
|
||||
#social .button-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
font-weight: 500;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 56px;
|
||||
letter-spacing: -1.68px;
|
||||
margin: 32px 0;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
line-height: 118%;
|
||||
letter-spacing: -0.24px;
|
||||
margin: 0 0 8px;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code,
|
||||
.counter {
|
||||
font-family: var(--mono);
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 15px;
|
||||
line-height: 135%;
|
||||
padding: 4px 8px;
|
||||
background: var(--code-bg);
|
||||
}
|
||||
|
||||
.counter {
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
color: var(--accent);
|
||||
background: var(--accent-bg);
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.3s;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
|
||||
.base,
|
||||
.framework,
|
||||
.vite {
|
||||
inset-inline: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.base {
|
||||
width: 170px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.framework,
|
||||
.vite {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.framework {
|
||||
z-index: 1;
|
||||
top: 34px;
|
||||
height: 28px;
|
||||
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
||||
scale(1.4);
|
||||
}
|
||||
|
||||
.vite {
|
||||
z-index: 0;
|
||||
top: 107px;
|
||||
height: 26px;
|
||||
width: auto;
|
||||
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
||||
scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 1126px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border-inline: 1px solid var(--border);
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
#center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
padding: 32px 20px 24px;
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
#next-steps {
|
||||
display: flex;
|
||||
border-top: 1px solid var(--border);
|
||||
text-align: left;
|
||||
|
||||
& > div {
|
||||
flex: 1 1 0;
|
||||
padding: 32px;
|
||||
@media (max-width: 1024px) {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 16px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
#docs {
|
||||
border-right: 1px solid var(--border);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
#next-steps ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 32px 0 0;
|
||||
|
||||
.logo {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-h);
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
background: var(--social-bg);
|
||||
display: flex;
|
||||
padding: 6px 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
transition: box-shadow 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.button-icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
li {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
}
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#spacer {
|
||||
height: 88px;
|
||||
border-top: 1px solid var(--border);
|
||||
@media (max-width: 1024px) {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.ticks {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4.5px;
|
||||
border: 5px solid transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
border-left-color: var(--border);
|
||||
}
|
||||
&::after {
|
||||
right: 0;
|
||||
border-right-color: var(--border);
|
||||
#app {
|
||||
min-height: 100svh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ async function handleResolve(alertId) {
|
||||
|
||||
<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">
|
||||
<div v-else class="mt-4 space-y-4">
|
||||
<AlertCard
|
||||
v-for="alert in alerts"
|
||||
:key="alert.id"
|
||||
|
||||
@@ -47,8 +47,8 @@ watch(() => route.params.encounterId, loadAll)
|
||||
|
||||
<template>
|
||||
<Skeleton v-if="loading && !encounter" :rows="6" />
|
||||
<div v-else-if="encounter" class="space-y-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div v-else-if="encounter" class="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
|
||||
</RouterLink>
|
||||
@@ -57,13 +57,13 @@ watch(() => route.params.encounterId, loadAll)
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="grid gap-4 sm: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">
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<OrdersPanel :orders="orders" />
|
||||
<SepsisBundlePanel v-if="sepsisBundle" :bundle="sepsisBundle" />
|
||||
</div>
|
||||
|
||||
@@ -8,20 +8,46 @@ import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
import Badge from '@/components/ui/Badge.vue'
|
||||
|
||||
const wardStore = useWardStore()
|
||||
const { sortedByRisk, loading, error, criticalCount } = storeToRefs(wardStore)
|
||||
const { sortedByRisk, loading, error, criticalCount, department } = storeToRefs(wardStore)
|
||||
|
||||
usePolling(() => wardStore.loadEncounters(), 10_000)
|
||||
|
||||
const departments = [
|
||||
{ value: '', label: 'All departments' },
|
||||
{ value: 'ICU', label: 'ICU' },
|
||||
{ value: 'GENERAL_MEDICINE', label: 'General Medicine' },
|
||||
{ value: 'SURGERY', label: 'Surgery' },
|
||||
]
|
||||
|
||||
function onDepartmentChange(event) {
|
||||
wardStore.setDepartment(event.target.value || null)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="mb-4 flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 class="text-xl font-bold dark:text-white">Virtual Ward</h1>
|
||||
<Badge v-if="criticalCount > 0" variant="critical">
|
||||
{{ criticalCount }} critical
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<label class="mb-4 block sm:hidden">
|
||||
<span class="mb-2 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Department
|
||||
</span>
|
||||
<select
|
||||
:value="department ?? ''"
|
||||
class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
@change="onDepartmentChange"
|
||||
>
|
||||
<option v-for="dept in departments" :key="dept.value" :value="dept.value">
|
||||
{{ dept.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<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" />
|
||||
|
||||
Reference in New Issue
Block a user