Schedule graphical CCS procedures
This commit is contained in:
@@ -128,6 +128,11 @@ public sealed class CcsCaseTests
|
||||
Assert.Contains(request.Orders, value => value.Code == "bismuth_quadruple");
|
||||
Assert.Contains(request.Cases.Single().OrderResponses,
|
||||
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,
|
||||
value => value.TargetCode == "ibuprofen" && value.Points < 0);
|
||||
}
|
||||
|
||||
@@ -164,6 +164,8 @@ public class CcsCaseOrderDefinition
|
||||
{
|
||||
public string OrderCode { get; set; } = "";
|
||||
public int? TurnaroundMinutes { get; set; }
|
||||
public string PerformanceLocation { get; set; } = "";
|
||||
public bool RequiresPatientVisit { get; set; }
|
||||
public string Result { get; set; } = "";
|
||||
public string ResultSection { get; set; } = "";
|
||||
public Dictionary<string, string> VitalPatches { get; set; } = [];
|
||||
@@ -243,6 +245,8 @@ public class CcsPendingResult
|
||||
public string Category { get; set; } = "";
|
||||
public string Title { get; set; } = "";
|
||||
public int DueMinute { get; set; }
|
||||
public string PerformanceLocation { get; set; } = "";
|
||||
public bool RequiresPatientVisit { get; set; }
|
||||
public string Result { get; set; } = "";
|
||||
public Dictionary<string, string> VitalPatches { get; set; } = [];
|
||||
}
|
||||
@@ -271,10 +275,22 @@ public class CcsActiveOrder
|
||||
public string Frequency { get; set; } = "";
|
||||
public int OrderedMinute { 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 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 double Percent { get; set; }
|
||||
@@ -385,6 +401,7 @@ public class CcsAttemptResponse
|
||||
public List<CcsChartEntry> Chart { get; set; } = [];
|
||||
public List<string> ActiveOrderCodes { get; set; } = [];
|
||||
public List<CcsActiveOrder> ActiveOrders { get; set; } = [];
|
||||
public List<CcsOrderOption> OrderOptions { get; set; } = [];
|
||||
public List<CcsVitalSnapshot> VitalHistory { get; set; } = [];
|
||||
public List<CcsPendingResult> PendingResults { get; set; } = [];
|
||||
public List<CcsExamDefinition> AvailableExams { get; set; } = [];
|
||||
|
||||
@@ -60,6 +60,9 @@ public static class CcsCaseValidator
|
||||
foreach (var response in item.OrderResponses)
|
||||
{
|
||||
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))
|
||||
errors.Add(prefix + $"order response {response.OrderCode} resultSection is invalid");
|
||||
}
|
||||
|
||||
@@ -411,6 +411,23 @@ public sealed class CcsStore
|
||||
{
|
||||
var definition = await DefinitionAsync(attempt);
|
||||
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
|
||||
? []
|
||||
: await _orders.Find(value => attempt.ActiveOrderCodes.Contains(value.Code)).ToListAsync();
|
||||
@@ -423,11 +440,19 @@ public sealed class CcsStore
|
||||
var reported = attempt.Chart.LastOrDefault(value =>
|
||||
value.Code.Equals(order.Code, StringComparison.OrdinalIgnoreCase)
|
||||
&& 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
|
||||
{
|
||||
Code = order.Code, Name = order.CanonicalName, Category = order.Category,
|
||||
Route = order.DisplayRoute, Frequency = order.DisplayFrequency, OrderedMinute = orderedMinute,
|
||||
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
|
||||
};
|
||||
}).OrderBy(value => value.OrderedMinute).ThenBy(value => value.Name).ToList();
|
||||
@@ -469,10 +494,11 @@ public sealed class CcsStore
|
||||
? [] : definition.InitialChart.HistorySections,
|
||||
MedicalHistory = MedicalHistory(definition),
|
||||
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
|
||||
{
|
||||
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(),
|
||||
AvailableExams = attempt.State == CcsAttemptStates.Active ? definition.Exams : [],
|
||||
AllowedLocations = attempt.State == CcsAttemptStates.Active ? definition.AllowedLocations : [],
|
||||
@@ -525,23 +551,31 @@ public sealed class CcsStore
|
||||
{
|
||||
code = code.Trim().ToLowerInvariant();
|
||||
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.");
|
||||
attempt.Actions.Add(Action("order", code, attempt.SimulatedMinute, now));
|
||||
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 performanceLocation = PerformanceLocation(response, catalog, attempt.Location);
|
||||
var requiresPatientVisit = response?.RequiresPatientVisit == true;
|
||||
var resultCategory = ResultCategory(response?.ResultSection, catalog.ResultSection, catalog.Category);
|
||||
if (response is not null && (!string.IsNullOrWhiteSpace(response.Result) || response.VitalPatches.Count > 0))
|
||||
attempt.PendingResults.Add(new CcsPendingResult
|
||||
{
|
||||
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
|
||||
{
|
||||
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);
|
||||
}
|
||||
@@ -606,6 +640,13 @@ public sealed class CcsStore
|
||||
var vitalsChanged = false;
|
||||
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))
|
||||
attempt.Chart.Add(new CcsChartEntry
|
||||
{
|
||||
@@ -619,6 +660,13 @@ public sealed class CcsStore
|
||||
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)
|
||||
{
|
||||
var activeOrders = attempt.Actions.Where(value => value.Type == "order").Select(value => value.Code).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -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." }
|
||||
],
|
||||
"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": "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": "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": "h_pylori_breath", "turnaroundMinutes": 45, "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": "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, "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, "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, "performanceLocation": "office_diagnostic_area", "requiresPatientVisit": true, "result": "Urea breath test is positive for Helicobacter pylori.", "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": "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": "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": "ibuprofen", "turnaroundMinutes": 0, "result": "Ibuprofen is prescribed despite suspected peptic ulcer disease and possible gastrointestinal bleeding.", "resultSection": "treatment" }
|
||||
],
|
||||
|
||||
@@ -88,10 +88,16 @@ var orders_status: Label
|
||||
var order_search_timer: Timer
|
||||
var staged_order_codes: Array[String] = []
|
||||
var order_names := {}
|
||||
var order_options := {}
|
||||
var final_orders_mode := false
|
||||
var active_order_codes: Array = []
|
||||
var current_order_location := ""
|
||||
var current_order_minute := 0
|
||||
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 pending_choices: Array = []
|
||||
var available_choices: Array = []
|
||||
@@ -132,6 +138,7 @@ func _ready() -> void:
|
||||
_style_desktop_shortcut(orders_launcher)
|
||||
_build_medical_history_tabs()
|
||||
_build_orders_window()
|
||||
_build_patient_update()
|
||||
_resize_computer_windows()
|
||||
_update_clock()
|
||||
_schedule_patient_blink()
|
||||
@@ -315,6 +322,8 @@ func complete_response_transition() -> 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):
|
||||
return
|
||||
if _event_targets_computer_launcher(event):
|
||||
@@ -370,6 +379,9 @@ func is_computer_open() -> 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_result_button.visible:
|
||||
_close_exam_result()
|
||||
@@ -591,22 +603,29 @@ func _queue_live_order_search(query: String) -> void:
|
||||
|
||||
func render_order_search(rows: Array) -> void:
|
||||
_clear_dynamic_children(orders_search_results)
|
||||
var available_count := 0
|
||||
var result_count := 0
|
||||
for raw_order in rows:
|
||||
var order: Dictionary = raw_order
|
||||
var locations: Array = order.get("availableLocations", [])
|
||||
if not locations.is_empty() and current_order_location not in locations:
|
||||
continue
|
||||
available_count += 1
|
||||
result_count += 1
|
||||
var code := str(order.get("code", ""))
|
||||
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()
|
||||
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.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))
|
||||
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:
|
||||
@@ -622,7 +641,9 @@ func _render_staged_orders() -> void:
|
||||
var row := HBoxContainer.new()
|
||||
orders_staged.add_child(row)
|
||||
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
|
||||
row.add_child(label)
|
||||
var remove := Button.new()
|
||||
@@ -647,12 +668,18 @@ func configure_orders(attempt: Dictionary) -> void:
|
||||
if orders_window == null:
|
||||
return
|
||||
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()
|
||||
for code in attempt.get("activeOrderCodes", []):
|
||||
staged_order_codes.erase(str(code))
|
||||
_render_staged_orders()
|
||||
_render_active_orders(attempt.get("activeOrders", []))
|
||||
_render_order_results(attempt.get("chart", []))
|
||||
_track_patient_visit_completions(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:
|
||||
orders_launcher.get_node("Label").text = "Orders •"
|
||||
@@ -676,13 +703,13 @@ func show_order_action_complete(action: String) -> void:
|
||||
match action:
|
||||
"orders":
|
||||
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":
|
||||
orders_tabs.current_tab = 1
|
||||
orders_status.text = "Order cancelled."
|
||||
"advance":
|
||||
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:
|
||||
@@ -697,7 +724,12 @@ func _render_active_orders(rows: Array) -> void:
|
||||
var row := HBoxContainer.new()
|
||||
orders_active.add_child(row)
|
||||
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
|
||||
row.add_child(label)
|
||||
var cancel := Button.new()
|
||||
@@ -723,6 +755,66 @@ func _order_result_count(chart: Array) -> int:
|
||||
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:
|
||||
orders_confirm_button.disabled = pending or staged_order_codes.is_empty()
|
||||
orders_advance_button.disabled = pending
|
||||
|
||||
@@ -107,16 +107,28 @@ func _run() -> void:
|
||||
{"code": "cbc", "canonicalName": "Complete blood count", "category": "laboratory", "availableLocations": []},
|
||||
{"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()
|
||||
_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({
|
||||
"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."}]
|
||||
})
|
||||
_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")
|
||||
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")
|
||||
_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")
|
||||
@@ -351,6 +363,7 @@ func _sample_attempt(completed: bool) -> Dictionary:
|
||||
"openingCompleted": completed,
|
||||
"experienceMode": "graphical_interview",
|
||||
"availableExams": _sample_exams(), "chart": [],
|
||||
"orderOptions": [{"code": "cbc", "performanceLocation": "outpatient_laboratory", "turnaroundMinutes": 20, "requiresPatientVisit": true}],
|
||||
"interview": {"currentNodeId": node.id, "currentNode": node, "visitedChoiceIds": [], "completed": completed}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user