Fix issues with critical alerts breaking the simulation
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (9–12)',
|
||||
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 (9–12)',
|
||||
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 (91–110 bpm or 41–50 bpm).`,
|
||||
},
|
||||
WarningSystolicBp: {
|
||||
WARNING_SYSTOLIC_BP: {
|
||||
label: 'Systolic BP Warning',
|
||||
explain: (a) => `Systolic BP ${extractValue(a)} is in the warning range (101–110 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 5–6 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 9–12.',
|
||||
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 9–12.',
|
||||
}
|
||||
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')
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user