10 Commits
Author SHA1 Message Date
hz 030bcf6d7a Fix graphical final orders workflow
Deploy STEP3 UAT / deploy (push) Canceled after 0s
2026-08-14 19:31:31 -05:00
hz 25c2188264 Schedule graphical CCS procedures 2026-08-14 17:54:03 -05:00
hz 375b3ea62f Clarify and filter graphical orders 2026-08-14 17:02:35 -05:00
hz 325d27eec5 Add graphical CCS orders and finalization 2026-08-14 00:53:21 -05:00
hz 7da07cb2a4 Refine physical exam chart layout 2026-08-13 22:21:02 -05:00
hz 6312b45a92 Preserve dialogue after examinations 2026-08-13 20:54:50 -05:00
hz 07195dd24a Prioritize examination modal input 2026-08-12 01:48:15 -05:00
hz 026f0be174 Restore terminal examination review 2026-08-11 22:28:21 -05:00
hz 9a54bfc6d6 Integrate examinations into CCS interview 2026-08-11 22:27:37 -05:00
hz 6108011f74 Refresh stale CCS exam attempts 2026-08-10 21:30:43 -05:00
9 changed files with 952 additions and 78 deletions
+12 -1
View File
@@ -112,7 +112,7 @@ public sealed class CcsCaseTests
Assert.False(topics.Single(value => value.Id == "past_medical_history").Required);
Assert.Contains("cholesterol was high", topics.Single(value => value.Id == "past_medical_history").PatientText,
StringComparison.OrdinalIgnoreCase);
Assert.Equal(2, request.Cases.Single().GraphicalPresentation.InterviewRevision);
Assert.Equal(4, request.Cases.Single().GraphicalPresentation.InterviewRevision);
Assert.Equal("Hyperlipidemia", CcsStore.MedicalHistory(request.Cases.Single()));
var exams = request.Cases.Single().Exams;
Assert.Equal([
@@ -124,6 +124,17 @@ public sealed class CcsCaseTests
StringComparison.OrdinalIgnoreCase);
Assert.Contains("brown stool", exams.Single(value => value.Code == "rectal").Finding,
StringComparison.OrdinalIgnoreCase);
Assert.Contains(request.Orders, value => value.Code == "h_pylori_breath");
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);
}
[Fact]
+17
View File
@@ -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");
}
+54 -6
View File
@@ -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);
+39 -4
View File
@@ -1,5 +1,18 @@
{
"orders": [],
"orders": [
{ "code": "cbc", "canonicalName": "Complete blood count", "category": "laboratory", "aliases": ["cbc", "blood count", "hemoglobin"], "defaultTurnaroundMinutes": 20, "availableLocations": [], "displayRoute": "Blood", "displayFrequency": "Once", "resultSection": "lab" },
{ "code": "cmp", "canonicalName": "Comprehensive metabolic panel", "category": "laboratory", "aliases": ["cmp", "chemistry", "bun", "creatinine"], "defaultTurnaroundMinutes": 25, "availableLocations": [], "displayRoute": "Blood", "displayFrequency": "Once", "resultSection": "lab" },
{ "code": "iron_studies", "canonicalName": "Iron studies and ferritin", "category": "laboratory", "aliases": ["iron", "ferritin", "transferrin saturation"], "defaultTurnaroundMinutes": 30, "availableLocations": [], "displayRoute": "Blood", "displayFrequency": "Once", "resultSection": "lab" },
{ "code": "h_pylori_breath", "canonicalName": "Helicobacter pylori urea breath test", "category": "diagnostic", "aliases": ["h pylori", "urea breath test", "helicobacter"], "defaultTurnaroundMinutes": 45, "availableLocations": ["office"], "displayRoute": "Breath", "displayFrequency": "Once", "resultSection": "other_test" },
{ "code": "upper_endoscopy", "canonicalName": "Upper endoscopy with biopsy", "category": "procedure", "aliases": ["egd", "endoscopy", "gastroscopy", "biopsy"], "defaultTurnaroundMinutes": 60, "availableLocations": ["office"], "displayRoute": "Endoscopic", "displayFrequency": "Once", "resultSection": "other_test" },
{ "code": "omeprazole", "canonicalName": "Omeprazole acid suppression", "category": "medication", "aliases": ["ppi", "proton pump inhibitor", "acid suppression"], "defaultTurnaroundMinutes": 0, "availableLocations": [], "displayRoute": "Oral", "displayFrequency": "Twice daily", "resultSection": "treatment" },
{ "code": "bismuth_quadruple", "canonicalName": "Optimized bismuth quadruple therapy for 14 days", "category": "medication", "aliases": ["h pylori treatment", "bismuth", "tetracycline", "metronidazole"], "defaultTurnaroundMinutes": 0, "availableLocations": [], "displayRoute": "Oral", "displayFrequency": "14-day course", "resultSection": "treatment" },
{ "code": "h_pylori_test_of_cure", "canonicalName": "H. pylori test of cure after therapy", "category": "diagnostic", "aliases": ["test of cure", "repeat breath test", "eradication testing"], "defaultTurnaroundMinutes": 0, "availableLocations": [], "displayRoute": "Outpatient", "displayFrequency": "At least 4 weeks after therapy", "resultSection": "other_test" },
{ "code": "gastro_follow_up", "canonicalName": "Gastroenterology follow-up", "category": "disposition", "aliases": ["gi follow up", "gastroenterology", "follow-up"], "defaultTurnaroundMinutes": 0, "availableLocations": [], "displayRoute": "Outpatient", "displayFrequency": "Follow-up", "resultSection": "treatment" },
{ "code": "abdominal_ct", "canonicalName": "CT abdomen and pelvis with contrast", "category": "imaging", "aliases": ["ct abdomen", "cat scan"], "defaultTurnaroundMinutes": 60, "availableLocations": [], "displayRoute": "Imaging", "displayFrequency": "Once", "resultSection": "imaging" },
{ "code": "clarithromycin_triple", "canonicalName": "Empiric clarithromycin triple therapy", "category": "medication", "aliases": ["clarithromycin", "triple therapy", "prevpac"], "defaultTurnaroundMinutes": 0, "availableLocations": [], "displayRoute": "Oral", "displayFrequency": "14-day course", "resultSection": "treatment" },
{ "code": "ibuprofen", "canonicalName": "Ibuprofen for abdominal pain", "category": "medication", "aliases": ["nsaid", "motrin", "advil"], "defaultTurnaroundMinutes": 0, "availableLocations": [], "displayRoute": "Oral", "displayFrequency": "As needed", "resultSection": "treatment" }
],
"cases": [
{
"externalId": "ccs-gastro-duodenal-ulcer-office-001",
@@ -14,7 +27,7 @@
"graphicalPresentation": {
"sceneKey": "doctor_office",
"patientAssetKey": "patient_44_white_male",
"interviewRevision": 2
"interviewRevision": 4
},
"initialChart": {
"patientSummary": "44-year-old man attending an office visit",
@@ -124,7 +137,20 @@
{ "code": "extremities_spine", "name": "Extremities / Spine", "durationMinutes": 1, "finding": "Extremities have full range of motion without edema, clubbing, cyanosis, or joint swelling. The spine is nontender." },
{ "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, "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, "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" }
],
"events": [],
"scoringCriteria": [
{ "id": "elicit_chief_concern", "title": "Elicit the chief concern", "actionType": "dialogue_choice", "targetCode": "chief_concern", "points": 10, "rationale": "An open-ended opening allows the patient to identify lightheadedness and fatigue in his own words." },
@@ -140,7 +166,16 @@
{ "id": "examine_heart", "title": "Examine the cardiovascular system", "actionType": "exam", "targetCode": "heart_cardiovascular", "points": 5, "rationale": "Cardiovascular examination helps assess presyncope and hemodynamic status." },
{ "id": "examine_abdomen", "title": "Examine the abdomen", "actionType": "exam", "targetCode": "abdomen", "points": 15, "rationale": "Focused abdominal examination identifies epigastric tenderness and screens for peritoneal signs or masses." },
{ "id": "perform_rectal_exam", "title": "Perform a rectal examination", "actionType": "exam", "targetCode": "rectal", "points": 10, "rationale": "Rectal examination can assess for current gross gastrointestinal bleeding when the history includes black stool." },
{ "id": "perform_neurologic_exam", "title": "Perform a neurologic examination", "actionType": "exam", "targetCode": "neuro_psych", "points": 5, "rationale": "A focused neurologic examination helps evaluate lightheadedness and exclude focal deficits." }
{ "id": "perform_neurologic_exam", "title": "Perform a neurologic examination", "actionType": "exam", "targetCode": "neuro_psych", "points": 5, "rationale": "A focused neurologic examination helps evaluate lightheadedness and exclude focal deficits." },
{ "id": "check_cbc", "title": "Assess anemia with a CBC", "actionType": "order", "targetCode": "cbc", "points": 12, "rationale": "Fatigue, lightheadedness, and intermittent melena warrant assessment for blood-loss anemia." },
{ "id": "check_iron", "title": "Confirm iron deficiency", "actionType": "order", "targetCode": "iron_studies", "points": 6, "rationale": "Iron studies characterize the microcytic anemia." },
{ "id": "test_h_pylori", "title": "Test for H. pylori", "actionType": "order", "targetCode": "h_pylori_breath", "points": 12, "rationale": "H. pylori testing identifies a treatable cause of peptic ulcer disease." },
{ "id": "perform_egd", "title": "Evaluate suspected bleeding with upper endoscopy", "actionType": "order", "targetCode": "upper_endoscopy", "points": 12, "rationale": "Endoscopy confirms the ulcer and evaluates bleeding risk." },
{ "id": "start_ppi", "title": "Begin proton-pump inhibitor therapy", "actionType": "order", "targetCode": "omeprazole", "points": 8, "rationale": "Acid suppression promotes ulcer healing." },
{ "id": "eradicate_h_pylori", "title": "Treat H. pylori appropriately", "actionType": "order", "targetCode": "bismuth_quadruple", "points": 15, "afterTargetCode": "h_pylori_breath", "rationale": "Optimized bismuth quadruple therapy is an appropriate empiric eradication regimen." },
{ "id": "confirm_eradication", "title": "Arrange H. pylori test of cure", "actionType": "order", "targetCode": "h_pylori_test_of_cure", "points": 8, "afterTargetCode": "bismuth_quadruple", "rationale": "Eradication should be confirmed after treatment." },
{ "id": "avoid_clarithromycin", "title": "Avoid unsupported clarithromycin triple therapy", "actionType": "order", "targetCode": "clarithromycin_triple", "points": -8, "rationale": "Empiric clarithromycin therapy should be avoided without demonstrated susceptibility." },
{ "id": "avoid_nsaid", "title": "Avoid NSAIDs", "actionType": "order", "targetCode": "ibuprofen", "points": -12, "rationale": "NSAIDs may worsen ulcer injury and gastrointestinal bleeding." }
],
"references": [
{
+48 -1
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=13 format=3]
[gd_scene load_steps=14 format=3]
[ext_resource type="Texture2D" path="res://assets/ccs_cases_2/images/doctor/office-backdrop.png" id="1_backdrop"]
[ext_resource type="Script" path="res://scripts/ui/ccs_cases_2/doctor_office.gd" id="2_script"]
@@ -11,6 +11,7 @@
[ext_resource type="Texture2D" path="res://assets/icons/lucide/x.svg" id="9_close"]
[ext_resource type="Texture2D" path="res://assets/ccs_cases_2/icons/vitals.svg" id="10_vitals"]
[ext_resource type="Texture2D" path="res://assets/ccs_cases_2/icons/medical-history.svg" id="11_history"]
[ext_resource type="Texture2D" path="res://assets/icons/lucide/clipboard-list.svg" id="12_orders"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_patient_fit"]
shader = ExtResource("5_patient_fit")
@@ -241,6 +242,21 @@ icon_max_width = 25
expand_icon = true
text = "Computer"
[node name="ExaminationLauncher" type="Button" parent="."]
visible = false
z_index = 40
layout_mode = 0
offset_left = 202.0
offset_top = 24.0
offset_right = 394.0
offset_bottom = 78.0
mouse_default_cursor_shape = 2
tooltip_text = "Perform physical examination"
icon = ExtResource("10_vitals")
icon_max_width = 25
expand_icon = true
text = "Examination"
[node name="ComputerOverlay" type="Control" parent="."]
visible = false
z_index = 100
@@ -318,6 +334,37 @@ expand_icon = true
icon_alignment = 1
vertical_icon_alignment = 0
[node name="OrdersLauncher" type="Button" parent="ComputerOverlay"]
layout_mode = 0
offset_left = 30.0
offset_top = 276.0
offset_right = 142.0
offset_bottom = 382.0
mouse_default_cursor_shape = 2
icon = ExtResource("12_orders")
icon_max_width = 58
expand_icon = true
icon_alignment = 1
vertical_icon_alignment = 0
[node name="Label" type="Label" parent="ComputerOverlay/OrdersLauncher"]
layout_mode = 1
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -34.0
grow_horizontal = 2
grow_vertical = 0
mouse_filter = 2
theme_override_colors/font_color = Color(0.96, 0.98, 1, 1)
theme_override_colors/font_outline_color = Color(0.01, 0.04, 0.07, 0.95)
theme_override_constants/outline_size = 4
theme_override_font_sizes/font_size = 17
text = "Orders"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Label" type="Label" parent="ComputerOverlay/MedicalHistoryLauncher"]
layout_mode = 1
anchors_preset = 12
+607 -18
View File
@@ -1,9 +1,13 @@
extends Control
signal choice_selected(choice_id: String)
signal interview_continue_requested
signal physical_exam_selected(exam_code: String)
signal physical_exam_finish_requested
signal order_search_requested(query: String)
signal orders_submit_requested(codes: Array)
signal order_cancel_requested(code: String)
signal time_advance_requested(minutes: int)
signal final_orders_submit_requested
signal debrief_return_requested
signal debrief_repeat_requested
@@ -32,6 +36,7 @@ const DIALOGUE_BUBBLE_VERTICAL_CHROME := 95.0
@onready var page_label: Label = $DialogueChoiceArea/ChoicePager/Page
@onready var next_page_button: Button = $DialogueChoiceArea/ChoicePager/Next
@onready var computer_launcher: Button = $ComputerLauncher
@onready var examination_launcher: Button = $ExaminationLauncher
@onready var computer_overlay: Control = $ComputerOverlay
@onready var computer_close_button: Button = $ComputerOverlay/Close
@onready var vitals_launcher: Button = $ComputerOverlay/VitalsLauncher
@@ -45,6 +50,7 @@ const DIALOGUE_BUBBLE_VERTICAL_CHROME := 95.0
@onready var medical_history_header: HBoxContainer = $ComputerOverlay/MedicalHistoryWindow/Margin/Content/Header
@onready var medical_history_close_button: Button = $ComputerOverlay/MedicalHistoryWindow/Margin/Content/Header/Close
@onready var medical_history_text: Label = $ComputerOverlay/MedicalHistoryWindow/Margin/Content/Text
@onready var orders_launcher: Button = $ComputerOverlay/OrdersLauncher
var blink_tween: Tween
var dialogue_tween: Tween
@@ -57,6 +63,42 @@ var physical_exam_finding_text: Label
var physical_exam_status: Label
var physical_exam_finish_button: Button
var physical_exam_buttons := {}
var physical_exam_close_button: Button
var physical_exam_result_button: Button
var physical_exam_terminal := false
var available_exams: Array = []
var attempt_chart: Array = []
var medical_history_section_title: Label
var medical_history_exam_text: RichTextLabel
var medical_history_exam_scroll: ScrollContainer
var medical_history_tab_button: Button
var physical_exam_tab_button: Button
var orders_window: PanelContainer
var orders_search: LineEdit
var orders_search_results: VBoxContainer
var orders_staged: VBoxContainer
var orders_active: VBoxContainer
var orders_results: RichTextLabel
var orders_tabs: TabContainer
var orders_confirm_button: Button
var orders_advance_button: Button
var orders_advance_option: OptionButton
var orders_submit_final_button: Button
var orders_close_button: Button
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 = []
@@ -77,11 +119,13 @@ func _ready() -> void:
previous_page_button.pressed.connect(_change_choice_page.bind(-1))
next_page_button.pressed.connect(_change_choice_page.bind(1))
computer_launcher.pressed.connect(open_computer)
examination_launcher.pressed.connect(show_exam_selector)
computer_close_button.pressed.connect(close_computer)
vitals_launcher.pressed.connect(open_vitals)
vitals_close_button.pressed.connect(close_vitals)
medical_history_launcher.pressed.connect(open_medical_history)
medical_history_close_button.pressed.connect(close_medical_history)
orders_launcher.pressed.connect(open_orders)
vitals_header.gui_input.connect(_on_computer_window_header_input.bind(vitals_window, vitals_close_button))
medical_history_header.gui_input.connect(_on_computer_window_header_input.bind(
medical_history_window, medical_history_close_button))
@@ -92,6 +136,10 @@ func _ready() -> void:
_style_computer_window(medical_history_window)
_style_desktop_shortcut(vitals_launcher)
_style_desktop_shortcut(medical_history_launcher)
_style_desktop_shortcut(orders_launcher)
_build_medical_history_tabs()
_build_orders_window()
_build_patient_update()
_resize_computer_windows()
_update_clock()
_schedule_patient_blink()
@@ -117,7 +165,7 @@ func _update_clock() -> void:
func configure_presentation(
presentation: Dictionary, simulated_start: String, simulated_minute: int,
vitals: Dictionary = {}, medical_history: String = "") -> void:
vitals: Dictionary = {}, medical_history: String = "", chart: Array = []) -> void:
if str(presentation.get("patientAssetKey", "")) != "patient_44_white_male":
push_warning("Unsupported CCS patient asset key; using the office default.")
var date_time := Time.get_datetime_dict_from_datetime_string(simulated_start, false)
@@ -129,6 +177,8 @@ func configure_presentation(
medical_history_text.text = medical_history.strip_edges() \
if not medical_history.strip_edges().is_empty() \
else "No past medical history is available."
attempt_chart = chart.duplicate(true)
_render_physical_exam_history()
func configure_experience_mode(value: String) -> void:
@@ -217,7 +267,7 @@ func configure_interview(node: Dictionary) -> void:
if bool(node.get("isTerminal", false)):
next_choices = [{
"id": "__continue",
"text": "Begin Physical Examination" if experience_mode == "graphical_interview" else "Continue to Patient Management"
"text": "Review Examinations" if experience_mode == "graphical_interview" else "Continue to Patient Management"
}]
else:
next_choices = node.get("choices", [])
@@ -269,10 +319,13 @@ func complete_response_transition() -> void:
dialogue_page_index = 0
dialogue_reveal_complete = false
show_dialogue_choices(choices)
_update_examination_launcher()
func _input(event: InputEvent) -> void:
if computer_overlay.visible:
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):
return
@@ -312,6 +365,7 @@ func close_computer() -> bool:
return false
close_vitals()
close_medical_history()
close_orders()
computer_overlay.visible = false
computer_launcher.visible = computer_available
if dialogue_tween and dialogue_tween.is_valid():
@@ -324,11 +378,24 @@ 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()
elif not physical_exam_terminal:
close_exam_overlay()
else:
return false
return true
var top_window := _topmost_computer_window()
if top_window == vitals_window:
return close_vitals()
if top_window == medical_history_window:
return close_medical_history()
if top_window == orders_window:
return close_orders()
return close_computer()
@@ -374,6 +441,456 @@ func is_medical_history_open() -> bool:
return medical_history_window.visible
func _build_orders_window() -> void:
orders_window = PanelContainer.new()
orders_window.name = "OrdersWindow"
orders_window.visible = false
orders_window.z_index = 2
orders_window.size = Vector2(1040.0, 700.0)
orders_window.position = Vector2(280.0, 100.0)
_style_computer_window(orders_window)
computer_overlay.add_child(orders_window)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 28)
margin.add_theme_constant_override("margin_top", 22)
margin.add_theme_constant_override("margin_right", 28)
margin.add_theme_constant_override("margin_bottom", 24)
orders_window.add_child(margin)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 12)
margin.add_child(content)
var header := HBoxContainer.new()
header.mouse_default_cursor_shape = Control.CURSOR_MOVE
content.add_child(header)
var title := Label.new()
title.text = "Orders"
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title.add_theme_font_size_override("font_size", 30)
header.add_child(title)
orders_close_button = Button.new()
orders_close_button.text = "Close"
orders_close_button.pressed.connect(close_orders)
header.add_child(orders_close_button)
header.gui_input.connect(_on_computer_window_header_input.bind(orders_window, orders_close_button))
orders_window.gui_input.connect(_on_computer_window_input.bind(orders_window))
orders_status = Label.new()
orders_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
content.add_child(orders_status)
orders_tabs = TabContainer.new()
orders_tabs.size_flags_vertical = Control.SIZE_EXPAND_FILL
content.add_child(orders_tabs)
_build_order_search_tab()
_build_active_orders_tab()
_build_order_results_tab()
var actions := HBoxContainer.new()
actions.alignment = BoxContainer.ALIGNMENT_END
content.add_child(actions)
orders_advance_option = OptionButton.new()
for item in [["10 minutes", 10], ["1 hour", 60], ["4 hours", 240], ["1 day", 1440]]:
orders_advance_option.add_item(item[0])
orders_advance_option.set_item_metadata(orders_advance_option.item_count - 1, item[1])
actions.add_child(orders_advance_option)
orders_advance_button = Button.new()
orders_advance_button.text = "Obtain Results"
orders_advance_button.pressed.connect(func(): time_advance_requested.emit(
int(orders_advance_option.get_item_metadata(orders_advance_option.selected))))
actions.add_child(orders_advance_button)
orders_submit_final_button = Button.new()
orders_submit_final_button.text = "Submit Final Orders"
orders_submit_final_button.visible = false
orders_submit_final_button.pressed.connect(func(): final_orders_submit_requested.emit())
actions.add_child(orders_submit_final_button)
func _build_order_search_tab() -> void:
var page := VBoxContainer.new()
page.name = "Search"
page.add_theme_constant_override("separation", 8)
orders_tabs.add_child(page)
var row := HBoxContainer.new()
page.add_child(row)
orders_search = LineEdit.new()
orders_search.placeholder_text = "Search tests, medications, treatments, and procedures"
orders_search.size_flags_horizontal = Control.SIZE_EXPAND_FILL
orders_search.text_submitted.connect(func(_value): _request_order_search())
orders_search.text_changed.connect(_queue_live_order_search)
row.add_child(orders_search)
order_search_timer = Timer.new()
order_search_timer.one_shot = true
order_search_timer.wait_time = 0.35
order_search_timer.timeout.connect(_request_order_search)
add_child(order_search_timer)
var search_button := Button.new()
search_button.text = "Search"
search_button.pressed.connect(_request_order_search)
row.add_child(search_button)
var search_scroll := ScrollContainer.new()
search_scroll.custom_minimum_size.y = 170.0
search_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
page.add_child(search_scroll)
orders_search_results = VBoxContainer.new()
orders_search_results.size_flags_horizontal = Control.SIZE_EXPAND_FILL
search_scroll.add_child(orders_search_results)
page.add_child(Label.new())
page.get_child(page.get_child_count() - 1).text = "Orders awaiting confirmation"
orders_staged = VBoxContainer.new()
page.add_child(orders_staged)
orders_confirm_button = Button.new()
orders_confirm_button.text = "Confirm Orders"
orders_confirm_button.disabled = true
orders_confirm_button.pressed.connect(_confirm_staged_orders)
page.add_child(orders_confirm_button)
func _build_active_orders_tab() -> void:
var page := VBoxContainer.new()
page.name = "Active Orders"
orders_tabs.add_child(page)
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
page.add_child(scroll)
orders_active = VBoxContainer.new()
orders_active.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.add_child(orders_active)
func _build_order_results_tab() -> void:
var page := VBoxContainer.new()
page.name = "Results"
orders_tabs.add_child(page)
orders_results = RichTextLabel.new()
orders_results.bbcode_enabled = true
orders_results.size_flags_vertical = Control.SIZE_EXPAND_FILL
page.add_child(orders_results)
func open_orders() -> void:
if not computer_overlay.visible:
return
orders_window.visible = true
orders_launcher.get_node("Label").text = "Orders"
_bring_computer_window_to_front(orders_window)
orders_search.grab_focus()
func close_orders() -> bool:
if orders_window == null or not orders_window.visible:
return false
orders_window.visible = false
return true
func _request_order_search() -> void:
var query := orders_search.text.strip_edges()
if query.length() < 2:
orders_status.text = "Enter at least two characters to search."
return
orders_status.text = "Searching..."
order_search_requested.emit(query)
func _queue_live_order_search(query: String) -> void:
order_search_timer.stop()
if query.strip_edges().length() < 2:
_clear_dynamic_children(orders_search_results)
orders_status.text = ""
return
order_search_timer.start()
func render_order_search(rows: Array) -> void:
_clear_dynamic_children(orders_search_results)
var result_count := 0
for raw_order in rows:
var order: Dictionary = raw_order
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()
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 = 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 result_count > 0 else "No matching orders found."
func _stage_order(code: String) -> void:
if code.is_empty() or code in staged_order_codes:
return
staged_order_codes.append(code)
_render_staged_orders()
func _render_staged_orders() -> void:
_clear_dynamic_children(orders_staged)
for code in staged_order_codes:
var row := HBoxContainer.new()
orders_staged.add_child(row)
var label := Label.new()
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()
remove.text = "Remove"
remove.pressed.connect(_remove_staged_order.bind(code))
row.add_child(remove)
orders_confirm_button.disabled = staged_order_codes.is_empty()
func _remove_staged_order(code: String) -> void:
staged_order_codes.erase(code)
_render_staged_orders()
func _confirm_staged_orders() -> void:
if staged_order_codes.is_empty():
return
orders_submit_requested.emit(staged_order_codes.duplicate())
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 •"
known_order_result_count = result_count
final_orders_mode = str(attempt.get("state", "")) == "final_orders"
orders_advance_button.disabled = false
orders_submit_final_button.disabled = false
orders_advance_button.visible = not final_orders_mode
orders_advance_option.visible = not final_orders_mode
orders_submit_final_button.visible = final_orders_mode
orders_close_button.visible = true
if final_orders_mode:
orders_status.text = "FINAL STEP — Review your active orders. Add or cancel anything necessary, then select Submit Final Orders. Pending results do not need to return first."
open_computer()
open_orders()
elif orders_status.text.begins_with("Submitting") or orders_status.text.begins_with("Cancelling") \
or orders_status.text.begins_with("Obtaining"):
orders_status.text = ""
func show_order_action_complete(action: String) -> void:
match action:
"orders":
orders_tabs.current_tab = 1
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 = "Simulated time advanced. Newly available results are shown here."
func _render_active_orders(rows: Array) -> void:
_clear_dynamic_children(orders_active)
if rows.is_empty():
var empty := Label.new()
empty.text = "No active orders."
orders_active.add_child(empty)
return
for raw_order in rows:
var order: Dictionary = raw_order
var row := HBoxContainer.new()
orders_active.add_child(row)
var label := Label.new()
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()
cancel.text = "Cancel"
cancel.pressed.connect(_emit_cancel_order.bind(str(order.get("code", ""))))
row.add_child(cancel)
func _render_order_results(chart: Array) -> void:
var parts: Array[String] = []
for raw_entry in chart:
var entry: Dictionary = raw_entry
if str(entry.get("category", "")) in ["lab", "imaging", "other_test", "treatment"]:
parts.append("[b]%s[/b]\n%s" % [str(entry.get("title", "Result")), str(entry.get("text", ""))])
orders_results.text = "\n\n".join(parts) if not parts.is_empty() else "No results are available."
func _order_result_count(chart: Array) -> int:
var count := 0
for raw_entry in chart:
if str(raw_entry.get("category", "")) in ["lab", "imaging", "other_test", "treatment"]:
count += 1
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
orders_submit_final_button.disabled = pending
orders_status.text = message
func _emit_cancel_order(code: String) -> void:
order_cancel_requested.emit(code)
func _clear_dynamic_children(parent: Node) -> void:
for child in parent.get_children():
child.queue_free()
func _build_medical_history_tabs() -> void:
var content := medical_history_text.get_parent() as VBoxContainer
medical_history_section_title = content.get_node("SectionTitle") as Label
var tabs := HBoxContainer.new()
tabs.add_theme_constant_override("separation", 8)
content.add_child(tabs)
content.move_child(tabs, medical_history_section_title.get_index())
medical_history_tab_button = Button.new()
medical_history_tab_button.text = "Medical History"
medical_history_tab_button.pressed.connect(_show_chart_tab.bind(false))
tabs.add_child(medical_history_tab_button)
physical_exam_tab_button = Button.new()
physical_exam_tab_button.text = "Physical Exam"
physical_exam_tab_button.pressed.connect(_show_chart_tab.bind(true))
tabs.add_child(physical_exam_tab_button)
medical_history_exam_scroll = ScrollContainer.new()
medical_history_exam_scroll.custom_minimum_size = Vector2(0.0, 120.0)
medical_history_exam_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
medical_history_exam_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
content.add_child(medical_history_exam_scroll)
medical_history_exam_text = RichTextLabel.new()
medical_history_exam_text.bbcode_enabled = true
medical_history_exam_text.fit_content = true
medical_history_exam_text.custom_minimum_size = Vector2(560.0, 120.0)
medical_history_exam_text.size_flags_horizontal = Control.SIZE_EXPAND_FILL
medical_history_exam_text.add_theme_font_size_override("normal_font_size", 18)
medical_history_exam_scroll.add_child(medical_history_exam_text)
_show_chart_tab(false)
func _show_chart_tab(show_exams: bool) -> void:
medical_history_section_title.visible = not show_exams
medical_history_text.visible = not show_exams
medical_history_exam_scroll.visible = show_exams
medical_history_tab_button.disabled = not show_exams
physical_exam_tab_button.disabled = show_exams
_resize_computer_windows()
func _render_physical_exam_history() -> void:
if medical_history_exam_text == null:
return
var exam_codes := {}
for raw_exam in available_exams:
exam_codes[str(raw_exam.get("code", ""))] = true
var findings: Array[String] = []
for raw_entry in attempt_chart:
var entry: Dictionary = raw_entry
if exam_codes.has(str(entry.get("code", ""))):
findings.append("[b]%s[/b]\n%s" % [str(entry.get("title", "Examination")), str(entry.get("text", ""))])
medical_history_exam_text.text = "\n\n".join(findings) if not findings.is_empty() \
else "No physical examinations have been completed."
func _update_examination_launcher() -> void:
if examination_launcher == null:
return
examination_launcher.visible = choice_container.visible and not computer_overlay.visible \
and (physical_exam_panel == null or not physical_exam_panel.visible)
examination_launcher.disabled = available_exams.is_empty()
func _render_vitals(vitals: Dictionary) -> void:
for child in vitals_rows.get_children():
vitals_rows.remove_child(child)
@@ -490,9 +1007,12 @@ func _resize_computer_windows() -> void:
minf(720.0, maxf(320.0, size.x - 48.0)),
minf(vitals_content_height, maxf(300.0, size.y - 48.0)))
_set_centered_window_rect(vitals_window, vitals_size, Vector2(-55.0, -25.0))
var exam_count := medical_history_exam_text.text.count("[b]") if medical_history_exam_text != null else 0
var showing_exams := medical_history_exam_scroll != null and medical_history_exam_scroll.visible
var history_height := 340.0 if not showing_exams else clampf(250.0 + exam_count * 105.0, 340.0, 650.0)
var history_size := Vector2(
minf(560.0, maxf(320.0, size.x - 48.0)),
minf(300.0, maxf(260.0, size.y - 48.0)))
minf(680.0, maxf(320.0, size.x - 48.0)),
minf(history_height, maxf(300.0, size.y - 48.0)))
_set_centered_window_rect(medical_history_window, history_size, Vector2(55.0, 25.0))
@@ -551,7 +1071,7 @@ func _bring_computer_window_to_front(window: Control) -> void:
func _topmost_computer_window() -> Control:
var top_window: Control
for window in [vitals_window, medical_history_window]:
for window in [vitals_window, medical_history_window, orders_window]:
if window.visible and (top_window == null or window.z_index > top_window.z_index):
top_window = window
return top_window
@@ -667,6 +1187,7 @@ func _render_choice_page() -> void:
next_page_button.disabled = choice_page >= page_count - 1
choice_area.visible = true
choice_container.visible = true
_update_examination_launcher()
func hide_dialogue_choices() -> void:
@@ -676,6 +1197,7 @@ func hide_dialogue_choices() -> void:
choice_container.visible = false
choice_pager.visible = false
choice_area.visible = false
_update_examination_launcher()
func _clear_choice_buttons() -> void:
@@ -723,18 +1245,19 @@ func _on_choice_pressed_visual(button: TextureButton, pressed: bool) -> void:
func _on_dialogue_choice_pressed(choice_id: String) -> void:
if choice_id == "__continue":
interview_continue_requested.emit()
show_exam_selector(true)
else:
choice_selected.emit(choice_id)
func show_physical_exam(exams: Array, chart: Array, pending_code: String = "") -> void:
hide_patient_dialogue()
hide_dialogue_choices()
computer_available = true
computer_launcher.visible = not computer_overlay.visible
available_exams = exams.duplicate(true)
attempt_chart = chart.duplicate(true)
_ensure_physical_exam_panel()
physical_exam_panel.visible = true
physical_exam_finding_title.visible = true
physical_exam_finding_text.visible = true
physical_exam_result_button.visible = false
for child in physical_exam_grid.get_children():
physical_exam_grid.remove_child(child)
child.queue_free()
@@ -777,6 +1300,60 @@ func show_physical_exam(exams: Array, chart: Array, pending_code: String = "") -
set_physical_exam_pending(pending_code)
func configure_examinations(exams: Array, chart: Array) -> void:
available_exams = exams.duplicate(true)
attempt_chart = chart.duplicate(true)
_render_physical_exam_history()
_update_examination_launcher()
func show_exam_selector(terminal: bool = false) -> void:
if not choice_container.visible or (physical_exam_panel != null and physical_exam_panel.visible):
return
physical_exam_terminal = terminal
show_physical_exam(available_exams, attempt_chart)
physical_exam_finish_button.visible = terminal
physical_exam_close_button.visible = not terminal
func show_exam_result(chart: Array, exam_code: String) -> void:
attempt_chart = chart.duplicate(true)
_render_physical_exam_history()
_ensure_physical_exam_panel()
physical_exam_panel.visible = true
for child in physical_exam_grid.get_children():
child.visible = false
var finding := _chart_exam_entry(chart, exam_code)
physical_exam_finding_title.text = str(finding.get("title", "Examination Finding"))
physical_exam_finding_text.text = str(finding.get("text", "No finding was reported."))
physical_exam_result_button.visible = true
physical_exam_close_button.visible = false
physical_exam_finish_button.visible = false
func close_exam_overlay() -> void:
if physical_exam_panel != null:
physical_exam_panel.visible = false
_update_examination_launcher()
func _close_exam_result() -> void:
if physical_exam_terminal:
show_physical_exam(available_exams, attempt_chart)
physical_exam_finish_button.visible = true
physical_exam_close_button.visible = false
else:
close_exam_overlay()
func _chart_exam_entry(chart: Array, code: String) -> Dictionary:
for index in range(chart.size() - 1, -1, -1):
var entry: Dictionary = chart[index]
if str(entry.get("code", "")) == code:
return entry
return {}
func set_physical_exam_pending(exam_code: String) -> void:
var pending := not exam_code.is_empty()
for code in physical_exam_buttons:
@@ -801,12 +1378,13 @@ func _ensure_physical_exam_panel() -> void:
physical_exam_panel = PanelContainer.new()
physical_exam_panel.name = "PhysicalExamination"
physical_exam_panel.z_index = 50
physical_exam_panel.custom_minimum_size = Vector2(980.0, 650.0)
add_child(physical_exam_panel)
physical_exam_panel.set_anchors_preset(Control.PRESET_FULL_RECT)
physical_exam_panel.offset_left = 44.0
physical_exam_panel.offset_top = 28.0
physical_exam_panel.offset_right = -44.0
physical_exam_panel.offset_bottom = -28.0
physical_exam_panel.set_anchors_preset(Control.PRESET_CENTER)
physical_exam_panel.offset_left = -490.0
physical_exam_panel.offset_top = -325.0
physical_exam_panel.offset_right = 490.0
physical_exam_panel.offset_bottom = 325.0
var panel_style := StyleBoxFlat.new()
panel_style.bg_color = Color(0.96, 0.97, 0.98, 0.97)
panel_style.border_color = Color(0.12, 0.22, 0.28, 0.9)
@@ -865,10 +1443,21 @@ func _ensure_physical_exam_panel() -> void:
physical_exam_status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
actions.add_child(physical_exam_status)
physical_exam_finish_button = Button.new()
physical_exam_finish_button.text = "Finish Case"
physical_exam_finish_button.text = "Review Final Orders"
physical_exam_finish_button.custom_minimum_size = Vector2(170.0, 48.0)
physical_exam_finish_button.pressed.connect(func(): physical_exam_finish_requested.emit())
actions.add_child(physical_exam_finish_button)
physical_exam_close_button = Button.new()
physical_exam_close_button.text = "Return to Interview"
physical_exam_close_button.custom_minimum_size = Vector2(190.0, 48.0)
physical_exam_close_button.pressed.connect(close_exam_overlay)
actions.add_child(physical_exam_close_button)
physical_exam_result_button = Button.new()
physical_exam_result_button.text = "Continue"
physical_exam_result_button.custom_minimum_size = Vector2(150.0, 48.0)
physical_exam_result_button.visible = false
physical_exam_result_button.pressed.connect(_close_exam_result)
actions.add_child(physical_exam_result_button)
func _style_physical_exam_button(button: Button, complete: bool) -> void:
+91 -39
View File
@@ -5,7 +5,7 @@ const Style := preload("res://scripts/ui/ui_style.gd")
const DOCTOR_SCENE := preload("res://scenes/ccs_cases_2/doctor_office.tscn")
const DESIGN_SIZE := Vector2(1600.0, 900.0)
const FOCUSED_INTERVIEW_CASE_ID := "ccs-gastro-duodenal-ulcer-office-001"
const FOCUSED_INTERVIEW_REVISION := 2
const FOCUSED_INTERVIEW_REVISION := 4
var main
var stage_root: Control
@@ -43,6 +43,8 @@ func _connect_service() -> void:
CCSService.attempt_changed.connect(_on_attempt_changed)
if not CCSService.request_failed.is_connected(_on_request_failed):
CCSService.request_failed.connect(_on_request_failed)
if not CCSService.orders_changed.is_connected(_on_orders_changed):
CCSService.orders_changed.connect(_on_orders_changed)
if not CCSService.attempt_abandoned.is_connected(_on_attempt_abandoned):
CCSService.attempt_abandoned.connect(_on_attempt_abandoned)
@@ -89,11 +91,39 @@ func _on_cases_changed() -> void:
func _on_attempt_changed() -> void:
active_loaded = true
var completed_action := pending_action
var completed_exam_code := pending_exam_code
pending_action = ""
pending_exam_code = ""
if completed_action == "physical_exam" and active_scene != null \
and current_view == "doctor_office":
_refresh_exam_result(CCSService.attempt, completed_exam_code)
return
if completed_action in ["orders", "cancel_order", "advance", "enter_final_orders"] \
and active_scene != null and current_view == "doctor_office":
_refresh_case_workspace(CCSService.attempt)
if completed_action != "enter_final_orders":
active_scene.show_order_action_complete(completed_action)
return
_render_state()
func _refresh_exam_result(attempt: Dictionary, exam_code: String) -> void:
_refresh_case_workspace(attempt)
active_scene.show_exam_result(attempt.get("chart", []), exam_code)
func _refresh_case_workspace(attempt: Dictionary) -> void:
active_scene.configure_presentation(
attempt.get("graphicalPresentation", {}),
str(attempt.get("simulatedStartUtc", "")),
int(attempt.get("simulatedMinute", 0)),
attempt.get("vitals", {}),
str(attempt.get("medicalHistory", "")), attempt.get("chart", []))
active_scene.configure_examinations(attempt.get("availableExams", []), attempt.get("chart", []))
active_scene.configure_orders(attempt)
func _render_state() -> void:
if not visible:
return
@@ -111,7 +141,7 @@ func _render_state() -> void:
func _render_attempt(attempt: Dictionary) -> void:
if str(attempt.get("state", "")) != "active":
if str(attempt.get("state", "")) not in ["active", "final_orders"]:
_show_unsupported_attempt("This active case is not available in the graphical interview yet.")
return
var interview: Dictionary = attempt.get("interview", {})
@@ -129,9 +159,6 @@ func _render_attempt(attempt: Dictionary) -> void:
and bool(attempt.get("openingCompleted", false)) and not bool(node.get("isTerminal", false)):
_show_unsupported_attempt("This case has progressed beyond the graphical interview.")
return
if current_view == "physical_examination" and bool(node.get("isTerminal", false)):
_show_physical_examination(attempt)
return
_show_doctor_office(node, attempt)
@@ -227,9 +254,13 @@ func _show_doctor_office(node: Dictionary, attempt: Dictionary = {}) -> void:
if active_scene == null:
_set_active_scene(DOCTOR_SCENE.instantiate(), "doctor_office")
active_scene.choice_selected.connect(_submit_dialogue_choice)
active_scene.interview_continue_requested.connect(_begin_physical_examination)
active_scene.physical_exam_selected.connect(_submit_physical_exam)
active_scene.physical_exam_finish_requested.connect(_finish_physical_examination)
active_scene.order_search_requested.connect(_search_orders)
active_scene.orders_submit_requested.connect(_submit_orders)
active_scene.order_cancel_requested.connect(_cancel_order)
active_scene.time_advance_requested.connect(_advance_time)
active_scene.final_orders_submit_requested.connect(_submit_final_orders)
active_scene.debrief_return_requested.connect(_return_to_cases)
active_scene.debrief_repeat_requested.connect(_repeat_interview)
selection_view.visible = false
@@ -240,8 +271,10 @@ func _show_doctor_office(node: Dictionary, attempt: Dictionary = {}) -> void:
str(attempt.get("simulatedStartUtc", "")),
int(attempt.get("simulatedMinute", 0)),
attempt.get("vitals", {}),
str(attempt.get("medicalHistory", "")))
str(attempt.get("medicalHistory", "")), attempt.get("chart", []))
active_scene.configure_examinations(attempt.get("availableExams", []), attempt.get("chart", []))
active_scene.configure_interview(node)
active_scene.configure_orders(attempt)
active_scene.set_choices_pending(not pending_action.is_empty())
@@ -253,33 +286,6 @@ func _submit_dialogue_choice(choice_id: String) -> void:
CCSService.send_dialogue_choice(choice_id)
func _begin_physical_examination() -> void:
if not pending_action.is_empty():
return
_show_physical_examination(CCSService.attempt)
func _show_physical_examination(attempt: Dictionary) -> void:
if active_scene == null:
_set_active_scene(DOCTOR_SCENE.instantiate(), "physical_examination")
active_scene.physical_exam_selected.connect(_submit_physical_exam)
active_scene.physical_exam_finish_requested.connect(_finish_physical_examination)
active_scene.debrief_return_requested.connect(_return_to_cases)
active_scene.debrief_repeat_requested.connect(_repeat_interview)
selection_view.visible = false
stage_root.visible = true
active_scene.configure_experience_mode("graphical_interview")
active_scene.configure_presentation(
attempt.get("graphicalPresentation", {}),
str(attempt.get("simulatedStartUtc", "")),
int(attempt.get("simulatedMinute", 0)),
attempt.get("vitals", {}),
str(attempt.get("medicalHistory", "")))
active_scene.show_physical_exam(
attempt.get("availableExams", []), attempt.get("chart", []), pending_exam_code)
current_view = "physical_examination"
func _submit_physical_exam(exam_code: String) -> void:
if not pending_action.is_empty() or exam_code.is_empty():
return
@@ -292,8 +298,52 @@ func _submit_physical_exam(exam_code: String) -> void:
func _finish_physical_examination() -> void:
if not pending_action.is_empty():
return
pending_action = "finishing_exam"
pending_action = "enter_final_orders"
active_scene.set_physical_exam_pending("finishing_case")
CCSService.send_command("end_case")
func _search_orders(query: String) -> void:
CCSService.search_orders(query)
func _on_orders_changed() -> void:
if active_scene != null and active_scene.has_method("render_order_search"):
active_scene.render_order_search(CCSService.orders)
func _submit_orders(codes: Array) -> void:
if not pending_action.is_empty() or codes.is_empty():
return
pending_action = "orders"
active_scene.set_orders_pending(true, "Submitting orders...")
var commands: Array = []
for code in codes:
commands.append({"type": "order", "code": str(code), "minutes": 0})
CCSService.send_batch(commands)
func _cancel_order(code: String) -> void:
if not pending_action.is_empty() or code.is_empty():
return
pending_action = "cancel_order"
active_scene.set_orders_pending(true, "Cancelling order...")
CCSService.send_batch([{"type": "cancel_order", "code": code, "minutes": 0}])
func _advance_time(minutes: int) -> void:
if not pending_action.is_empty() or minutes <= 0:
return
pending_action = "advance"
active_scene.set_orders_pending(true, "Obtaining results...")
CCSService.send_command("advance", "", minutes)
func _submit_final_orders() -> void:
if not pending_action.is_empty():
return
pending_action = "submit_final_orders"
active_scene.set_orders_pending(true, "Submitting final orders...")
CCSService.send_command("exit_case")
@@ -309,7 +359,7 @@ func _render_debrief(attempt: Dictionary) -> void:
str(attempt.get("simulatedStartUtc", "")),
int(attempt.get("simulatedMinute", 0)),
attempt.get("vitals", {}),
str(attempt.get("medicalHistory", "")))
str(attempt.get("medicalHistory", "")), attempt.get("chart", []))
active_scene.show_debrief(attempt.get("interviewDebrief", {}), attempt.get("score", {}))
current_view = "interview_debrief"
@@ -340,7 +390,7 @@ func _restart_with_current_interview(attempt: Dictionary) -> void:
return
restart_case_external_id = str(attempt.get("caseExternalId", ""))
pending_action = "refreshing_interview"
_show_selection("Updating this interview to its latest patient history dialogue...")
_show_selection("Updating this case to the latest interactive examination...")
CCSService.abandon()
@@ -369,11 +419,13 @@ func _on_request_failed(message: String) -> void:
retry_button.visible = true
return
if active_scene != null:
if current_view == "physical_examination":
if failed_action == "physical_exam":
pending_exam_code = ""
active_scene.show_physical_exam_error(message)
else:
active_scene.set_choices_pending(false)
if active_scene.has_method("set_orders_pending"):
active_scene.set_orders_pending(false, message)
else:
status_label.text = message
retry_button.visible = failed_action == "loading"
@@ -398,7 +450,7 @@ func _clear_active_scene() -> void:
active_scene.get_parent().remove_child(active_scene)
active_scene.queue_free()
active_scene = null
if current_view in ["doctor_office", "physical_examination"]:
if current_view == "doctor_office":
current_view = ""
+81 -9
View File
@@ -71,7 +71,7 @@ func _run() -> void:
_assert(screen._is_retired_interview_revision(retired_attempt, {
"id": "fatigue_course", "patientText": "I feel tired.", "choices": []
}), "an older authored interview revision should trigger a restart")
retired_attempt.graphicalPresentation.interviewRevision = 2
retired_attempt.graphicalPresentation.interviewRevision = 4
_assert(not screen._is_retired_interview_revision(retired_attempt, {}),
"the current authored interview revision should remain active")
@@ -81,6 +81,7 @@ func _run() -> void:
_assert(screen.current_view == "doctor_office", "an active interactive attempt should open the doctor office")
_assert(not screen.active_scene.is_patient_dialogue_visible(), "the opening should wait for a clinician dialogue selection")
_assert(screen.active_scene.choice_container.get_child_count() == 1, "the scene should render server-provided dialogue choices")
_assert(screen.active_scene.examination_launcher.visible, "Examination should be available while questions are visible")
_assert(screen.active_scene.computer_launcher.visible, "the doctor office should expose the simulated computer launcher")
_assert(screen.active_scene.computer_launcher.icon.resource_path.ends_with("monitor.svg"), "the computer launcher should use the monitor icon")
screen.active_scene.computer_launcher.pressed.emit()
@@ -90,11 +91,62 @@ func _run() -> void:
_assert(screen.active_scene.computer_overlay.get_node("Screen").stretch_mode == TextureRect.STRETCH_SCALE, "the computer should fill the viewport without side bars")
_assert(screen.active_scene.vitals_launcher.visible, "the computer desktop should expose a Vitals app")
_assert(screen.active_scene.medical_history_launcher.visible, "the computer desktop should expose a Medical History app")
_assert(screen.active_scene.orders_launcher.visible, "the computer desktop should expose an Orders app")
_assert(screen.active_scene.vitals_launcher.text.is_empty() and screen.active_scene.medical_history_launcher.text.is_empty(), "desktop shortcuts should place labels below centered icons")
_assert(screen.active_scene.vitals_launcher.get_node("Label").text == "Vitals" and screen.active_scene.medical_history_launcher.get_node("Label").text == "Medical History", "desktop shortcut labels should remain readable")
_assert(screen.active_scene.vitals_launcher.get_theme_stylebox("normal") is StyleBoxEmpty and screen.active_scene.medical_history_launcher.get_theme_stylebox("normal") is StyleBoxEmpty, "desktop shortcuts should not render permanent button tiles")
_assert(screen.active_scene.medical_history_launcher.position.y > screen.active_scene.vitals_launcher.get_rect().end.y, "desktop shortcuts should form a vertical icon column")
_assert(screen.active_scene.vitals_launcher.icon.get_width() >= 58 and screen.active_scene.medical_history_launcher.icon.get_width() >= 58, "desktop shortcuts should use high-resolution vector imports")
screen.active_scene.orders_launcher.pressed.emit()
_assert(screen.active_scene.orders_window.visible and screen.active_scene.orders_tabs.get_tab_count() == 3, "Orders should open a three-tab clinical workspace")
screen.active_scene._queue_live_order_search("bl")
_assert(not screen.active_scene.order_search_timer.is_stopped(), "order typing should schedule a debounced live search")
screen.active_scene.order_search_timer.stop()
screen.active_scene.current_order_location = "office"
screen.active_scene.render_order_search([
{"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() == 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"],
"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")
_assert(screen.active_scene.orders_tabs.current_tab == 2, "obtaining results should open the Results tab")
screen.active_scene.close_orders()
screen.active_scene.configure_orders({"state": "final_orders", "activeOrderCodes": [], "activeOrders": [], "chart": [], "finalOrdersDeadlineUtc": "2026-08-14T12:00:00Z"})
_assert(screen.active_scene.is_computer_open() and screen.active_scene.orders_window.visible, "final orders should automatically open the Orders app")
_assert(screen.active_scene.orders_submit_final_button.visible and not screen.active_scene.orders_submit_final_button.disabled \
and not screen.active_scene.orders_advance_button.visible, "final orders should expose an enabled submission while locking time advancement")
_assert(screen.active_scene.orders_status.text.contains("Pending results do not need to return"), "final review should clearly explain pending orders")
_assert(screen.active_scene.close_orders(), "the Orders app should remain closable during final review")
_assert(screen.active_scene.close_computer(), "the computer should remain closable during final review")
screen.active_scene.open_computer()
screen.active_scene.open_orders()
screen.active_scene.final_orders_mode = false
screen.active_scene.close_orders()
screen.active_scene.close_computer()
screen.active_scene.open_computer()
screen.active_scene.vitals_launcher.pressed.emit()
_assert(screen.active_scene.is_vitals_open(), "the Vitals app should open a nurse vital-sign window")
_assert(screen.active_scene.vitals_header.get_node("Title").text == "Vitals", "the vital window should use the concise Vitals heading")
@@ -103,7 +155,11 @@ func _run() -> void:
screen.active_scene.medical_history_launcher.pressed.emit()
_assert(screen.active_scene.is_vitals_open() and screen.active_scene.is_medical_history_open(), "both chart windows should be able to remain open")
_assert(screen.active_scene.medical_history_text.text == "Hyperlipidemia", "Medical History should render the case-authored past medical history")
_assert(screen.active_scene.medical_history_window.size.y <= 310.0, "Medical History should size closely to its short content (size %s, minimum %s)" % [screen.active_scene.medical_history_window.size, screen.active_scene.medical_history_window.get_combined_minimum_size()])
_assert(screen.active_scene.medical_history_window.size.y <= 350.0, "Medical History should size closely to its tabbed content (size %s, minimum %s)" % [screen.active_scene.medical_history_window.size, screen.active_scene.medical_history_window.get_combined_minimum_size()])
_assert(screen.active_scene.physical_exam_tab_button.text == "Physical Exam", "Medical History should include a Physical Exam tab")
_assert(screen.active_scene.medical_history_exam_text.text == "No physical examinations have been completed.", "Physical Exam should begin with an empty state")
_assert(screen.active_scene.medical_history_exam_text.bbcode_enabled, "Physical Exam headings should render as rich text")
_assert(screen.active_scene.medical_history_exam_text.get_parent() is ScrollContainer, "Physical Exam overflow should use an internal scroller")
_assert(screen.active_scene.vitals_window.size.y <= 410.0, "Vitals should size to its rendered measurement rows")
_assert(screen.active_scene.medical_history_window.z_index > screen.active_scene.vitals_window.z_index, "the most recently opened chart window should come to the front")
var original_history_position: Vector2 = screen.active_scene.medical_history_window.position
@@ -145,6 +201,7 @@ func _run() -> void:
screen._render_state()
await process_frame
_assert(screen.active_scene.is_patient_dialogue_visible(), "a selected topic should display the patient's response")
_assert(not screen.active_scene.examination_launcher.visible, "Examination should be unavailable while the patient is speaking")
_assert(not screen.active_scene.choice_container.visible, "the next choices should wait until the patient finishes speaking")
_assert(screen.active_scene.dialogue_pages.size() > 1, "long patient responses should split into readable dialogue pages")
_assert(_label_content_fits(screen.active_scene.dialogue_text), "the first patient response page should fit inside its speech bubble")
@@ -211,16 +268,18 @@ func _run() -> void:
_assert(not screen.active_scene.choice_pager.visible, "a single terminal action should not show paging controls")
var terminal_choice := screen.active_scene.choice_container.get_child(0) as Control
_assert(terminal_choice.size.x <= 360.0, "a single terminal choice should remain a compact centered card")
_assert((terminal_choice.get_child(0) as Label).text == "Begin Physical Examination",
_assert((terminal_choice.get_child(0) as Label).text == "Review Examinations",
"terminal dialogue should lead into the graphical physical examination")
screen.active_scene.interview_continue_requested.emit()
terminal_choice.pressed.emit()
await process_frame
_assert(screen.current_view == "physical_examination", "the examination should remain inside CCS Cases 2")
_assert(screen.current_view == "doctor_office", "the examination should remain inside the active interview")
_assert(screen.active_scene.is_physical_exam_visible(), "the physical examination panel should be visible")
_assert(screen.active_scene.physical_exam_grid.columns == 2, "exam components should use two responsive columns")
_assert(screen.active_scene.physical_exam_grid.get_child_count() == 12, "all twelve examination components should be available")
_assert(not screen.active_scene.physical_exam_finish_button.disabled, "Finish Case should remain available before every exam is complete")
screen.active_scene.set_physical_exam_pending("abdomen")
screen.pending_action = "physical_exam"
screen.pending_exam_code = "abdomen"
_assert(screen.active_scene.physical_exam_finish_button.disabled, "case completion should pause while an exam command is pending")
_assert(screen.active_scene.physical_exam_buttons.values().all(func(button): return button.disabled),
"exam controls should disable while one component is submitted")
@@ -229,10 +288,22 @@ func _run() -> void:
service.attempt = examined_attempt
screen._on_attempt_changed()
await process_frame
_assert(screen.current_view == "physical_examination", "attempt refreshes should preserve the physical examination phase")
_assert(screen.active_scene.physical_exam_buttons.abdomen.text.begins_with("[Done]"), "completed exams should be marked")
_assert(screen.active_scene.physical_exam_buttons.abdomen.disabled, "completed exams should not be repeated")
_assert(screen.current_view == "doctor_office", "attempt refreshes should preserve the interview phase")
_assert(not screen.active_scene.is_patient_dialogue_visible(), "an examination result should not replay the current patient dialogue")
_assert(screen.active_scene.physical_exam_finding_text.text == "Mild epigastric tenderness.", "the returned chart finding should be displayed")
_assert(screen.active_scene.physical_exam_result_button.visible, "a completed exam should present a focused result card")
var result_dialogue_page: int = screen.active_scene.dialogue_page_index
screen.active_scene._input(_left_click(screen.active_scene.physical_exam_result_button.get_global_rect().get_center()))
_assert(screen.active_scene.physical_exam_result_button.visible \
and screen.active_scene.dialogue_page_index == result_dialogue_page,
"scene-wide dialogue input should not consume clicks while an examination result is open")
screen.active_scene.physical_exam_result_button.pressed.emit()
_assert(screen.active_scene.is_physical_exam_visible(), "terminal exam results should return to the exam selector")
screen.active_scene.open_computer()
screen.active_scene.open_medical_history()
screen.active_scene.physical_exam_tab_button.pressed.emit()
_assert(screen.active_scene.medical_history_exam_text.text.contains("Mild epigastric tenderness."), "completed findings should be stored in the Physical Exam tab")
_assert(screen.active_scene.medical_history_window.size.y <= 650.0, "the Physical Exam chart should remain bounded while findings scroll")
service.attempt = {"id": "classic-attempt", "state": "active", "openingCompleted": false, "interview": {}}
host.shown_screen = ""
@@ -288,7 +359,7 @@ func _sample_attempt(completed: bool) -> Dictionary:
return {
"id": "attempt-1", "state": "active", "revision": 1,
"simulatedStartUtc": "2026-08-05T09:00:00Z", "simulatedMinute": 0,
"graphicalPresentation": {"sceneKey": "doctor_office", "patientAssetKey": "patient_44_white_male", "interviewRevision": 2},
"graphicalPresentation": {"sceneKey": "doctor_office", "patientAssetKey": "patient_44_white_male", "interviewRevision": 4},
"medicalHistory": "Hyperlipidemia",
"vitals": {
"temperature": "36.8 C (98.2 F)", "bloodPressure": "122/76 mm Hg",
@@ -297,6 +368,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}
}