Compare commits
10
Commits
07516784d2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
030bcf6d7a | ||
|
|
25c2188264 | ||
|
|
375b3ea62f | ||
|
|
325d27eec5 | ||
|
|
7da07cb2a4 | ||
|
|
6312b45a92 | ||
|
|
07195dd24a | ||
|
|
026f0be174 | ||
|
|
9a54bfc6d6 | ||
|
|
6108011f74 |
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user