Schedule graphical CCS procedures

This commit is contained in:
hz
2026-08-14 17:54:03 -05:00
parent 375b3ea62f
commit 25c2188264
7 changed files with 204 additions and 26 deletions
+5
View File
@@ -128,6 +128,11 @@ public sealed class CcsCaseTests
Assert.Contains(request.Orders, value => value.Code == "bismuth_quadruple"); Assert.Contains(request.Orders, value => value.Code == "bismuth_quadruple");
Assert.Contains(request.Cases.Single().OrderResponses, Assert.Contains(request.Cases.Single().OrderResponses,
value => value.OrderCode == "upper_endoscopy" && value.Result.Contains("duodenal", StringComparison.OrdinalIgnoreCase)); value => value.OrderCode == "upper_endoscopy" && value.Result.Contains("duodenal", StringComparison.OrdinalIgnoreCase));
var endoscopy = request.Cases.Single().OrderResponses.Single(value => value.OrderCode == "upper_endoscopy");
Assert.Equal("endoscopy_suite", endoscopy.PerformanceLocation);
Assert.True(endoscopy.RequiresPatientVisit);
Assert.All(request.Cases.Single().OrderResponses.Where(value => value.RequiresPatientVisit),
value => Assert.False(string.IsNullOrWhiteSpace(value.PerformanceLocation)));
Assert.Contains(request.Cases.Single().ScoringCriteria, Assert.Contains(request.Cases.Single().ScoringCriteria,
value => value.TargetCode == "ibuprofen" && value.Points < 0); value => value.TargetCode == "ibuprofen" && value.Points < 0);
} }
+17
View File
@@ -164,6 +164,8 @@ public class CcsCaseOrderDefinition
{ {
public string OrderCode { get; set; } = ""; public string OrderCode { get; set; } = "";
public int? TurnaroundMinutes { get; set; } public int? TurnaroundMinutes { get; set; }
public string PerformanceLocation { get; set; } = "";
public bool RequiresPatientVisit { get; set; }
public string Result { get; set; } = ""; public string Result { get; set; } = "";
public string ResultSection { get; set; } = ""; public string ResultSection { get; set; } = "";
public Dictionary<string, string> VitalPatches { get; set; } = []; public Dictionary<string, string> VitalPatches { get; set; } = [];
@@ -243,6 +245,8 @@ public class CcsPendingResult
public string Category { get; set; } = ""; public string Category { get; set; } = "";
public string Title { get; set; } = ""; public string Title { get; set; } = "";
public int DueMinute { get; set; } public int DueMinute { get; set; }
public string PerformanceLocation { get; set; } = "";
public bool RequiresPatientVisit { get; set; }
public string Result { get; set; } = ""; public string Result { get; set; } = "";
public Dictionary<string, string> VitalPatches { get; set; } = []; public Dictionary<string, string> VitalPatches { get; set; } = [];
} }
@@ -271,10 +275,22 @@ public class CcsActiveOrder
public string Frequency { get; set; } = ""; public string Frequency { get; set; } = "";
public int OrderedMinute { get; set; } public int OrderedMinute { get; set; }
public int? ReportMinute { get; set; } public int? ReportMinute { get; set; }
public int? DueMinute { get; set; }
public string PerformanceLocation { get; set; } = "";
public bool RequiresPatientVisit { get; set; }
public string Status { get; set; } = "active";
public bool IsPending { get; set; } public bool IsPending { get; set; }
public bool HasResult { get; set; } public bool HasResult { get; set; }
} }
public class CcsOrderOption
{
public string Code { get; set; } = "";
public int TurnaroundMinutes { get; set; }
public string PerformanceLocation { get; set; } = "";
public bool RequiresPatientVisit { get; set; }
}
public class CcsCaseScore public class CcsCaseScore
{ {
public double Percent { get; set; } public double Percent { get; set; }
@@ -385,6 +401,7 @@ public class CcsAttemptResponse
public List<CcsChartEntry> Chart { get; set; } = []; public List<CcsChartEntry> Chart { get; set; } = [];
public List<string> ActiveOrderCodes { get; set; } = []; public List<string> ActiveOrderCodes { get; set; } = [];
public List<CcsActiveOrder> ActiveOrders { get; set; } = []; public List<CcsActiveOrder> ActiveOrders { get; set; } = [];
public List<CcsOrderOption> OrderOptions { get; set; } = [];
public List<CcsVitalSnapshot> VitalHistory { get; set; } = []; public List<CcsVitalSnapshot> VitalHistory { get; set; } = [];
public List<CcsPendingResult> PendingResults { get; set; } = []; public List<CcsPendingResult> PendingResults { get; set; } = [];
public List<CcsExamDefinition> AvailableExams { get; set; } = []; public List<CcsExamDefinition> AvailableExams { get; set; } = [];
@@ -60,6 +60,9 @@ public static class CcsCaseValidator
foreach (var response in item.OrderResponses) foreach (var response in item.OrderResponses)
{ {
if (!orderCodes.Contains(response.OrderCode)) errors.Add(prefix + $"unknown order code {response.OrderCode}"); if (!orderCodes.Contains(response.OrderCode)) errors.Add(prefix + $"unknown order code {response.OrderCode}");
if (response.TurnaroundMinutes < 0) errors.Add(prefix + $"order response {response.OrderCode} turnaroundMinutes cannot be negative");
if (response.RequiresPatientVisit && string.IsNullOrWhiteSpace(response.PerformanceLocation))
errors.Add(prefix + $"order response {response.OrderCode} requires a performanceLocation");
if (!string.IsNullOrWhiteSpace(response.ResultSection) && !CcsChartCategories.ResultSections.Contains(response.ResultSection)) if (!string.IsNullOrWhiteSpace(response.ResultSection) && !CcsChartCategories.ResultSections.Contains(response.ResultSection))
errors.Add(prefix + $"order response {response.OrderCode} resultSection is invalid"); errors.Add(prefix + $"order response {response.OrderCode} resultSection is invalid");
} }
+54 -6
View File
@@ -411,6 +411,23 @@ public sealed class CcsStore
{ {
var definition = await DefinitionAsync(attempt); var definition = await DefinitionAsync(attempt);
var completed = attempt.State == CcsAttemptStates.Completed; var completed = attempt.State == CcsAttemptStates.Completed;
var optionCodes = definition.OrderResponses.Select(response => response.OrderCode).ToList();
List<CcsOrderDefinition> optionCatalog = definition.ExperienceMode == CcsExperienceModes.GraphicalInterview
? await _orders.Find(value => optionCodes.Contains(value.Code)).ToListAsync()
: [];
var orderOptions = definition.ExperienceMode == CcsExperienceModes.GraphicalInterview
? definition.OrderResponses.Select(response =>
{
var catalog = optionCatalog.FirstOrDefault(value => value.Code.Equals(response.OrderCode, StringComparison.OrdinalIgnoreCase));
return new CcsOrderOption
{
Code = response.OrderCode,
TurnaroundMinutes = response.TurnaroundMinutes ?? catalog?.DefaultTurnaroundMinutes ?? 0,
PerformanceLocation = PerformanceLocation(response, catalog, attempt.Location),
RequiresPatientVisit = response.RequiresPatientVisit
};
}).ToList()
: [];
List<CcsOrderDefinition> activeCatalog = attempt.ActiveOrderCodes.Count == 0 List<CcsOrderDefinition> activeCatalog = attempt.ActiveOrderCodes.Count == 0
? [] ? []
: await _orders.Find(value => attempt.ActiveOrderCodes.Contains(value.Code)).ToListAsync(); : await _orders.Find(value => attempt.ActiveOrderCodes.Contains(value.Code)).ToListAsync();
@@ -423,11 +440,19 @@ public sealed class CcsStore
var reported = attempt.Chart.LastOrDefault(value => var reported = attempt.Chart.LastOrDefault(value =>
value.Code.Equals(order.Code, StringComparison.OrdinalIgnoreCase) value.Code.Equals(order.Code, StringComparison.OrdinalIgnoreCase)
&& value.Category is not CcsChartCategories.Orders); && value.Category is not CcsChartCategories.Orders);
var response = definition.OrderResponses.FirstOrDefault(value =>
value.OrderCode.Equals(order.Code, StringComparison.OrdinalIgnoreCase));
var performanceLocation = pending?.PerformanceLocation ?? PerformanceLocation(response, order, attempt.Location);
var requiresPatientVisit = pending?.RequiresPatientVisit ?? response?.RequiresPatientVisit == true;
return new CcsActiveOrder return new CcsActiveOrder
{ {
Code = order.Code, Name = order.CanonicalName, Category = order.Category, Code = order.Code, Name = order.CanonicalName, Category = order.Category,
Route = order.DisplayRoute, Frequency = order.DisplayFrequency, OrderedMinute = orderedMinute, Route = order.DisplayRoute, Frequency = order.DisplayFrequency, OrderedMinute = orderedMinute,
ReportMinute = pending is not null ? null : reported?.SimulatedMinute, ReportMinute = pending is not null ? null : reported?.SimulatedMinute,
DueMinute = pending?.DueMinute, PerformanceLocation = performanceLocation,
RequiresPatientVisit = requiresPatientVisit,
Status = pending is not null ? (requiresPatientVisit ? "scheduled" : "pending")
: reported is not null ? "completed" : "active",
IsPending = pending is not null, HasResult = reported is not null IsPending = pending is not null, HasResult = reported is not null
}; };
}).OrderBy(value => value.OrderedMinute).ThenBy(value => value.Name).ToList(); }).OrderBy(value => value.OrderedMinute).ThenBy(value => value.Name).ToList();
@@ -469,10 +494,11 @@ public sealed class CcsStore
? [] : definition.InitialChart.HistorySections, ? [] : definition.InitialChart.HistorySections,
MedicalHistory = MedicalHistory(definition), MedicalHistory = MedicalHistory(definition),
Vitals = attempt.CurrentVitals, Chart = attempt.Chart, ActiveOrderCodes = attempt.ActiveOrderCodes, Vitals = attempt.CurrentVitals, Chart = attempt.Chart, ActiveOrderCodes = attempt.ActiveOrderCodes,
ActiveOrders = activeOrders, VitalHistory = vitalHistory, ActiveOrders = activeOrders, OrderOptions = orderOptions, VitalHistory = vitalHistory,
PendingResults = attempt.PendingResults.Select(value => new CcsPendingResult PendingResults = attempt.PendingResults.Select(value => new CcsPendingResult
{ {
OrderCode = value.OrderCode, DueMinute = value.DueMinute, Category = value.Category, Title = value.Title OrderCode = value.OrderCode, DueMinute = value.DueMinute, Category = value.Category, Title = value.Title,
PerformanceLocation = value.PerformanceLocation, RequiresPatientVisit = value.RequiresPatientVisit
}).ToList(), }).ToList(),
AvailableExams = attempt.State == CcsAttemptStates.Active ? definition.Exams : [], AvailableExams = attempt.State == CcsAttemptStates.Active ? definition.Exams : [],
AllowedLocations = attempt.State == CcsAttemptStates.Active ? definition.AllowedLocations : [], AllowedLocations = attempt.State == CcsAttemptStates.Active ? definition.AllowedLocations : [],
@@ -525,23 +551,31 @@ public sealed class CcsStore
{ {
code = code.Trim().ToLowerInvariant(); code = code.Trim().ToLowerInvariant();
var catalog = await _orders.Find(value => value.Code == code).FirstOrDefaultAsync() ?? throw new ArgumentException("Unknown order code."); var catalog = await _orders.Find(value => value.Code == code).FirstOrDefaultAsync() ?? throw new ArgumentException("Unknown order code.");
if (catalog.AvailableLocations.Count > 0 && !catalog.AvailableLocations.Contains(attempt.Location, StringComparer.OrdinalIgnoreCase)) var response = definition.OrderResponses.FirstOrDefault(value => value.OrderCode.Equals(code, StringComparison.OrdinalIgnoreCase));
if (definition.ExperienceMode == CcsExperienceModes.GraphicalInterview && response is null)
throw new InvalidOperationException("That order is not configured for this case.");
if (response is null && catalog.AvailableLocations.Count > 0 && !catalog.AvailableLocations.Contains(attempt.Location, StringComparer.OrdinalIgnoreCase))
throw new InvalidOperationException("That order is unavailable at the current location."); throw new InvalidOperationException("That order is unavailable at the current location.");
attempt.Actions.Add(Action("order", code, attempt.SimulatedMinute, now)); attempt.Actions.Add(Action("order", code, attempt.SimulatedMinute, now));
if (!attempt.ActiveOrderCodes.Contains(code, StringComparer.OrdinalIgnoreCase)) attempt.ActiveOrderCodes.Add(code); if (!attempt.ActiveOrderCodes.Contains(code, StringComparer.OrdinalIgnoreCase)) attempt.ActiveOrderCodes.Add(code);
var response = definition.OrderResponses.FirstOrDefault(value => value.OrderCode.Equals(code, StringComparison.OrdinalIgnoreCase));
var delay = response?.TurnaroundMinutes ?? catalog.DefaultTurnaroundMinutes; var delay = response?.TurnaroundMinutes ?? catalog.DefaultTurnaroundMinutes;
var performanceLocation = PerformanceLocation(response, catalog, attempt.Location);
var requiresPatientVisit = response?.RequiresPatientVisit == true;
var resultCategory = ResultCategory(response?.ResultSection, catalog.ResultSection, catalog.Category); var resultCategory = ResultCategory(response?.ResultSection, catalog.ResultSection, catalog.Category);
if (response is not null && (!string.IsNullOrWhiteSpace(response.Result) || response.VitalPatches.Count > 0)) if (response is not null && (!string.IsNullOrWhiteSpace(response.Result) || response.VitalPatches.Count > 0))
attempt.PendingResults.Add(new CcsPendingResult attempt.PendingResults.Add(new CcsPendingResult
{ {
OrderCode = code, Category = resultCategory, Title = catalog.CanonicalName, OrderCode = code, Category = resultCategory, Title = catalog.CanonicalName,
DueMinute = attempt.SimulatedMinute + delay, Result = response.Result, VitalPatches = response.VitalPatches DueMinute = attempt.SimulatedMinute + delay, Result = response.Result, VitalPatches = response.VitalPatches,
PerformanceLocation = performanceLocation, RequiresPatientVisit = requiresPatientVisit
}); });
attempt.Chart.Add(new CcsChartEntry attempt.Chart.Add(new CcsChartEntry
{ {
Category = CcsChartCategories.Orders, Code = code, Title = catalog.CanonicalName, Category = CcsChartCategories.Orders, Code = code, Title = catalog.CanonicalName,
SimulatedMinute = attempt.SimulatedMinute, Text = $"Ordered: {catalog.CanonicalName}" SimulatedMinute = attempt.SimulatedMinute,
Text = requiresPatientVisit
? $"Scheduled: {catalog.CanonicalName} at {Humanize(performanceLocation)}; expected in {delay} minutes."
: $"Ordered: {catalog.CanonicalName}"
}); });
if (delay == 0 && processImmediately) ProcessDue(attempt); if (delay == 0 && processImmediately) ProcessDue(attempt);
} }
@@ -606,6 +640,13 @@ public sealed class CcsStore
var vitalsChanged = false; var vitalsChanged = false;
foreach (var result in due) foreach (var result in due)
{ {
if (result.RequiresPatientVisit)
attempt.Chart.Add(new CcsChartEntry
{
Category = CcsChartCategories.Progress, Code = $"{result.OrderCode}_completed", Title = "Patient returned",
SimulatedMinute = attempt.SimulatedMinute,
Text = $"The patient completed {result.Title} at {Humanize(result.PerformanceLocation)} and returned."
});
if (!string.IsNullOrWhiteSpace(result.Result)) if (!string.IsNullOrWhiteSpace(result.Result))
attempt.Chart.Add(new CcsChartEntry attempt.Chart.Add(new CcsChartEntry
{ {
@@ -619,6 +660,13 @@ public sealed class CcsStore
if (vitalsChanged) SnapshotVitals(attempt); if (vitalsChanged) SnapshotVitals(attempt);
} }
private static string PerformanceLocation(CcsCaseOrderDefinition? response, CcsOrderDefinition? catalog, string currentLocation)
{
if (!string.IsNullOrWhiteSpace(response?.PerformanceLocation)) return response.PerformanceLocation;
if (catalog?.AvailableLocations.Contains(currentLocation, StringComparer.OrdinalIgnoreCase) == true) return currentLocation;
return catalog?.AvailableLocations.FirstOrDefault() ?? currentLocation;
}
private static bool ProcessEvents(CcsAttempt attempt, CcsCaseRevision definition) private static bool ProcessEvents(CcsAttempt attempt, CcsCaseRevision definition)
{ {
var activeOrders = attempt.Actions.Where(value => value.Type == "order").Select(value => value.Code).ToHashSet(StringComparer.OrdinalIgnoreCase); var activeOrders = attempt.Actions.Where(value => value.Type == "order").Select(value => value.Code).ToHashSet(StringComparer.OrdinalIgnoreCase);
+6 -6
View File
@@ -138,16 +138,16 @@
{ "code": "neuro_psych", "name": "Neuro / Psych", "durationMinutes": 1, "finding": "He is alert and fully oriented with normal speech, strength, sensation, coordination, gait, mood, and affect." } { "code": "neuro_psych", "name": "Neuro / Psych", "durationMinutes": 1, "finding": "He is alert and fully oriented with normal speech, strength, sensation, coordination, gait, mood, and affect." }
], ],
"orderResponses": [ "orderResponses": [
{ "orderCode": "cbc", "turnaroundMinutes": 20, "result": "Hemoglobin is 10.4 g/dL, hematocrit 32%, MCV 74 fL, leukocyte count 7,100/mm3, and platelet count 312,000/mm3.", "resultSection": "lab" }, { "orderCode": "cbc", "turnaroundMinutes": 20, "performanceLocation": "outpatient_laboratory", "requiresPatientVisit": true, "result": "Hemoglobin is 10.4 g/dL, hematocrit 32%, MCV 74 fL, leukocyte count 7,100/mm3, and platelet count 312,000/mm3.", "resultSection": "lab" },
{ "orderCode": "cmp", "turnaroundMinutes": 25, "result": "BUN is 27 mg/dL and creatinine 1.0 mg/dL. Electrolytes, glucose, aminotransferases, bilirubin, and albumin are within reference ranges.", "resultSection": "lab" }, { "orderCode": "cmp", "turnaroundMinutes": 25, "performanceLocation": "outpatient_laboratory", "requiresPatientVisit": true, "result": "BUN is 27 mg/dL and creatinine 1.0 mg/dL. Electrolytes, glucose, aminotransferases, bilirubin, and albumin are within reference ranges.", "resultSection": "lab" },
{ "orderCode": "iron_studies", "turnaroundMinutes": 30, "result": "Ferritin is 10 ng/mL, serum iron 31 mcg/dL, and transferrin saturation 9%, supporting iron deficiency.", "resultSection": "lab" }, { "orderCode": "iron_studies", "turnaroundMinutes": 30, "performanceLocation": "outpatient_laboratory", "requiresPatientVisit": true, "result": "Ferritin is 10 ng/mL, serum iron 31 mcg/dL, and transferrin saturation 9%, supporting iron deficiency.", "resultSection": "lab" },
{ "orderCode": "h_pylori_breath", "turnaroundMinutes": 45, "result": "Urea breath test is positive for Helicobacter pylori.", "resultSection": "other_test" }, { "orderCode": "h_pylori_breath", "turnaroundMinutes": 45, "performanceLocation": "office_diagnostic_area", "requiresPatientVisit": true, "result": "Urea breath test is positive for Helicobacter pylori.", "resultSection": "other_test" },
{ "orderCode": "upper_endoscopy", "turnaroundMinutes": 60, "result": "Upper endoscopy shows an 8 mm clean-based ulcer in the duodenal bulb without active bleeding. Gastric biopsy is positive for H. pylori and shows no dysplasia or malignancy.", "resultSection": "other_test" }, { "orderCode": "upper_endoscopy", "turnaroundMinutes": 60, "performanceLocation": "endoscopy_suite", "requiresPatientVisit": true, "result": "Upper endoscopy shows an 8 mm clean-based ulcer in the duodenal bulb without active bleeding. Gastric biopsy is positive for H. pylori and shows no dysplasia or malignancy.", "resultSection": "other_test" },
{ "orderCode": "omeprazole", "turnaroundMinutes": 0, "result": "Proton-pump inhibitor therapy is started.", "resultSection": "treatment" }, { "orderCode": "omeprazole", "turnaroundMinutes": 0, "result": "Proton-pump inhibitor therapy is started.", "resultSection": "treatment" },
{ "orderCode": "bismuth_quadruple", "turnaroundMinutes": 0, "result": "A 14-day optimized bismuth quadruple regimen is prescribed.", "resultSection": "treatment" }, { "orderCode": "bismuth_quadruple", "turnaroundMinutes": 0, "result": "A 14-day optimized bismuth quadruple regimen is prescribed.", "resultSection": "treatment" },
{ "orderCode": "h_pylori_test_of_cure", "turnaroundMinutes": 0, "result": "Test-of-cure follow-up is scheduled for at least four weeks after therapy, with PPI held for two weeks before testing.", "resultSection": "treatment" }, { "orderCode": "h_pylori_test_of_cure", "turnaroundMinutes": 0, "result": "Test-of-cure follow-up is scheduled for at least four weeks after therapy, with PPI held for two weeks before testing.", "resultSection": "treatment" },
{ "orderCode": "gastro_follow_up", "turnaroundMinutes": 0, "result": "Gastroenterology follow-up is arranged to review healing, anemia, and eradication testing.", "resultSection": "treatment" }, { "orderCode": "gastro_follow_up", "turnaroundMinutes": 0, "result": "Gastroenterology follow-up is arranged to review healing, anemia, and eradication testing.", "resultSection": "treatment" },
{ "orderCode": "abdominal_ct", "turnaroundMinutes": 60, "result": "CT of the abdomen and pelvis shows no acute intra-abdominal abnormality.", "resultSection": "imaging" }, { "orderCode": "abdominal_ct", "turnaroundMinutes": 60, "performanceLocation": "imaging_center", "requiresPatientVisit": true, "result": "CT of the abdomen and pelvis shows no acute intra-abdominal abnormality.", "resultSection": "imaging" },
{ "orderCode": "clarithromycin_triple", "turnaroundMinutes": 0, "result": "Empiric clarithromycin-based therapy is prescribed without susceptibility confirmation.", "resultSection": "treatment" }, { "orderCode": "clarithromycin_triple", "turnaroundMinutes": 0, "result": "Empiric clarithromycin-based therapy is prescribed without susceptibility confirmation.", "resultSection": "treatment" },
{ "orderCode": "ibuprofen", "turnaroundMinutes": 0, "result": "Ibuprofen is prescribed despite suspected peptic ulcer disease and possible gastrointestinal bleeding.", "resultSection": "treatment" } { "orderCode": "ibuprofen", "turnaroundMinutes": 0, "result": "Ibuprofen is prescribed despite suspected peptic ulcer disease and possible gastrointestinal bleeding.", "resultSection": "treatment" }
], ],
+104 -12
View File
@@ -88,10 +88,16 @@ var orders_status: Label
var order_search_timer: Timer var order_search_timer: Timer
var staged_order_codes: Array[String] = [] var staged_order_codes: Array[String] = []
var order_names := {} var order_names := {}
var order_options := {}
var final_orders_mode := false var final_orders_mode := false
var active_order_codes: Array = [] var active_order_codes: Array = []
var current_order_location := "" var current_order_location := ""
var current_order_minute := 0
var known_order_result_count := -1 var known_order_result_count := -1
var known_visit_completions: Array[String] = []
var visit_completion_tracking_ready := false
var patient_update_panel: PanelContainer
var patient_update_text: Label
var experience_mode := "management" var experience_mode := "management"
var pending_choices: Array = [] var pending_choices: Array = []
var available_choices: Array = [] var available_choices: Array = []
@@ -132,6 +138,7 @@ func _ready() -> void:
_style_desktop_shortcut(orders_launcher) _style_desktop_shortcut(orders_launcher)
_build_medical_history_tabs() _build_medical_history_tabs()
_build_orders_window() _build_orders_window()
_build_patient_update()
_resize_computer_windows() _resize_computer_windows()
_update_clock() _update_clock()
_schedule_patient_blink() _schedule_patient_blink()
@@ -315,6 +322,8 @@ func complete_response_transition() -> void:
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
if patient_update_panel != null and patient_update_panel.visible:
return
if computer_overlay.visible or (physical_exam_panel != null and physical_exam_panel.visible): if computer_overlay.visible or (physical_exam_panel != null and physical_exam_panel.visible):
return return
if _event_targets_computer_launcher(event): if _event_targets_computer_launcher(event):
@@ -370,6 +379,9 @@ func is_computer_open() -> bool:
func close_transient_overlay() -> bool: func close_transient_overlay() -> bool:
if patient_update_panel != null and patient_update_panel.visible:
patient_update_panel.visible = false
return true
if physical_exam_panel != null and physical_exam_panel.visible: if physical_exam_panel != null and physical_exam_panel.visible:
if physical_exam_result_button.visible: if physical_exam_result_button.visible:
_close_exam_result() _close_exam_result()
@@ -591,22 +603,29 @@ func _queue_live_order_search(query: String) -> void:
func render_order_search(rows: Array) -> void: func render_order_search(rows: Array) -> void:
_clear_dynamic_children(orders_search_results) _clear_dynamic_children(orders_search_results)
var available_count := 0 var result_count := 0
for raw_order in rows: for raw_order in rows:
var order: Dictionary = raw_order var order: Dictionary = raw_order
var locations: Array = order.get("availableLocations", []) result_count += 1
if not locations.is_empty() and current_order_location not in locations:
continue
available_count += 1
var code := str(order.get("code", "")) var code := str(order.get("code", ""))
order_names[code] = str(order.get("canonicalName", code.capitalize())) order_names[code] = str(order.get("canonicalName", code.capitalize()))
var option: Dictionary = order_options.get(code, {})
var configured := not option.is_empty()
var button := Button.new() var button := Button.new()
button.text = "%s · %s" % [order_names[code], str(order.get("category", "")).capitalize()] var detail := str(order.get("category", "")).capitalize()
if configured:
var location := _humanize_order_location(str(option.get("performanceLocation", current_order_location)))
var delay := int(option.get("turnaroundMinutes", 0))
detail += " - %s%s" % [location, " - %d min" % delay if delay > 0 else ""]
else:
detail += " - Unavailable in this case"
button.text = "%s\n%s" % [order_names[code], detail]
button.alignment = HORIZONTAL_ALIGNMENT_LEFT button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.disabled = code in staged_order_codes or code in active_order_codes button.disabled = not configured or code in staged_order_codes or code in active_order_codes
button.tooltip_text = "This order is not configured for this case." if not configured else ""
button.pressed.connect(_stage_order.bind(code)) button.pressed.connect(_stage_order.bind(code))
orders_search_results.add_child(button) orders_search_results.add_child(button)
orders_status.text = "" if available_count > 0 else "No matching orders are available at this location." orders_status.text = "" if result_count > 0 else "No matching orders found."
func _stage_order(code: String) -> void: func _stage_order(code: String) -> void:
@@ -622,7 +641,9 @@ func _render_staged_orders() -> void:
var row := HBoxContainer.new() var row := HBoxContainer.new()
orders_staged.add_child(row) orders_staged.add_child(row)
var label := Label.new() var label := Label.new()
label.text = str(order_names.get(code, code.capitalize())) var option: Dictionary = order_options.get(code, {})
var location := _humanize_order_location(str(option.get("performanceLocation", current_order_location)))
label.text = "%s - %s" % [str(order_names.get(code, code.capitalize())), location]
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(label) row.add_child(label)
var remove := Button.new() var remove := Button.new()
@@ -647,12 +668,18 @@ func configure_orders(attempt: Dictionary) -> void:
if orders_window == null: if orders_window == null:
return return
current_order_location = str(attempt.get("location", "")) current_order_location = str(attempt.get("location", ""))
current_order_minute = int(attempt.get("simulatedMinute", 0))
order_options.clear()
for raw_option in attempt.get("orderOptions", []):
var option: Dictionary = raw_option
order_options[str(option.get("code", ""))] = option
active_order_codes = attempt.get("activeOrderCodes", []).duplicate() active_order_codes = attempt.get("activeOrderCodes", []).duplicate()
for code in attempt.get("activeOrderCodes", []): for code in attempt.get("activeOrderCodes", []):
staged_order_codes.erase(str(code)) staged_order_codes.erase(str(code))
_render_staged_orders() _render_staged_orders()
_render_active_orders(attempt.get("activeOrders", [])) _render_active_orders(attempt.get("activeOrders", []))
_render_order_results(attempt.get("chart", [])) _render_order_results(attempt.get("chart", []))
_track_patient_visit_completions(attempt.get("chart", []))
var result_count := _order_result_count(attempt.get("chart", [])) var result_count := _order_result_count(attempt.get("chart", []))
if known_order_result_count >= 0 and result_count > known_order_result_count and not orders_window.visible: if known_order_result_count >= 0 and result_count > known_order_result_count and not orders_window.visible:
orders_launcher.get_node("Label").text = "Orders •" orders_launcher.get_node("Label").text = "Orders •"
@@ -676,13 +703,13 @@ func show_order_action_complete(action: String) -> void:
match action: match action:
"orders": "orders":
orders_tabs.current_tab = 1 orders_tabs.current_tab = 1
orders_status.text = "Orders confirmed. Use Obtain Results for pending tests." orders_status.text = "Orders confirmed and automatically scheduled. Use Obtain Results to advance time."
"cancel_order": "cancel_order":
orders_tabs.current_tab = 1 orders_tabs.current_tab = 1
orders_status.text = "Order cancelled." orders_status.text = "Order cancelled."
"advance": "advance":
orders_tabs.current_tab = 2 orders_tabs.current_tab = 2
orders_status.text = "The chart has been updated with all results now available." orders_status.text = "Simulated time advanced. Newly available results are shown here."
func _render_active_orders(rows: Array) -> void: func _render_active_orders(rows: Array) -> void:
@@ -697,7 +724,12 @@ func _render_active_orders(rows: Array) -> void:
var row := HBoxContainer.new() var row := HBoxContainer.new()
orders_active.add_child(row) orders_active.add_child(row)
var label := Label.new() var label := Label.new()
label.text = "%s — %s, %s" % [str(order.get("name", "Order")), str(order.get("route", "")), str(order.get("frequency", ""))] var status := str(order.get("status", "active")).capitalize()
var location := _humanize_order_location(str(order.get("performanceLocation", current_order_location)))
var due_text := ""
if order.get("dueMinute", null) != null:
due_text = " - due in %d min" % maxi(0, int(order.get("dueMinute", 0)) - current_order_minute)
label.text = "%s - %s at %s%s" % [str(order.get("name", "Order")), status, location, due_text]
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(label) row.add_child(label)
var cancel := Button.new() var cancel := Button.new()
@@ -723,6 +755,66 @@ func _order_result_count(chart: Array) -> int:
return count return count
func _humanize_order_location(value: String) -> String:
if value.is_empty():
return "Current location"
return value.replace("_", " ").capitalize()
func _build_patient_update() -> void:
patient_update_panel = PanelContainer.new()
patient_update_panel.name = "PatientProcedureUpdate"
patient_update_panel.visible = false
patient_update_panel.z_index = 100
patient_update_panel.position = Vector2(400, 300)
patient_update_panel.custom_minimum_size = Vector2(800, 0)
add_child(patient_update_panel)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 24)
margin.add_theme_constant_override("margin_right", 24)
margin.add_theme_constant_override("margin_top", 18)
margin.add_theme_constant_override("margin_bottom", 18)
patient_update_panel.add_child(margin)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 12)
margin.add_child(content)
var heading := Label.new()
heading.text = "Patient Update"
heading.add_theme_font_size_override("font_size", 24)
content.add_child(heading)
patient_update_text = Label.new()
patient_update_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
patient_update_text.add_theme_font_size_override("font_size", 18)
content.add_child(patient_update_text)
var dismiss := Button.new()
dismiss.text = "Continue"
dismiss.custom_minimum_size = Vector2(140, 44)
dismiss.size_flags_horizontal = Control.SIZE_SHRINK_END
dismiss.pressed.connect(func() -> void: patient_update_panel.visible = false)
content.add_child(dismiss)
func _track_patient_visit_completions(chart: Array) -> void:
var current: Array[String] = []
var new_messages: Array[String] = []
for raw_entry in chart:
var entry: Dictionary = raw_entry
var code := str(entry.get("code", ""))
if str(entry.get("category", "")) != "progress" or not code.ends_with("_completed"):
continue
var key := "%s:%d" % [code, int(entry.get("simulatedMinute", 0))]
current.append(key)
if visit_completion_tracking_ready and key not in known_visit_completions:
new_messages.append(str(entry.get("text", "The patient completed a scheduled procedure and returned.")))
known_visit_completions = current
if not visit_completion_tracking_ready:
visit_completion_tracking_ready = true
return
if not new_messages.is_empty():
patient_update_text.text = "\n".join(new_messages) + "\nNew results are available in the computer."
patient_update_panel.visible = true
func set_orders_pending(pending: bool, message: String = "") -> void: func set_orders_pending(pending: bool, message: String = "") -> void:
orders_confirm_button.disabled = pending or staged_order_codes.is_empty() orders_confirm_button.disabled = pending or staged_order_codes.is_empty()
orders_advance_button.disabled = pending orders_advance_button.disabled = pending
+15 -2
View File
@@ -107,16 +107,28 @@ func _run() -> void:
{"code": "cbc", "canonicalName": "Complete blood count", "category": "laboratory", "availableLocations": []}, {"code": "cbc", "canonicalName": "Complete blood count", "category": "laboratory", "availableLocations": []},
{"code": "venous_blood_gas", "canonicalName": "Venous blood gas", "category": "laboratory", "availableLocations": ["emergency_department", "icu"]} {"code": "venous_blood_gas", "canonicalName": "Venous blood gas", "category": "laboratory", "availableLocations": ["emergency_department", "icu"]}
]) ])
_assert(screen.active_scene.orders_search_results.get_child_count() == 1, "order search should hide catalog entries unavailable at the current location") _assert(screen.active_scene.orders_search_results.get_child_count() == 2, "order search should keep unavailable catalog entries visible")
_assert(not (screen.active_scene.orders_search_results.get_child(0) as Button).disabled, "case-authored orders should remain selectable across performance locations")
_assert((screen.active_scene.orders_search_results.get_child(1) as Button).disabled, "orders not authored for this case should explain their unavailability")
(screen.active_scene.orders_search_results.get_child(0) as Button).pressed.emit() (screen.active_scene.orders_search_results.get_child(0) as Button).pressed.emit()
_assert(screen.active_scene.staged_order_codes == ["cbc"] and not screen.active_scene.orders_confirm_button.disabled, "catalog results should stage for confirmation") _assert(screen.active_scene.staged_order_codes == ["cbc"] and not screen.active_scene.orders_confirm_button.disabled, "catalog results should stage for confirmation")
screen.active_scene.configure_orders({ screen.active_scene.configure_orders({
"state": "active", "activeOrderCodes": ["cbc"], "state": "active", "activeOrderCodes": ["cbc"],
"activeOrders": [{"code": "cbc", "name": "Complete blood count", "route": "Blood", "frequency": "Once"}], "orderOptions": [{"code": "cbc", "performanceLocation": "outpatient_laboratory", "turnaroundMinutes": 20, "requiresPatientVisit": true}],
"activeOrders": [{"code": "cbc", "name": "Complete blood count", "status": "completed", "performanceLocation": "outpatient_laboratory"}],
"chart": [{"category": "lab", "code": "cbc", "title": "Complete blood count", "text": "Hemoglobin 10.4 g/dL."}] "chart": [{"category": "lab", "code": "cbc", "title": "Complete blood count", "text": "Hemoglobin 10.4 g/dL."}]
}) })
_assert(screen.active_scene.staged_order_codes.is_empty(), "confirmed active orders should leave the staged list") _assert(screen.active_scene.staged_order_codes.is_empty(), "confirmed active orders should leave the staged list")
_assert(screen.active_scene.orders_results.text.contains("Hemoglobin 10.4"), "returned results should render in the Orders results tab") _assert(screen.active_scene.orders_results.text.contains("Hemoglobin 10.4"), "returned results should render in the Orders results tab")
screen.active_scene.configure_orders({
"state": "active", "activeOrderCodes": ["cbc"], "orderOptions": [], "activeOrders": [],
"chart": [
{"category": "progress", "code": "cbc_completed", "simulatedMinute": 20, "text": "The patient completed Complete blood count at Outpatient Laboratory and returned."},
{"category": "lab", "code": "cbc", "title": "Complete blood count", "text": "Hemoglobin 10.4 g/dL."}
]
})
_assert(screen.active_scene.patient_update_panel.visible and screen.active_scene.patient_update_text.text.contains("returned"), "completed off-site work should produce an in-scene patient update")
screen.active_scene.patient_update_panel.visible = false
screen.active_scene.show_order_action_complete("orders") screen.active_scene.show_order_action_complete("orders")
_assert(screen.active_scene.orders_tabs.current_tab == 1 and screen.active_scene.orders_status.text.begins_with("Orders confirmed"), "successful submission should explain completion and open Active Orders") _assert(screen.active_scene.orders_tabs.current_tab == 1 and screen.active_scene.orders_status.text.begins_with("Orders confirmed"), "successful submission should explain completion and open Active Orders")
screen.active_scene.show_order_action_complete("advance") screen.active_scene.show_order_action_complete("advance")
@@ -351,6 +363,7 @@ func _sample_attempt(completed: bool) -> Dictionary:
"openingCompleted": completed, "openingCompleted": completed,
"experienceMode": "graphical_interview", "experienceMode": "graphical_interview",
"availableExams": _sample_exams(), "chart": [], "availableExams": _sample_exams(), "chart": [],
"orderOptions": [{"code": "cbc", "performanceLocation": "outpatient_laboratory", "turnaroundMinutes": 20, "requiresPatientVisit": true}],
"interview": {"currentNodeId": node.id, "currentNode": node, "visitedChoiceIds": [], "completed": completed} "interview": {"currentNodeId": node.id, "currentNode": node, "visitedChoiceIds": [], "completed": completed}
} }