Fix issues with critical alerts breaking the simulation

This commit is contained in:
voltsrage
2026-06-25 02:06:35 +08:00
parent df6fbed401
commit 09a84f34ba
29 changed files with 196 additions and 155 deletions
@@ -0,0 +1,11 @@
using System.Text.Json;
using System.Text.Json.Serialization;
public sealed class AlertSeverityJsonConverter : JsonConverter<AlertSeverity>
{
public override AlertSeverity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> AlertSeverityExtensions.FromDbString(reader.GetString()!);
public override void Write(Utf8JsonWriter writer, AlertSeverity value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToDbString());
}
@@ -0,0 +1,11 @@
using System.Text.Json;
using System.Text.Json.Serialization;
public sealed class AlertStatusJsonConverter : JsonConverter<AlertStatus>
{
public override AlertStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> AlertStatusExtensions.FromDbString(reader.GetString()!);
public override void Write(Utf8JsonWriter writer, AlertStatus value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToDbString());
}
@@ -0,0 +1,11 @@
using System.Text.Json;
using System.Text.Json.Serialization;
public sealed class AlertTypeJsonConverter : JsonConverter<AlertType>
{
public override AlertType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> AlertTypeExtensions.FromDbString(reader.GetString()!);
public override void Write(Utf8JsonWriter writer, AlertType value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToDbString());
}
@@ -44,7 +44,10 @@ namespace VigilCareClinicalAPI.Migrations
table: "patients");
migrationBuilder.Sql(
"ALTER TABLE patients ALTER COLUMN date_of_birth TYPE date USING date_of_birth::date;");
"""
UPDATE patients SET date_of_birth = '1900-01-01' WHERE date_of_birth !~ '^\d{4}-\d{2}-\d{2}$';
ALTER TABLE patients ALTER COLUMN date_of_birth TYPE date USING date_of_birth::date;
""");
}
}
}
@@ -56,6 +56,14 @@ namespace VigilCareClinicalAPI.Migrations
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
UPDATE patients SET last_name = LEFT(last_name, 100);
UPDATE patients SET first_name = LEFT(first_name, 100);
UPDATE patients SET emergency_contact_phone = LEFT(emergency_contact_phone, 20);
UPDATE patients SET emergency_contact_name = LEFT(emergency_contact_name, 200);
""");
migrationBuilder.AlterColumn<string>(
name: "last_name",
table: "patients",
+4 -1
View File
@@ -258,9 +258,12 @@ try
opts.JsonSerializerOptions.Converters.Add(new BloodTypeJsonConverter());
opts.JsonSerializerOptions.Converters.Add(new AuditActionJsonConverter());
opts.JsonSerializerOptions.Converters.Add(new SepsisBundleComplianceStatusJsonConverter());
opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
opts.JsonSerializerOptions.Converters.Add(new AlertTypeJsonConverter());
opts.JsonSerializerOptions.Converters.Add(new AlertSeverityJsonConverter());
opts.JsonSerializerOptions.Converters.Add(new AlertStatusJsonConverter());
opts.JsonSerializerOptions.Converters.Add(new ObservationSourceJsonConverter());
opts.JsonSerializerOptions.Converters.Add(new DepartmentJsonConverter());
opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
@@ -6,9 +6,9 @@ import { useAuthStore } from '@/stores/auth'
const alert = {
id: 'alert-1',
alertType: 'SofaSepsis',
severity: 'Critical',
status: 'Open',
alertType: 'SOFA_SEPSIS',
severity: 'CRITICAL',
status: 'OPEN',
details: 'SOFA delta >= 2',
}
@@ -5,9 +5,9 @@ import AlertCard from '@/components/alerts/AlertCard.vue'
const openAlert = {
id: 'alert-1',
alertType: 'SepsisWarning',
severity: 'Critical',
status: 'Open',
alertType: 'SEPSIS_WARNING',
severity: 'CRITICAL',
status: 'OPEN',
details: 'SIRS criteria met',
triggeredAt: '2026-06-19T12:00:00Z',
}
@@ -15,7 +15,7 @@ const openAlert = {
const resolvedAlert = {
...openAlert,
id: 'alert-2',
status: 'Resolved',
status: 'RESOLVED',
}
describe('AlertCard', () => {
@@ -25,7 +25,7 @@ describe('AlertCard', () => {
it('showsAlertTypeAndSeverity', () => {
const wrapper = mount(AlertCard, { props: { alert: openAlert } })
expect(wrapper.text()).toContain('Critical')
expect(wrapper.text()).toContain('CRITICAL')
expect(wrapper.text()).toContain('Sepsis Warning (SIRS — Legacy)')
})
@@ -55,7 +55,7 @@ describe('AlertCard', () => {
props: {
alert: {
...openAlert,
status: 'Acknowledged',
status: 'ACKNOWLEDGED',
acknowledgedBy: 'Demo Nurse (NURSE)',
acknowledgedAt: '2026-06-19T12:30:00Z',
},
@@ -3,19 +3,19 @@ import { alertTypeLabel, bundleTriggerLabel } from '@/api/normalize'
describe('AlertLabels', () => {
it('sofaSepsisLabel', () => {
expect(alertTypeLabel('SofaSepsis')).toBe('Sepsis Alert (SOFA)')
expect(alertTypeLabel('SOFA_SEPSIS')).toBe('Sepsis Alert (SOFA)')
})
it('legacySepsisLabel', () => {
expect(alertTypeLabel('SepsisWarning')).toBe('Sepsis Warning (SIRS — Legacy)')
expect(alertTypeLabel('SEPSIS_WARNING')).toBe('Sepsis Warning (SIRS — Legacy)')
})
it('qsofaScreenLabel', () => {
expect(alertTypeLabel('QsofaScreen')).toBe('qSOFA Screen')
expect(alertTypeLabel('QSOFA_SCREEN')).toBe('qSOFA Screen')
})
it('gcsCriticalLabel', () => {
expect(alertTypeLabel('GcsCritical')).toBe('GCS Critical (≤ 8)')
expect(alertTypeLabel('GCS_CRITICAL')).toBe('GCS Critical (≤ 8)')
})
it('bundleTriggerSofa', () => {
@@ -5,16 +5,16 @@ import AlertReasoning from '@/components/alerts/AlertReasoning.vue'
const sepsisAlert = {
id: 'alert-1',
alertType: 'SepsisWarning',
severity: 'Critical',
alertType: 'SEPSIS_WARNING',
severity: 'CRITICAL',
details: 'SIRS criteria met',
triggeredAt: '2026-06-19T12:00:00Z',
}
const qsofaScreenAlert = {
id: 'alert-2',
alertType: 'QsofaScreen',
severity: 'Warning',
alertType: 'QSOFA_SCREEN',
severity: 'WARNING',
details: 'qSOFA screen positive',
triggeredAt: '2026-06-19T12:30:00Z',
}
@@ -42,7 +42,7 @@ describe('AlertReasoning', () => {
const unknown = {
id: 'alert-3',
alertType: 'CustomUnknown',
severity: 'Warning',
severity: 'WARNING',
details: 'Something unusual happened',
triggeredAt: '2026-06-19T13:00:00Z',
}
@@ -54,8 +54,8 @@ describe('AlertReasoning', () => {
it('showsStructuredExplanationWhenPresent', () => {
const news2Alert = {
id: 'alert-4',
alertType: 'News2Emergency',
severity: 'Critical',
alertType: 'NEWS2_EMERGENCY',
severity: 'CRITICAL',
details: 'NEWS2 score 8 (HIGH): RESP_RATE=3, SPO2=2',
triggeredAt: '2026-06-19T14:00:00Z',
explanation: {
@@ -18,9 +18,9 @@ describe('CriticalAlertBanner', () => {
const alertStore = useAlertStore()
alertStore.bannerAlerts = [{
id: 'alert-1',
alertType: 'SofaSepsis',
severity: 'Critical',
status: 'Open',
alertType: 'SOFA_SEPSIS',
severity: 'CRITICAL',
status: 'OPEN',
details: 'SOFA delta >= 2',
explanation: {
narrativeSummary: 'SOFA 6 — respiratory (+2), renal (+2).',
@@ -13,9 +13,9 @@ const events = [
{
type: 'alert',
timestamp: '2026-06-23T11:00:00Z',
alertType: 'QsofaScreen',
severity: 'Warning',
status: 'Open',
alertType: 'QSOFA_SCREEN',
severity: 'WARNING',
status: 'OPEN',
},
{
type: 'medication',
@@ -23,8 +23,8 @@ vi.mock('@/stores/alertQuality', () => ({
const defaultProps = {
alertId: 'alert-1',
alertType: 'SepsisWarning',
severity: 'Critical',
alertType: 'SEPSIS_WARNING',
severity: 'CRITICAL',
}
function ratingButtons(wrapper) {
@@ -10,8 +10,8 @@ const { mockSummaryStats, mockByAlertType } = vi.hoisted(() => ({
falsePositiveRate: 33,
},
mockByAlertType: {
SepsisWarning: [{ rating: 'useful' }, { rating: 'would-act' }],
WarningHeartRate: [{ rating: 'false-positive' }],
SEPSIS_WARNING: [{ rating: 'useful' }, { rating: 'would-act' }],
WARNING_HEART_RATE: [{ rating: 'false-positive' }],
},
}))
@@ -3,23 +3,23 @@ import { detectNewCriticalAlerts, isNotifiableCriticalAlert } from '@/composable
const criticalOpen = {
id: 'a1',
severity: 'Critical',
status: 'Open',
alertType: 'SofaSepsis',
severity: 'CRITICAL',
status: 'OPEN',
alertType: 'SOFA_SEPSIS',
}
const criticalAcknowledged = {
id: 'a2',
severity: 'Critical',
status: 'Acknowledged',
alertType: 'News2Emergency',
severity: 'CRITICAL',
status: 'ACKNOWLEDGED',
alertType: 'NEWS2_EMERGENCY',
}
const warningOpen = {
id: 'a3',
severity: 'Warning',
status: 'Open',
alertType: 'News2Warning',
severity: 'WARNING',
status: 'OPEN',
alertType: 'NEWS2_WARNING',
}
describe('criticalAlertDetect', () => {
@@ -39,7 +39,7 @@ describe('criticalAlertDetect', () => {
it('detectsNewCriticalAlertsAfterSeed', () => {
const seeded = detectNewCriticalAlerts([criticalOpen], [], false)
const next = detectNewCriticalAlerts(
[criticalOpen, { ...criticalOpen, id: 'a4', alertType: 'GcsCritical' }],
[criticalOpen, { ...criticalOpen, id: 'a4', alertType: 'GCS_CRITICAL' }],
seeded.nextSeenIds,
seeded.seeded,
)
@@ -29,7 +29,7 @@ const enrichment = {
admissionReason: 'Sepsis workup',
patient: { allergies: 'Penicillin' },
},
openAlerts: [{ alertType: 'News2Emergency' }],
openAlerts: [{ alertType: 'NEWS2_EMERGENCY' }],
pendingOrders: [{ description: 'Blood cultures', status: 'Pending' }],
vitals: extractLatestVitals([
{ observationCode: 'HEART_RATE', value: 110, unit: 'bpm', recordedAt: '2026-06-23T10:00:00Z' },
@@ -11,9 +11,9 @@ describe('alert store notifications', () => {
const store = useAlertStore()
const critical = {
id: 'alert-1',
severity: 'Critical',
status: 'Open',
alertType: 'SofaSepsis',
severity: 'CRITICAL',
status: 'OPEN',
alertType: 'SOFA_SEPSIS',
}
expect(store.applyPollResults([critical])).toEqual([])
@@ -21,9 +21,9 @@ describe('alert store notifications', () => {
const next = {
id: 'alert-2',
severity: 'Critical',
status: 'Open',
alertType: 'GcsCritical',
severity: 'CRITICAL',
status: 'OPEN',
alertType: 'GCS_CRITICAL',
}
const newAlerts = store.applyPollResults([critical, next])
expect(newAlerts.map(alert => alert.id)).toEqual(['alert-2'])
@@ -14,7 +14,7 @@ describe('useFeedbackStore', () => {
it('addFeedback_createsEntry', () => {
const store = useFeedbackStore()
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
store.addFeedback('a1', 'SEPSIS_WARNING', 'CRITICAL', 'useful')
expect(store.entries).toHaveLength(1)
expect(store.entries[0].alertId).toBe('a1')
@@ -23,8 +23,8 @@ describe('useFeedbackStore', () => {
it('addFeedback_updatesExisting', () => {
const store = useFeedbackStore()
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'false-positive')
store.addFeedback('a1', 'SEPSIS_WARNING', 'CRITICAL', 'useful')
store.addFeedback('a1', 'SEPSIS_WARNING', 'CRITICAL', 'false-positive')
expect(store.entries).toHaveLength(1)
expect(store.entries[0].rating).toBe('false-positive')
@@ -32,10 +32,10 @@ describe('useFeedbackStore', () => {
it('stats_computesCorrectly', () => {
const store = useFeedbackStore()
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
store.addFeedback('a2', 'SepsisWarning', 'Critical', 'would-act')
store.addFeedback('a3', 'WarningHeartRate', 'Warning', 'false-positive')
store.addFeedback('a4', 'WarningHeartRate', 'Warning', 'too-early')
store.addFeedback('a1', 'SEPSIS_WARNING', 'CRITICAL', 'useful')
store.addFeedback('a2', 'SEPSIS_WARNING', 'CRITICAL', 'would-act')
store.addFeedback('a3', 'WARNING_HEART_RATE', 'WARNING', 'false-positive')
store.addFeedback('a4', 'WARNING_HEART_RATE', 'WARNING', 'too-early')
expect(store.stats.total).toBe(4)
expect(store.stats.useful).toBe(2)
@@ -46,18 +46,18 @@ describe('useFeedbackStore', () => {
it('byAlertType_groupsCorrectly', () => {
const store = useFeedbackStore()
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
store.addFeedback('a2', 'WarningHeartRate', 'Warning', 'false-positive')
store.addFeedback('a3', 'SepsisWarning', 'Critical', 'too-early')
store.addFeedback('a1', 'SEPSIS_WARNING', 'CRITICAL', 'useful')
store.addFeedback('a2', 'WARNING_HEART_RATE', 'WARNING', 'false-positive')
store.addFeedback('a3', 'SEPSIS_WARNING', 'CRITICAL', 'too-early')
expect(Object.keys(store.byAlertType)).toHaveLength(2)
expect(store.byAlertType.SepsisWarning).toHaveLength(2)
expect(store.byAlertType.WarningHeartRate).toHaveLength(1)
expect(store.byAlertType.SEPSIS_WARNING).toHaveLength(2)
expect(store.byAlertType.WARNING_HEART_RATE).toHaveLength(1)
})
it('exportAsJson_generatesValidJson', () => {
const store = useFeedbackStore()
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
store.addFeedback('a1', 'SEPSIS_WARNING', 'CRITICAL', 'useful')
let capturedBlob = null
const click = vi.fn()
@@ -81,7 +81,7 @@ describe('useFeedbackStore', () => {
it('clearAll_emptiesEntries', () => {
const store = useFeedbackStore()
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
store.addFeedback('a1', 'SEPSIS_WARNING', 'CRITICAL', 'useful')
store.clearAll()
expect(store.entries).toHaveLength(0)
@@ -89,7 +89,7 @@ describe('useFeedbackStore', () => {
it('persistsToLocalStorage', () => {
const store = useFeedbackStore()
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
store.addFeedback('a1', 'SEPSIS_WARNING', 'CRITICAL', 'useful')
const stored = JSON.parse(localStorage.getItem('vigilcare-feedback'))
expect(stored).toHaveLength(1)
+41 -47
View File
@@ -1,43 +1,43 @@
const ALERT_TYPE_LABELS = {
SepsisWarning: 'Sepsis Warning (SIRS — Legacy)',
QsofaWarning: 'qSOFA Alert (Legacy)',
QsofaScreen: 'qSOFA Screen',
SofaSepsis: 'Sepsis Alert (SOFA)',
SofaWarning: 'SOFA Warning',
GcsCritical: 'GCS Critical (≤ 8)',
GcsWarning: 'GCS Warning (912)',
News2Warning: 'NEWS2 Warning',
News2Emergency: 'NEWS2 Emergency',
RapidDeterioration: 'Rapid Deterioration',
CriticalHeartRate: 'Critical Heart Rate',
CriticalTempC: 'Critical Temperature',
CriticalPotassiumMeqL: 'Critical Potassium',
CriticalSpo2: 'Critical SpO₂',
CriticalRespRate: 'Critical Respiratory Rate',
CriticalWbcKUl: 'Critical WBC',
CriticalSystolicBp: 'Critical Systolic BP',
CriticalDiastolicBp: 'Critical Diastolic BP',
CriticalLactateMmolL: 'Critical Lactate',
CriticalAvpu: 'Critical AVPU',
CriticalGlucoseMgDl: 'Critical Glucose',
WarningHeartRate: 'Warning Heart Rate',
WarningTempC: 'Warning Temperature',
WarningPotassiumMeqL: 'Warning Potassium',
WarningSpo2: 'Warning SpO₂',
WarningRespRate: 'Warning Respiratory Rate',
WarningWbcKUl: 'Warning WBC',
WarningSystolicBp: 'Warning Systolic BP',
WarningDiastolicBp: 'Warning Diastolic BP',
WarningLactateMmolL: 'Warning Lactate',
WarningGlucoseMgDl: 'Warning Glucose',
CriticalPao2MmHg: 'Critical PaO₂',
WarningPao2MmHg: 'Warning PaO₂',
CriticalPlateletKUl: 'Critical Platelets',
WarningPlateletKUl: 'Warning Platelets',
CriticalBilirubinMgDl: 'Critical Bilirubin',
WarningBilirubinMgDl: 'Warning Bilirubin',
CriticalCreatinineMgDl: 'Critical Creatinine',
WarningCreatinineMgDl: 'Warning Creatinine',
SEPSIS_WARNING: 'Sepsis Warning (SIRS — Legacy)',
QSOFA_WARNING: 'qSOFA Alert (Legacy)',
QSOFA_SCREEN: 'qSOFA Screen',
SOFA_SEPSIS: 'Sepsis Alert (SOFA)',
SOFA_WARNING: 'SOFA Warning',
GCS_CRITICAL: 'GCS Critical (≤ 8)',
GCS_WARNING: 'GCS Warning (912)',
NEWS2_WARNING: 'NEWS2 Warning',
NEWS2_EMERGENCY: 'NEWS2 Emergency',
RAPID_DETERIORATION: 'Rapid Deterioration',
CRITICAL_HEART_RATE: 'Critical Heart Rate',
CRITICAL_TEMP_C: 'Critical Temperature',
CRITICAL_POTASSIUM_MEQ_L: 'Critical Potassium',
CRITICAL_SPO2: 'Critical SpO₂',
CRITICAL_RESP_RATE: 'Critical Respiratory Rate',
CRITICAL_WBC_K_UL: 'Critical WBC',
CRITICAL_SYSTOLIC_BP: 'Critical Systolic BP',
CRITICAL_DIASTOLIC_BP: 'Critical Diastolic BP',
CRITICAL_LACTATE_MMOL_L: 'Critical Lactate',
CRITICAL_AVPU: 'Critical AVPU',
CRITICAL_GLUCOSE_MG_DL: 'Critical Glucose',
WARNING_HEART_RATE: 'Warning Heart Rate',
WARNING_TEMP_C: 'Warning Temperature',
WARNING_POTASSIUM_MEQ_L: 'Warning Potassium',
WARNING_SPO2: 'Warning SpO₂',
WARNING_RESP_RATE: 'Warning Respiratory Rate',
WARNING_WBC_K_UL: 'Warning WBC',
WARNING_SYSTOLIC_BP: 'Warning Systolic BP',
WARNING_DIASTOLIC_BP: 'Warning Diastolic BP',
WARNING_LACTATE_MMOL_L: 'Warning Lactate',
WARNING_GLUCOSE_MG_DL: 'Warning Glucose',
CRITICAL_PAO2_MMHG: 'Critical PaO₂',
WARNING_PAO2_MMHG: 'Warning PaO₂',
CRITICAL_PLATELET_K_UL: 'Critical Platelets',
WARNING_PLATELET_K_UL: 'Warning Platelets',
CRITICAL_BILIRUBIN_MG_DL: 'Critical Bilirubin',
WARNING_BILIRUBIN_MG_DL: 'Warning Bilirubin',
CRITICAL_CREATININE_MG_DL: 'Critical Creatinine',
WARNING_CREATININE_MG_DL: 'Warning Creatinine',
}
const BUNDLE_TRIGGER_LABELS = {
@@ -49,7 +49,7 @@ const BUNDLE_TRIGGER_LABELS = {
export function alertTypeLabel(type) {
if (!type) return ''
return ALERT_TYPE_LABELS[type]
?? type.replace(/([A-Z])/g, '_$1').slice(1).toUpperCase()
?? type.replace(/_/g, ' ')
}
export function bundleTriggerLabel(triggerType) {
@@ -58,13 +58,7 @@ export function bundleTriggerLabel(triggerType) {
}
export function alertStatusToApiFilter(status) {
const map = {
Open: 'OPEN',
Acknowledged: 'ACKNOWLEDGED',
Resolved: 'RESOLVED',
Escalated: 'ESCALATED',
}
return map[status] ?? null
return status ?? null
}
const OBSERVATION_LABELS = {
@@ -35,7 +35,7 @@ const notePreview = computed(() =>
)
function severityVariant(severity) {
return severity === 'Critical' ? 'critical' : 'warning'
return severity === 'CRITICAL' ? 'critical' : 'warning'
}
function onConfirm() {
@@ -17,24 +17,24 @@ const emit = defineEmits(['acknowledge', 'resolve'])
const actionHint = computed(() => {
const hints = {
QsofaScreen: 'Recommend SOFA labs',
SofaSepsis: 'Review organ breakdown · Initiate bundle',
GcsCritical: 'Urgent neuro assessment',
GcsWarning: 'Monitor consciousness',
QSOFA_SCREEN: 'Recommend SOFA labs',
SOFA_SEPSIS: 'Review organ breakdown · Initiate bundle',
GCS_CRITICAL: 'Urgent neuro assessment',
GCS_WARNING: 'Monitor consciousness',
}
return hints[props.alert.alertType] ?? null
})
function showActions(status) {
return status !== 'Resolved'
return status !== 'RESOLVED'
}
function canAcknowledge(status) {
return status === 'Open' || status === 'Escalated'
return status === 'OPEN' || status === 'ESCALATED'
}
function canResolve(status) {
return status === 'Acknowledged'
return status === 'ACKNOWLEDGED'
}
function formatTime(iso) {
@@ -93,7 +93,7 @@ function formatTime(iso) {
</div>
<div
v-if="alert.status === 'Acknowledged' || alert.status === 'Resolved'"
v-if="alert.status === 'ACKNOWLEDGED' || alert.status === 'RESOLVED'"
class="mt-4 border-t border-gray-100 pt-4 dark:border-gray-700"
>
<FeedbackButtons
@@ -1,11 +1,11 @@
<script setup>
const activeFilter = defineModel({ type: String, default: 'Open' })
const activeFilter = defineModel({ type: String, default: 'OPEN' })
const tabs = [
{ label: 'Open', value: 'Open', badgeVariant: 'critical' },
{ label: 'Acknowledged', value: 'Acknowledged', badgeVariant: 'warning' },
{ label: 'Resolved', value: 'Resolved', badgeVariant: 'success' },
{ label: 'Escalated', value: 'Escalated', badgeVariant: 'info' },
{ label: 'Open', value: 'OPEN', badgeVariant: 'critical' },
{ label: 'Acknowledged', value: 'ACKNOWLEDGED', badgeVariant: 'warning' },
{ label: 'Resolved', value: 'RESOLVED', badgeVariant: 'success' },
{ label: 'Escalated', value: 'ESCALATED', badgeVariant: 'info' },
]
</script>
@@ -19,57 +19,57 @@ const props = defineProps({
const CORRELATION_WINDOW_MS = 90 * 60 * 1000
const reasoningMap = {
WarningHeartRate: {
WARNING_HEART_RATE: {
label: 'Heart Rate Warning',
explain: (a) => `Heart rate ${extractValue(a)} is in the warning range (91110 bpm or 4150 bpm).`,
},
WarningSystolicBp: {
WARNING_SYSTOLIC_BP: {
label: 'Systolic BP Warning',
explain: (a) => `Systolic BP ${extractValue(a)} is in the warning range (101110 mmHg).`,
},
WarningTempC: {
WARNING_TEMP_C: {
label: 'Temperature Warning',
explain: (a) => `Temperature ${extractValue(a)} is in the warning range.`,
},
SepsisWarning: {
SEPSIS_WARNING: {
label: 'SIRS / Sepsis Alert (Legacy)',
explain: () =>
'Historical alert: ≥2 of 4 SIRS criteria met. New sepsis detection uses SOFA delta ≥ 2.',
},
QsofaWarning: {
QSOFA_WARNING: {
label: 'qSOFA Alert (Legacy)',
explain: () => 'Historical alert: ≥2 of 3 qSOFA criteria met.',
},
QsofaScreen: {
QSOFA_SCREEN: {
label: 'qSOFA Screen',
explain: () =>
'Bedside screen positive (≥2/3). Recommend ordering SOFA labs to evaluate organ dysfunction.',
},
SofaSepsis: {
SOFA_SEPSIS: {
label: 'Sepsis Alert (SOFA)',
explain: (a) => explainSofaAlert(a, true),
},
SofaWarning: {
SOFA_WARNING: {
label: 'SOFA Warning',
explain: (a) => explainSofaAlert(a, false),
},
GcsCritical: {
GCS_CRITICAL: {
label: 'GCS Critical',
explain: (a) => explainGcsAlert(a),
},
GcsWarning: {
GCS_WARNING: {
label: 'GCS Warning',
explain: (a) => explainGcsAlert(a),
},
News2Warning: {
NEWS2_WARNING: {
label: 'NEWS2 Medium Risk',
explain: () => 'NEWS2 composite score 56 indicating medium clinical risk.',
},
News2Emergency: {
NEWS2_EMERGENCY: {
label: 'NEWS2 High Risk',
explain: () => 'NEWS2 composite score ≥7 or any single parameter scored 3.',
},
RapidDeterioration: {
RAPID_DETERIORATION: {
label: 'Rapid Deterioration',
explain: () => 'Vital sign trajectory shows rapid change within the sliding window.',
},
@@ -104,10 +104,10 @@ const recentMedications = computed(() => {
const actionHint = computed(() => {
const hints = {
QsofaScreen: 'Recommend ordering SOFA labs (PaO₂, platelets, bilirubin, creatinine).',
SofaSepsis: 'Review organ dysfunction and confirm sepsis bundle initiation.',
GcsCritical: 'Urgent neurological assessment — GCS ≤ 8.',
GcsWarning: 'Monitor consciousness closely — GCS 912.',
QSOFA_SCREEN: 'Recommend ordering SOFA labs (PaO₂, platelets, bilirubin, creatinine).',
SOFA_SEPSIS: 'Review organ dysfunction and confirm sepsis bundle initiation.',
GCS_CRITICAL: 'Urgent neurological assessment — GCS ≤ 8.',
GCS_WARNING: 'Monitor consciousness closely — GCS 912.',
}
return hints[props.alert.alertType] ?? null
})
@@ -155,7 +155,7 @@ function formatMed(med) {
<Card>
<div class="space-y-4">
<div class="flex flex-wrap items-center gap-2">
<Badge :variant="alert.severity === 'Critical' ? 'critical' : 'warning'">
<Badge :variant="alert.severity === 'CRITICAL' ? 'critical' : 'warning'">
{{ alert.severity }}
</Badge>
<h3 class="font-medium dark:text-white">
@@ -30,7 +30,7 @@ function loadEncounterAlerts() {
usePolling(loadEncounterAlerts, 5_000)
const visibleAlerts = computed(() =>
alerts.value.filter(a => a.status !== 'Resolved'),
alerts.value.filter(a => a.status !== 'RESOLVED'),
)
async function handleAcknowledge(note) {
@@ -89,7 +89,7 @@ async function resolve(alertId) {
</div>
<div class="flex shrink-0 gap-2">
<Button
v-if="alert.status === 'Open' || alert.status === 'Escalated'"
v-if="alert.status === 'OPEN' || alert.status === 'ESCALATED'"
size="sm"
variant="secondary"
:aria-label="`Acknowledge ${alertTypeLabel(alert.alertType)} alert`"
@@ -98,7 +98,7 @@ async function resolve(alertId) {
Ack
</Button>
<Button
v-if="alert.status === 'Acknowledged'"
v-if="alert.status === 'ACKNOWLEDGED'"
size="sm"
variant="primary"
:aria-label="`Resolve ${alertTypeLabel(alert.alertType)} alert`"
@@ -55,7 +55,7 @@ function formatTime(iso) {
function alertBadgeVariant(event) {
if (event.type !== 'alert') return timelineEventStyles(event.type).badge
return event.severity === 'Critical' ? 'critical' : 'warning'
return event.severity === 'CRITICAL' ? 'critical' : 'warning'
}
</script>
@@ -1,7 +1,7 @@
export function isNotifiableCriticalAlert(alert) {
return (
alert.severity === 'Critical'
&& (alert.status === 'Open' || alert.status === 'Escalated')
alert.severity === 'CRITICAL'
&& (alert.status === 'OPEN' || alert.status === 'ESCALATED')
)
}
+4 -4
View File
@@ -11,8 +11,8 @@ export const useAlertStore = defineStore('alerts', () => {
const bannerAlerts = ref([])
const pollSeeded = ref(false)
const openAlerts = computed(() => alerts.value.filter(a => a.status === 'Open'))
const criticalAlerts = computed(() => alerts.value.filter(a => a.severity === 'Critical'))
const openAlerts = computed(() => alerts.value.filter(a => a.status === 'OPEN'))
const criticalAlerts = computed(() => alerts.value.filter(a => a.severity === 'CRITICAL'))
function mergeBannerAlerts(newAlerts) {
const existingIds = new Set(bannerAlerts.value.map(alert => alert.id))
@@ -85,7 +85,7 @@ export const useAlertStore = defineStore('alerts', () => {
const updated = await alertsApi.acknowledgeAlert(alertId, note || undefined)
const alert = alerts.value.find(a => a.id === alertId)
if (alert) {
alert.status = updated.status ?? 'Acknowledged'
alert.status = updated.status ?? 'ACKNOWLEDGED'
alert.acknowledgedBy = updated.acknowledgedBy ?? alert.acknowledgedBy
alert.acknowledgedAt = updated.acknowledgedAt ?? alert.acknowledgedAt
}
@@ -96,7 +96,7 @@ export const useAlertStore = defineStore('alerts', () => {
async function resolve(alertId) {
await alertsApi.resolveAlert(alertId)
const alert = alerts.value.find(a => a.id === alertId)
if (alert) alert.status = 'Resolved'
if (alert) alert.status = 'RESOLVED'
dismissBannerAlert(alertId)
}
@@ -12,7 +12,7 @@ import EmptyState from '@/components/ui/EmptyState.vue'
const alertStore = useAlertStore()
const { alerts, loading } = storeToRefs(alertStore)
const activeFilter = ref('Open')
const activeFilter = ref('OPEN')
const confirmingAlert = ref(null)
watch(activeFilter, (status) => {
@@ -68,7 +68,7 @@ const selectedAlert = ref(null)
let nextAlertIndex = 0
const openAlerts = computed(() =>
alerts.value.filter(a => a.status === 'Open' || a.status === 'Escalated'),
alerts.value.filter(a => a.status === 'OPEN' || a.status === 'ESCALATED'),
)
const isDischarged = computed(() => encounter.value?.status === 'Discharged')