diff --git a/VigilCare.Simulator/package-lock.json b/VigilCare.Simulator/package-lock.json
new file mode 100644
index 0000000..b2dc262
--- /dev/null
+++ b/VigilCare.Simulator/package-lock.json
@@ -0,0 +1,331 @@
+{
+ "name": "VigilCare.Simulator",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "chart.js": "^4.5.1",
+ "vue-chartjs": "^5.3.3"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz",
+ "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@vue/shared": "3.5.38",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz",
+ "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/compiler-core": "3.5.38",
+ "@vue/shared": "3.5.38"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz",
+ "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@vue/compiler-core": "3.5.38",
+ "@vue/compiler-dom": "3.5.38",
+ "@vue/compiler-ssr": "3.5.38",
+ "@vue/shared": "3.5.38",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.15",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz",
+ "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.38",
+ "@vue/shared": "3.5.38"
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz",
+ "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/shared": "3.5.38"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz",
+ "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/reactivity": "3.5.38",
+ "@vue/shared": "3.5.38"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz",
+ "integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/reactivity": "3.5.38",
+ "@vue/runtime-core": "3.5.38",
+ "@vue/shared": "3.5.38",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz",
+ "integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.38",
+ "@vue/shared": "3.5.38"
+ },
+ "peerDependencies": {
+ "vue": "3.5.38"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
+ "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.13",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.13.tgz",
+ "integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC",
+ "peer": true
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz",
+ "integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.38",
+ "@vue/compiler-sfc": "3.5.38",
+ "@vue/runtime-dom": "3.5.38",
+ "@vue/server-renderer": "3.5.38",
+ "@vue/shared": "3.5.38"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-chartjs": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.3.tgz",
+ "integrity": "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "vue": "^3.0.0-0 || ^2.7.0"
+ }
+ }
+ }
+}
diff --git a/VigilCare.Simulator/package.json b/VigilCare.Simulator/package.json
new file mode 100644
index 0000000..74bc07b
--- /dev/null
+++ b/VigilCare.Simulator/package.json
@@ -0,0 +1,6 @@
+{
+ "dependencies": {
+ "chart.js": "^4.5.1",
+ "vue-chartjs": "^5.3.3"
+ }
+}
diff --git a/vigilcare-dashboard/package-lock.json b/vigilcare-dashboard/package-lock.json
index dec3f57..ed1764b 100644
--- a/vigilcare-dashboard/package-lock.json
+++ b/vigilcare-dashboard/package-lock.json
@@ -9,10 +9,12 @@
"version": "0.0.0",
"dependencies": {
"@vueuse/core": "^14.3.0",
+ "chart.js": "^4.5.1",
"clsx": "^2.1.1",
"pinia": "^3.0.4",
"tailwind-merge": "^3.6.0",
"vue": "^3.5.34",
+ "vue-chartjs": "^5.3.3",
"vue-router": "^4.6.4"
},
"devDependencies": {
@@ -744,6 +746,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
@@ -2299,6 +2307,18 @@
"node": ">=18"
}
},
+ "node_modules/chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/check-error": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
@@ -4363,6 +4383,16 @@
}
}
},
+ "node_modules/vue-chartjs": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.3.tgz",
+ "integrity": "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "vue": "^3.0.0-0 || ^2.7.0"
+ }
+ },
"node_modules/vue-component-type-helpers": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.5.tgz",
diff --git a/vigilcare-dashboard/package.json b/vigilcare-dashboard/package.json
index 8f6ab2f..d8ed1d8 100644
--- a/vigilcare-dashboard/package.json
+++ b/vigilcare-dashboard/package.json
@@ -12,10 +12,12 @@
},
"dependencies": {
"@vueuse/core": "^14.3.0",
+ "chart.js": "^4.5.1",
"clsx": "^2.1.1",
"pinia": "^3.0.4",
"tailwind-merge": "^3.6.0",
"vue": "^3.5.34",
+ "vue-chartjs": "^5.3.3",
"vue-router": "^4.6.4"
},
"devDependencies": {
diff --git a/vigilcare-dashboard/src/__tests__/AlertReasoning.test.js b/vigilcare-dashboard/src/__tests__/AlertReasoning.test.js
new file mode 100644
index 0000000..e3b8825
--- /dev/null
+++ b/vigilcare-dashboard/src/__tests__/AlertReasoning.test.js
@@ -0,0 +1,46 @@
+import { describe, it, expect } from 'vitest'
+import { mount } from '@vue/test-utils'
+import AlertReasoning from '@/components/alerts/AlertReasoning.vue'
+
+const sepsisAlert = {
+ id: 'alert-1',
+ alertType: 'SepsisWarning',
+ severity: 'Critical',
+ details: 'SIRS criteria met',
+ triggeredAt: '2026-06-19T12:00:00Z',
+}
+
+const qsofaAlert = {
+ id: 'alert-2',
+ alertType: 'QsofaWarning',
+ severity: 'Warning',
+ details: 'qSOFA score elevated',
+ triggeredAt: '2026-06-19T12:30:00Z',
+}
+
+describe('AlertReasoning', () => {
+ it('showsExplanationForSepsisWarning', () => {
+ const wrapper = mount(AlertReasoning, { props: { alert: sepsisAlert } })
+ expect(wrapper.text()).toContain('SIRS / Sepsis Alert')
+ expect(wrapper.text()).toContain('≥2 of 4 SIRS criteria met')
+ })
+
+ it('showsExplanationForQsofa', () => {
+ const wrapper = mount(AlertReasoning, { props: { alert: qsofaAlert } })
+ expect(wrapper.text()).toContain('qSOFA Alert')
+ expect(wrapper.text()).toContain('≥2 of 3 qSOFA criteria met')
+ })
+
+ it('showsRawDetailsForUnknownType', () => {
+ const unknown = {
+ id: 'alert-3',
+ alertType: 'CustomUnknown',
+ severity: 'Warning',
+ details: 'Something unusual happened',
+ triggeredAt: '2026-06-19T13:00:00Z',
+ }
+ const wrapper = mount(AlertReasoning, { props: { alert: unknown } })
+ expect(wrapper.text()).toContain('CustomUnknown')
+ expect(wrapper.text()).toContain('Something unusual happened')
+ })
+})
diff --git a/vigilcare-dashboard/src/__tests__/useChartData.test.js b/vigilcare-dashboard/src/__tests__/useChartData.test.js
new file mode 100644
index 0000000..777f3af
--- /dev/null
+++ b/vigilcare-dashboard/src/__tests__/useChartData.test.js
@@ -0,0 +1,42 @@
+import { describe, it, expect } from 'vitest'
+import { defineComponent } from 'vue'
+import { mount } from '@vue/test-utils'
+import { useChartData } from '@/composables/useChartData'
+
+function mountChartData(observations, code) {
+ let exposed
+ const Comp = defineComponent({
+ setup() {
+ exposed = useChartData(observations, code)
+ return () => null
+ },
+ })
+ mount(Comp)
+ return exposed
+}
+
+const observations = [
+ { observationCode: 'HEART_RATE', value: 80, recordedAt: '2026-06-19T12:00:00Z' },
+ { observationCode: 'RESP_RATE', value: 18, recordedAt: '2026-06-19T12:00:00Z' },
+ { observationCode: 'HEART_RATE', value: 72, recordedAt: '2026-06-19T11:00:00Z' },
+]
+
+describe('useChartData', () => {
+ it('filtersObservationsByCode', () => {
+ const { chartData } = mountChartData(observations, 'HEART_RATE')
+ expect(chartData.value.datasets[0].data).toEqual([72, 80])
+ expect(chartData.value.labels).toHaveLength(2)
+ })
+
+ it('sortsChronologically', () => {
+ const { chartData } = mountChartData(observations, 'HEART_RATE')
+ expect(chartData.value.datasets[0].data[0]).toBe(72)
+ expect(chartData.value.datasets[0].data[1]).toBe(80)
+ })
+
+ it('handlesEmptyData', () => {
+ const { chartData } = mountChartData([], 'HEART_RATE')
+ expect(chartData.value.labels).toEqual([])
+ expect(chartData.value.datasets[0].data).toEqual([])
+ })
+})
diff --git a/vigilcare-dashboard/src/__tests__/useReplayControls.test.js b/vigilcare-dashboard/src/__tests__/useReplayControls.test.js
new file mode 100644
index 0000000..033a403
--- /dev/null
+++ b/vigilcare-dashboard/src/__tests__/useReplayControls.test.js
@@ -0,0 +1,33 @@
+import { describe, it, expect } from 'vitest'
+import { useReplayControls } from '@/composables/useReplayControls'
+
+describe('useReplayControls', () => {
+ it('pauseAndResume', () => {
+ const { isPaused, pause, resume } = useReplayControls()
+ expect(isPaused.value).toBe(false)
+ pause()
+ expect(isPaused.value).toBe(true)
+ resume()
+ expect(isPaused.value).toBe(false)
+ })
+
+ it('speedPresets', () => {
+ const { speed, setSpeed } = useReplayControls()
+ expect(speed.value).toBe(60)
+ setSpeed(360)
+ expect(speed.value).toBe(360)
+ })
+
+ it('progressComputation', () => {
+ const { currentOffsetMinutes, scenarioDurationMinutes, progress } = useReplayControls()
+ scenarioDurationMinutes.value = 100
+ currentOffsetMinutes.value = 50
+ expect(progress.value).toBe(50)
+ })
+
+ it('jumpToOffset', () => {
+ const { currentOffsetMinutes, jumpToOffset } = useReplayControls()
+ jumpToOffset(42)
+ expect(currentOffsetMinutes.value).toBe(42)
+ })
+})
diff --git a/vigilcare-dashboard/src/api/clinical.js b/vigilcare-dashboard/src/api/clinical.js
index 2f8bb6a..207010b 100644
--- a/vigilcare-dashboard/src/api/clinical.js
+++ b/vigilcare-dashboard/src/api/clinical.js
@@ -14,4 +14,32 @@ export function fetchSepsisBundle(encounterId) {
export function fetchOrders(encounterId) {
return api.get(`/api/v1/encounters/${encounterId}/orders`)
+}
+
+export async function fetchNews2History(encounterId, { limit = 100 } = {}) {
+ const all = []
+ let cursor = null
+ do {
+ const params = new URLSearchParams({ limit: String(limit) })
+ if (cursor) params.set('cursor', cursor)
+ const page = await api.get(`/api/v1/encounters/${encounterId}/news2/history?${params}`)
+ all.push(...(page.items ?? []))
+ cursor = page.hasMore ? page.nextCursor : null
+ } while (cursor)
+ return all
+}
+
+export async function fetchMedications(encounterId, { pageSize = 50, since } = {}) {
+ const all = []
+ let page = 1
+ let totalPages = 1
+ do {
+ const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) })
+ if (since) params.set('since', since)
+ const result = await api.get(`/api/v1/encounters/${encounterId}/medications?${params}`)
+ all.push(...(result.items ?? []))
+ totalPages = result.totalPages ?? 1
+ page++
+ } while (page <= totalPages)
+ return all
}
\ No newline at end of file
diff --git a/vigilcare-dashboard/src/components/alerts/AlertReasoning.vue b/vigilcare-dashboard/src/components/alerts/AlertReasoning.vue
new file mode 100644
index 0000000..8bf1a25
--- /dev/null
+++ b/vigilcare-dashboard/src/components/alerts/AlertReasoning.vue
@@ -0,0 +1,81 @@
+
+
+
+
+ {{ reasoningMap[alert.alertType]?.explain(alert) ?? alert.details }}
+ Recent medications (within 90 min)
+ {{ reasoningMap[alert.alertType]?.label ?? alert.alertType }}
+
+
+
+ NEWS2 Score Over Time
+ {{ title }}
+