Files
step3/scripts/ui/ccs_player.gd
T

1163 lines
42 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends VBoxContainer
const Brand := preload("res://scripts/ui/medmind_style.gd")
const ICON_STETHOSCOPE := preload("res://assets/icons/lucide/stethoscope.svg")
const ICON_CLIPBOARD := preload("res://assets/icons/lucide/clipboard-plus.svg")
const ICON_CLOCK := preload("res://assets/icons/lucide/clock-3.svg")
const ICON_LOCATION := preload("res://assets/icons/lucide/house.svg")
const ICON_SEARCH := preload("res://assets/icons/lucide/search.svg")
const RESPONSIVE_BREAKPOINT := 1050.0
const CHART_TABS := [
["Order Sheet", "orders"],
["Progress Notes", "progress"],
["Vital Signs", "vitals"],
["Lab Reports", "lab"],
["Imaging", "imaging"],
["Other Tests", "other_test"],
["Treatment Record", "treatment"]
]
var main: Control
var header_panel: PanelContainer
var header_layout: BoxContainer
var heading: Label
var allotted_label: Label
var toolbar: HFlowContainer
var theme_button: Button
var exit_button: Button
var abandon_button: Button
var action_grid: GridContainer
var action_buttons: Array[Button] = []
var workspace: PanelContainer
var chart_panel: PanelContainer
var clerk_panel: PanelContainer
var tabs: TabContainer
var chart_views := {}
var order_sheet: VBoxContainer
var active_orders: VBoxContainer
var search: LineEdit
var search_button: Button
var search_results: VBoxContainer
var staged_orders: VBoxContainer
var commit_orders_button: Button
var clear_orders_button: Button
var final_orders_notice: Label
var simulated_clock: Label
var elapsed_clock: Label
var remaining_clock: Label
var status: Label
var timer: Timer
var report_dialog: Control
var intro_dialog: AcceptDialog
var intro_title: Label
var intro_body: RichTextLabel
var opening_step := 0
var opening_active := false
var exam_dialog: ConfirmationDialog
var interval_history_check: CheckBox
var exam_checks := {}
var exam_summary_dialog: ConfirmationDialog
var exam_summary: RichTextLabel
var staged_exam_codes: Array[String] = []
var staged_interval_history := false
var awaiting_exam_result := false
var findings_dialog: AcceptDialog
var findings_body: RichTextLabel
var time_dialog: ConfirmationDialog
var time_preset: OptionButton
var custom_target: LineEdit
var time_preview: Label
var location_dialog: ConfirmationDialog
var location_option: OptionButton
var end_dialog: ConfirmationDialog
var final_instructions: AcceptDialog
var abandon_dialog: ConfirmationDialog
var staged_order_codes: Array[String] = []
var staged_order_names := {}
var pending_findings_before := 0
var leaving := false
var theme_service
var toolbar_buttons: Array[Button] = []
var clinical_labels: Array[RichTextLabel] = []
func _ready() -> void:
theme_service = get_node("/root/UIThemeService")
set_anchors_preset(Control.PRESET_FULL_RECT)
add_theme_constant_override("separation", 8)
_build_header()
_build_actions()
_build_workspace()
_build_footer()
_build_dialogs()
_build_runtime_services()
resized.connect(_update_responsive_layout)
if not theme_service.theme_changed.is_connected(_on_theme_changed):
theme_service.theme_changed.connect(_on_theme_changed)
if not CCSService.text_scale_changed.is_connected(_apply_text_scale):
CCSService.text_scale_changed.connect(_apply_text_scale)
_apply_theme()
call_deferred("_update_responsive_layout")
func _build_header() -> void:
header_panel = PanelContainer.new()
add_child(header_panel)
var margin := MarginContainer.new()
for side in ["left", "right"]:
margin.add_theme_constant_override("margin_%s" % side, 14)
margin.add_theme_constant_override("margin_top", 8)
margin.add_theme_constant_override("margin_bottom", 8)
header_panel.add_child(margin)
header_layout = BoxContainer.new()
header_layout.add_theme_constant_override("separation", 12)
margin.add_child(header_layout)
heading = Label.new()
heading.text = "CCS Case"
heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL
heading.add_theme_font_size_override("font_size", 18)
Brand.apply_font(heading)
header_layout.add_child(heading)
allotted_label = Label.new()
allotted_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
allotted_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
allotted_label.add_theme_font_size_override("font_size", 17)
Brand.apply_font(allotted_label)
header_layout.add_child(allotted_label)
toolbar = HFlowContainer.new()
toolbar.size_flags_horizontal = Control.SIZE_SHRINK_END
toolbar.add_theme_constant_override("h_separation", 6)
header_layout.add_child(toolbar)
var smaller := _toolbar_button("A−", "Decrease clinical text size")
smaller.pressed.connect(_zoom.bind(-CCSService.TEXT_SCALE_STEP))
var larger := _toolbar_button("A+", "Increase clinical text size")
larger.pressed.connect(_zoom.bind(CCSService.TEXT_SCALE_STEP))
var report := _toolbar_button("Report", "Report this CCS case")
report.pressed.connect(_report)
theme_button = _toolbar_button("", "Switch color theme")
theme_button.custom_minimum_size.x = 42
theme_button.pressed.connect(theme_service.toggle_theme)
exit_button = _toolbar_button("Exit Case", "Enter final orders")
exit_button.pressed.connect(_request_end_case)
abandon_button = _toolbar_button("Abandon", "Abandon this attempt without a score")
abandon_button.pressed.connect(func(): abandon_dialog.popup_centered(Vector2i(520, 220)))
func _toolbar_button(text_value: String, tooltip: String) -> Button:
var button := Button.new()
button.text = text_value
button.tooltip_text = tooltip
button.custom_minimum_size = Vector2(58, 38)
toolbar.add_child(button)
toolbar_buttons.append(button)
return button
func _build_actions() -> void:
action_grid = GridContainer.new()
action_grid.columns = 4
action_grid.add_theme_constant_override("h_separation", 8)
action_grid.add_theme_constant_override("v_separation", 8)
add_child(action_grid)
_add_action("History & Physical", "Interval history and physical exam", ICON_STETHOSCOPE, _open_exam)
_add_action("Write Orders / Review Chart", "Place orders and review the patient chart", ICON_CLIPBOARD, _open_order_sheet)
_add_action("Obtain Results / See Patient Later", "Advance simulated case time", ICON_CLOCK, _open_time_dialog)
_add_action("Change Location", "Move the patient to another care setting", ICON_LOCATION, _open_location_dialog)
func _add_action(title: String, subtitle: String, icon: Texture2D, callback: Callable) -> void:
var button := Button.new()
button.text = "%s\n%s" % [title, subtitle]
button.icon = icon
button.expand_icon = true
button.add_theme_constant_override("icon_max_width", 38)
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
button.custom_minimum_size = Vector2(220, 86)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.pressed.connect(callback)
action_grid.add_child(button)
action_buttons.append(button)
func _build_workspace() -> void:
workspace = PanelContainer.new()
workspace.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(workspace)
chart_panel = workspace
clerk_panel = workspace
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 14)
margin.add_theme_constant_override("margin_right", 14)
margin.add_theme_constant_override("margin_top", 10)
margin.add_theme_constant_override("margin_bottom", 12)
workspace.add_child(margin)
tabs = TabContainer.new()
tabs.size_flags_vertical = Control.SIZE_EXPAND_FILL
tabs.tab_changed.connect(func(_index: int): _render_chart())
margin.add_child(tabs)
for definition in CHART_TABS:
var page := VBoxContainer.new()
page.name = str(definition[0])
page.add_theme_constant_override("separation", 8)
tabs.add_child(page)
if str(definition[1]) == "orders":
_build_order_sheet(page)
else:
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
page.add_child(scroll)
var label := _clinical_text("chart_%s" % definition[1])
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.add_child(label)
chart_views[definition[1]] = label
func _build_order_sheet(page: VBoxContainer) -> void:
order_sheet = page
final_orders_notice = Label.new()
final_orders_notice.visible = false
final_orders_notice.text = "FINAL ORDERS — Add or cancel orders for the patient’s current condition, then submit."
final_orders_notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
page.add_child(final_orders_notice)
var search_row := HBoxContainer.new()
search_row.add_theme_constant_override("separation", 6)
page.add_child(search_row)
search = LineEdit.new()
search.placeholder_text = "Search tests, treatments, medications, and procedures"
search.size_flags_horizontal = Control.SIZE_EXPAND_FILL
search.text_submitted.connect(func(_value: String): _submit_search())
search_row.add_child(search)
search_button = Button.new()
search_button.icon = ICON_SEARCH
search_button.tooltip_text = "Search the order catalog"
search_button.custom_minimum_size = Vector2(44, 42)
search_button.pressed.connect(_submit_search)
search_row.add_child(search_button)
search_results = VBoxContainer.new()
search_results.add_theme_constant_override("separation", 4)
page.add_child(search_results)
var staged_title := Label.new()
staged_title.text = "Orders awaiting confirmation"
Brand.apply_font(staged_title)
page.add_child(staged_title)
staged_orders = VBoxContainer.new()
page.add_child(staged_orders)
var staged_actions := HBoxContainer.new()
page.add_child(staged_actions)
commit_orders_button = Button.new()
commit_orders_button.text = "Confirm Orders"
commit_orders_button.pressed.connect(_commit_orders)
staged_actions.add_child(commit_orders_button)
clear_orders_button = Button.new()
clear_orders_button.text = "Clear"
clear_orders_button.pressed.connect(_clear_staged_orders)
staged_actions.add_child(clear_orders_button)
page.add_child(HSeparator.new())
var active_title := Label.new()
active_title.text = "Order sheet"
active_title.add_theme_font_size_override("font_size", 18)
Brand.apply_font(active_title)
page.add_child(active_title)
var header := HBoxContainer.new()
for item in [["Order", 2.4], ["Route", 0.8], ["Frequency", 1.0], ["Order Time", 1.0], ["Report Time", 1.0], ["", 0.8]]:
var label := Label.new()
label.text = item[0]
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.size_flags_stretch_ratio = item[1]
Brand.apply_font(label)
header.add_child(label)
page.add_child(header)
var active_scroll := ScrollContainer.new()
active_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
page.add_child(active_scroll)
active_orders = VBoxContainer.new()
active_orders.size_flags_horizontal = Control.SIZE_EXPAND_FILL
active_scroll.add_child(active_orders)
func _build_footer() -> void:
var footer := HBoxContainer.new()
footer.add_theme_constant_override("separation", 12)
add_child(footer)
simulated_clock = _footer_label(HORIZONTAL_ALIGNMENT_LEFT)
footer.add_child(simulated_clock)
elapsed_clock = _footer_label(HORIZONTAL_ALIGNMENT_CENTER)
footer.add_child(elapsed_clock)
remaining_clock = _footer_label(HORIZONTAL_ALIGNMENT_RIGHT)
footer.add_child(remaining_clock)
status = Label.new()
status.visible = false
status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
add_child(status)
func _footer_label(h_align: HorizontalAlignment) -> Label:
var label := Label.new()
label.horizontal_alignment = h_align
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.add_theme_font_size_override("font_size", 14)
Brand.apply_font(label)
return label
func _build_dialogs() -> void:
intro_dialog = AcceptDialog.new()
intro_dialog.close_requested.connect(_accept_opening_step)
intro_dialog.confirmed.connect(_accept_opening_step)
add_child(intro_dialog)
var intro_box := VBoxContainer.new()
intro_box.custom_minimum_size = Vector2(760, 480)
intro_dialog.add_child(intro_box)
intro_title = Label.new()
intro_title.add_theme_font_size_override("font_size", 22)
Brand.apply_font(intro_title)
intro_box.add_child(intro_title)
intro_body = _clinical_text("opening")
intro_body.size_flags_vertical = Control.SIZE_EXPAND_FILL
intro_body.fit_content = false
intro_body.scroll_active = true
intro_box.add_child(intro_body)
exam_dialog = ConfirmationDialog.new()
exam_dialog.title = "History & Physical"
exam_dialog.ok_button_text = "Review selection"
exam_dialog.confirmed.connect(_review_exam_selection)
add_child(exam_dialog)
var exam_scroll := ScrollContainer.new()
exam_scroll.custom_minimum_size = Vector2(680, 480)
exam_dialog.add_child(exam_scroll)
var exam_box := VBoxContainer.new()
exam_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
exam_scroll.add_child(exam_box)
var history_help := Label.new()
history_help.text = "Select physical examination components. Findings become available after the confirmed time advance."
history_help.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
exam_box.add_child(history_help)
exam_box.set_meta("exam_container", true)
exam_summary_dialog = ConfirmationDialog.new()
exam_summary_dialog.title = "Confirm History & Physical"
exam_summary_dialog.confirmed.connect(_commit_exams)
add_child(exam_summary_dialog)
exam_summary = _clinical_text("exam_confirmation")
exam_summary.custom_minimum_size = Vector2(620, 300)
exam_summary_dialog.add_child(exam_summary)
findings_dialog = AcceptDialog.new()
findings_dialog.title = "History and Physical"
add_child(findings_dialog)
findings_body = _clinical_text("exam_findings")
findings_body.custom_minimum_size = Vector2(720, 440)
findings_body.fit_content = false
findings_body.scroll_active = true
findings_dialog.add_child(findings_body)
time_dialog = ConfirmationDialog.new()
time_dialog.title = "Obtain Results / See Patient Later"
time_dialog.confirmed.connect(_commit_time)
add_child(time_dialog)
var time_box := VBoxContainer.new()
time_box.custom_minimum_size = Vector2(540, 260)
time_dialog.add_child(time_box)
time_preset = OptionButton.new()
for item in [["10 minutes", 10], ["1 hour", 60], ["4 hours", 240], ["12 hours", 720], ["1 day", 1440], ["Custom target", -1]]:
time_preset.add_item(item[0])
time_preset.set_item_metadata(time_preset.item_count - 1, item[1])
time_preset.item_selected.connect(_update_time_preview)
time_box.add_child(time_preset)
custom_target = LineEdit.new()
custom_target.placeholder_text = "YYYY-MM-DD HH:MM (case time)"
custom_target.visible = false
custom_target.text_changed.connect(func(_value: String): _update_time_preview(time_preset.selected))
time_box.add_child(custom_target)
time_preview = Label.new()
time_preview.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
time_box.add_child(time_preview)
location_dialog = ConfirmationDialog.new()
location_dialog.title = "Change Location"
location_dialog.confirmed.connect(_commit_location)
add_child(location_dialog)
var location_box := VBoxContainer.new()
location_box.custom_minimum_size = Vector2(480, 180)
location_dialog.add_child(location_box)
var location_help := Label.new()
location_help.text = "Select the patient’s destination:"
location_box.add_child(location_help)
location_option = OptionButton.new()
location_box.add_child(location_option)
end_dialog = ConfirmationDialog.new()
end_dialog.title = "Enter Final Orders?"
end_dialog.dialog_text = "This ends active case management. You will have up to two minutes to finalize or cancel patient-care orders."
end_dialog.ok_button_text = "Enter Final Orders"
end_dialog.confirmed.connect(func(): CCSService.send_command("end_case"))
add_child(end_dialog)
final_instructions = AcceptDialog.new()
final_instructions.title = "Case-end Instructions"
final_instructions.dialog_text = "Finalize care of the patient’s current condition.\n\n• Cancel orders that should stop now.\n• Add orders that should be done now.\n• Add relevant future-care orders.\n\nThen submit final orders before time expires."
final_instructions.confirmed.connect(_open_order_sheet)
add_child(final_instructions)
abandon_dialog = ConfirmationDialog.new()
abandon_dialog.title = "Abandon CCS case?"
abandon_dialog.dialog_text = "This attempt will end without a score and cannot be resumed."
abandon_dialog.ok_button_text = "Abandon Case"
abandon_dialog.confirmed.connect(_confirm_abandon)
add_child(abandon_dialog)
func _build_runtime_services() -> void:
timer = Timer.new()
timer.wait_time = 1
timer.timeout.connect(_tick)
add_child(timer)
CCSService.attempt_changed.connect(_render)
CCSService.attempt_abandoned.connect(_on_abandoned)
CCSService.orders_changed.connect(_render_search)
CCSService.request_failed.connect(_request_failed)
CCSService.report_submitted.connect(_report_submitted)
report_dialog = preload("res://scripts/ui/ccs_report_dialog.gd").new()
add_child(report_dialog)
func on_show(payload: Variant = null) -> void:
_set_status("")
leaving = false
if typeof(payload) == TYPE_STRING and not str(payload).is_empty():
CCSService.load_attempt(str(payload))
_render()
timer.start()
func on_hide() -> void:
timer.stop()
opening_active = false
_dismiss_dialogs()
func _dismiss_dialogs() -> void:
# Dialogs are Window nodes, so hiding their parent Control does not dismiss
# them. Always close every CCS overlay when navigation or logout hides the
# player to prevent case content from appearing over another screen.
for dialog in _ccs_dialogs():
if dialog != null:
dialog.hide()
if report_dialog != null and report_dialog.has_method("hide_dialog"):
report_dialog.hide_dialog()
func _ccs_dialogs() -> Array[Window]:
var dialogs: Array[Window] = []
for dialog in [intro_dialog, exam_dialog, exam_summary_dialog, findings_dialog, time_dialog, location_dialog, end_dialog, final_instructions, abandon_dialog]:
if dialog is Window:
dialogs.append(dialog)
return dialogs
func _render() -> void:
# This screen stays subscribed to the shared CCS attempt while hidden.
# Never allow its Window-based dialogs to appear over CCS Cases 2.
if not visible:
opening_active = false
_dismiss_dialogs()
return
var attempt: Dictionary = CCSService.attempt
if attempt.is_empty():
return
if str(attempt.get("state", "")) == "completed":
main.show_screen("ccs_review", attempt)
return
var was_final := exit_button.text == "Submit Final Orders"
var final_orders := str(attempt.get("state", "")) == "final_orders"
for code in staged_order_codes.duplicate():
if code in attempt.get("activeOrderCodes", []):
staged_order_codes.erase(code)
staged_order_names.erase(code)
heading.text = "CCS Case | %s" % _humanize_code(str(attempt.get("location", "")))
var total := int(attempt.get("realTimeMinutes", 20))
allotted_label.text = (
"Legacy untimed CCS case"
if str(attempt.get("mode", "timed")) == "practice"
else "Maximum allotted real time: %d minutes + 2 minutes for final orders" % maxi(1, total - 2)
)
exit_button.text = "Submit Final Orders" if final_orders else "Exit Case"
exit_button.tooltip_text = "Submit final orders and complete the case" if final_orders else "Enter final orders"
final_orders_notice.visible = final_orders
for button in action_buttons:
button.disabled = final_orders
action_buttons[1].disabled = false
_render_active_orders()
_render_staged_orders()
_render_chart()
_tick()
_apply_text_scale()
if not bool(attempt.get("openingCompleted", true)) and not opening_active:
_start_opening()
if final_orders and not was_final:
final_instructions.popup_centered(Vector2i(820, 500))
_open_order_sheet()
if awaiting_exam_result and not CCSService.is_loading:
awaiting_exam_result = false
_show_new_findings()
func _start_opening() -> void:
opening_active = true
opening_step = 0
_show_opening_step()
func _show_opening_step() -> void:
var attempt: Dictionary = CCSService.attempt
match opening_step:
0:
intro_dialog.title = "Case Introduction"
intro_title.text = _simulated_time_text(0)
intro_body.text = "[center][b]%s[/b]\n\n%s[/center]" % [
_escape_bbcode(_humanize_code(str(attempt.get("location", "")))),
_escape_bbcode(str(attempt.get("reasonForEncounter", "")))
]
1:
intro_dialog.title = "Initial Vital Signs"
intro_title.text = _simulated_time_text(0)
intro_body.text = _vitals_bbcode(attempt.get("vitals", {}))
2:
intro_dialog.title = "Initial History"
intro_title.text = str(attempt.get("patientSummary", ""))
intro_body.text = _history_bbcode()
intro_dialog.popup_centered(Vector2i(820, 560))
func _accept_opening_step() -> void:
if not opening_active:
return
opening_step += 1
if opening_step < 3:
call_deferred("_show_opening_step")
else:
opening_active = false
CCSService.send_command("acknowledge_opening")
func _open_exam() -> void:
if _is_final_orders():
return
var container: VBoxContainer
for node in exam_dialog.find_children("*", "VBoxContainer", true, false):
if node.has_meta("exam_container"):
container = node
break
if container == null:
return
for child in container.get_children():
if child is CheckBox:
child.queue_free()
exam_checks.clear()
interval_history_check = CheckBox.new()
interval_history_check.text = "Interval / follow-up history (1 min)"
container.add_child(interval_history_check)
for raw_exam in CCSService.attempt.get("availableExams", []):
var exam: Dictionary = raw_exam
var check := CheckBox.new()
var minutes := maxi(1, int(exam.get("durationMinutes", 1)))
check.text = "%s (%d min)" % [str(exam.get("name", _humanize_code(str(exam.get("code", ""))))), minutes]
check.set_meta("exam_code", str(exam.get("code", "")))
check.set_meta("exam_minutes", minutes)
container.add_child(check)
exam_checks[str(exam.get("code", ""))] = check
exam_dialog.popup_centered(Vector2i(740, 600))
func _review_exam_selection() -> void:
staged_exam_codes.clear()
staged_interval_history = interval_history_check != null and interval_history_check.button_pressed
var total := 0
var lines: Array[String] = []
if staged_interval_history:
total += 1
lines.append("• Interval / follow-up history — 1 min")
for code in exam_checks:
var check: CheckBox = exam_checks[code]
if check.button_pressed:
staged_exam_codes.append(code)
var minutes := int(check.get_meta("exam_minutes", 1))
total += minutes
lines.append("• %s — %d min" % [check.text.get_slice(" (", 0), minutes])
if staged_exam_codes.is_empty() and not staged_interval_history:
_set_status("Select interval history or at least one examination component.")
return
exam_summary.text = "%s\n\n[b]Total simulated time: %d minutes[/b]\nThis will advance the clock to [b]%s[/b]." % [
"\n".join(lines), total, _simulated_time_text(int(CCSService.attempt.get("simulatedMinute", 0)) + total)
]
exam_summary_dialog.popup_centered(Vector2i(680, 420))
func _commit_exams() -> void:
var commands: Array = []
if staged_interval_history:
commands.append({"type": "interval_history", "code": "", "minutes": 0})
for code in staged_exam_codes:
commands.append({"type": "exam", "code": code, "minutes": 0})
pending_findings_before = _chart_entries("progress").size()
awaiting_exam_result = true
CCSService.send_batch(commands)
func _show_new_findings() -> void:
var entries := _chart_entries("progress")
var start := mini(pending_findings_before, entries.size())
var text := "[center][b]%s[/b][/center]\n\n" % _simulated_time_text(int(CCSService.attempt.get("simulatedMinute", 0)))
for index in range(start, entries.size()):
var entry: Dictionary = entries[index]
if str(entry.get("code", "")).is_empty():
continue
text += "[b]%s[/b]\n%s\n\n" % [_escape_bbcode(str(entry.get("title", ""))), _escape_bbcode(str(entry.get("text", "")))]
findings_body.text = text
findings_dialog.popup_centered(Vector2i(780, 540))
func _open_order_sheet() -> void:
tabs.current_tab = 0
search.grab_focus()
func _submit_search() -> void:
if search.text.strip_edges().length() < 2:
_set_status("Enter at least two characters to search.")
return
_set_status("")
CCSService.search_orders(search.text)
func _render_search() -> void:
_clear(search_results)
if CCSService.orders.is_empty():
_set_status(
"No matching orders found. Try a clinical name or category."
if search.text.strip_edges().length() >= 2
else "")
return
_set_status("")
for raw_order in CCSService.orders:
var order: Dictionary = raw_order
var code := str(order.get("code", ""))
var button := Button.new()
button.text = "%s · %s" % [str(order.get("canonicalName", code)), _humanize_code(str(order.get("category", "")))]
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.disabled = code in staged_order_codes or code in CCSService.attempt.get("activeOrderCodes", [])
button.pressed.connect(_stage_order.bind(code))
search_results.add_child(button)
func _stage_order(code: String) -> void:
if code.is_empty() or code in staged_order_codes:
return
staged_order_names[code] = _catalog_name(code)
staged_order_codes.append(code)
search.text = ""
CCSService.orders = []
_render_search()
_render_staged_orders()
func _render_staged_orders() -> void:
_clear(staged_orders)
for code in staged_order_codes:
var row := HBoxContainer.new()
staged_orders.add_child(row)
var label := Label.new()
label.text = _catalog_name(code)
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(label)
var remove := Button.new()
remove.text = "Remove"
remove.pressed.connect(_unstage_order.bind(code))
row.add_child(remove)
commit_orders_button.disabled = staged_order_codes.is_empty()
clear_orders_button.disabled = staged_order_codes.is_empty()
func _unstage_order(code: String) -> void:
staged_order_codes.erase(code)
staged_order_names.erase(code)
_render_staged_orders()
func _clear_staged_orders() -> void:
staged_order_codes.clear()
staged_order_names.clear()
_render_staged_orders()
func _commit_orders() -> void:
if staged_order_codes.is_empty():
return
var commands: Array = []
for code in staged_order_codes:
commands.append({"type": "order", "code": code, "minutes": 0})
CCSService.orders = []
_render_search()
_render_staged_orders()
CCSService.send_batch(commands)
func _render_active_orders() -> void:
_clear(active_orders)
var rows: Array = CCSService.attempt.get("activeOrders", [])
if rows.is_empty():
var empty := Label.new()
empty.text = "No active orders."
active_orders.add_child(empty)
return
for raw_order in rows:
var order: Dictionary = raw_order
var row := HBoxContainer.new()
row.custom_minimum_size.y = 44
active_orders.add_child(row)
_add_order_cell(row, str(order.get("name", _humanize_code(str(order.get("code", ""))))), 2.4)
_add_order_cell(row, str(order.get("route", "—")) if not str(order.get("route", "")).is_empty() else "—", 0.8)
_add_order_cell(row, str(order.get("frequency", "—")) if not str(order.get("frequency", "")).is_empty() else "—", 1.0)
_add_order_cell(row, _simulated_time_text(int(order.get("orderedMinute", 0)), false), 1.0)
var report_value: Variant = order.get("reportMinute", null)
var report_text := "—"
if bool(order.get("isPending", false)):
report_text = "Pending"
elif bool(order.get("hasResult", report_value != null)) and report_value != null:
report_text = _simulated_time_text(int(report_value), false)
_add_order_cell(row, report_text, 1.0)
var cancel := Button.new()
cancel.text = "Cancel"
cancel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
cancel.size_flags_stretch_ratio = 0.8
cancel.pressed.connect(_cancel_order.bind(str(order.get("code", ""))))
row.add_child(cancel)
func _add_order_cell(row: HBoxContainer, value: String, ratio: float) -> void:
var label := Label.new()
label.text = value
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.size_flags_stretch_ratio = ratio
row.add_child(label)
func _cancel_order(code: String) -> void:
CCSService.send_batch([{"type": "cancel_order", "code": code, "minutes": 0}])
func _open_time_dialog() -> void:
if _is_final_orders():
return
time_preset.select(0)
custom_target.visible = false
_update_time_preview(0)
time_dialog.popup_centered(Vector2i(600, 350))
func _update_time_preview(index: int) -> void:
var minutes := int(time_preset.get_item_metadata(index))
custom_target.visible = minutes < 0
if minutes < 0:
minutes = _custom_target_minutes()
time_preview.text = "Clock will advance to %s." % _simulated_time_text(int(CCSService.attempt.get("simulatedMinute", 0)) + maxi(0, minutes))
time_dialog.get_ok_button().disabled = minutes <= 0
func _custom_target_minutes() -> int:
var value := custom_target.text.strip_edges().replace(" ", "T")
if value.length() == 16:
value += ":00"
var target := Time.get_unix_time_from_datetime_string(value)
if target <= 0:
return 0
var start_text := str(CCSService.attempt.get("simulatedStartUtc", "")).trim_suffix("Z")
var start_unix := Time.get_unix_time_from_datetime_string(start_text)
var current_unix := start_unix + int(CCSService.attempt.get("simulatedMinute", 0)) * 60
return maxi(0, int((target - current_unix) / 60.0))
func _commit_time() -> void:
var minutes := int(time_preset.get_item_metadata(time_preset.selected))
if minutes < 0:
minutes = _custom_target_minutes()
if minutes > 0:
CCSService.send_command("advance", "", minutes)
func _open_location_dialog() -> void:
if _is_final_orders():
return
location_option.clear()
for raw_location in CCSService.attempt.get("allowedLocations", []):
var location := str(raw_location)
location_option.add_item(_humanize_code(location))
location_option.set_item_metadata(location_option.item_count - 1, location)
location_dialog.get_ok_button().disabled = location_option.item_count == 0
location_dialog.popup_centered(Vector2i(540, 280))
func _commit_location() -> void:
if location_option.item_count > 0:
CCSService.send_command("location", str(location_option.get_item_metadata(location_option.selected)))
func _request_end_case() -> void:
if _is_final_orders():
CCSService.send_command("exit_case")
else:
end_dialog.popup_centered(Vector2i(560, 260))
func leave_case() -> void:
abandon_dialog.popup_centered(Vector2i(520, 220))
func _confirm_abandon() -> void:
if leaving:
return
leaving = true
CCSService.abandon()
func _on_abandoned() -> void:
if leaving:
leaving = false
main.show_screen("ccs_cases_2" if CCSService.launch_origin == "ccs_cases_2" else "ccs_hub")
func _render_chart() -> void:
for definition in CHART_TABS:
var category := str(definition[1])
if category == "orders":
continue
var label: RichTextLabel = chart_views.get(category)
if label == null:
continue
label.text = _chart_category_bbcode(category)
func _chart_category_bbcode(category: String) -> String:
if category == "vitals":
return _vital_history_bbcode()
var entries := _chart_entries(category)
if entries.is_empty():
return "[center][color=#8490a5]No %s available.[/color][/center]" % str(_tab_title(category)).to_lower()
var text := ""
for raw_entry in entries:
var entry: Dictionary = raw_entry
text += "[b]%s[/b] [color=#8490a5]%s[/color]\n%s\n\n" % [
_escape_bbcode(str(entry.get("title", _tab_title(category)))),
_escape_bbcode(_simulated_time_text(int(entry.get("simulatedMinute", 0)), false)),
_escape_bbcode(str(entry.get("text", "")))
]
return text
func _chart_entries(category: String) -> Array:
var result: Array = []
for raw_entry in CCSService.attempt.get("chart", []):
var entry: Dictionary = raw_entry
var actual := str(entry.get("category", "progress"))
if actual == "exam":
actual = "progress"
elif actual == "results":
actual = "other_test"
if actual == category:
result.append(entry)
return result
func _vital_history_bbcode() -> String:
var snapshots: Array = CCSService.attempt.get("vitalHistory", [])
if snapshots.is_empty():
return _vitals_bbcode(CCSService.attempt.get("vitals", {}))
var text := ""
for raw_snapshot in snapshots:
var snapshot: Dictionary = raw_snapshot
text += "[b]%s[/b]\n%s\n\n" % [
_escape_bbcode(_simulated_time_text(int(snapshot.get("simulatedMinute", 0)), false)),
_vitals_bbcode(snapshot.get("values", {}))
]
return text
func _vitals_bbcode(vitals: Dictionary) -> String:
var lines: Array[String] = []
for key in _ordered_vital_keys(vitals):
lines.append("[b]%s:[/b] %s" % [_escape_bbcode(_vital_label(key)), _escape_bbcode(str(vitals[key]))])
return "\n".join(lines)
func _history_bbcode() -> String:
var sections: Array = CCSService.attempt.get("historySections", [])
if sections.is_empty():
return "[b]History of present illness[/b]\n%s\n\n[b]Initial appearance[/b]\n%s" % [
_escape_bbcode(str(CCSService.attempt.get("history", ""))),
_escape_bbcode(str(CCSService.attempt.get("appearance", "")))
]
var text := ""
for raw_section in sections:
var section: Dictionary = raw_section
text += "[b]%s[/b]\n%s\n\n" % [_escape_bbcode(str(section.get("title", ""))), _escape_bbcode(str(section.get("text", "")))]
return text
func _tick() -> void:
if CCSService.attempt.is_empty():
return
var now := Time.get_unix_time_from_system()
var created_text := str(CCSService.attempt.get("createdUtc", "")).trim_suffix("Z")
var created := Time.get_unix_time_from_datetime_string(created_text)
var elapsed := maxi(0, int(now - created))
var phase_deadline := CCSService.deadline_unix()
var hard_deadline := CCSService.hard_deadline_unix()
var display_deadline := phase_deadline if _is_final_orders() else hard_deadline
var remaining := maxi(0, int(ceil(display_deadline - now)))
var phase_remaining := maxi(0, int(ceil(phase_deadline - now)))
simulated_clock.text = "Elapsed SIMULATED case time: %s" % _elapsed_simulated_text(int(CCSService.attempt.get("simulatedMinute", 0)))
elapsed_clock.text = "Elapsed REAL time: %s" % _duration_text(elapsed)
remaining_clock.text = (
"Legacy untimed case"
if hard_deadline <= 0
else "%s REAL time: %s" % ["Final-order" if _is_final_orders() else "Remaining", _duration_text(remaining)]
)
if phase_remaining == 0 and phase_deadline > 0 and not CCSService.is_loading:
CCSService.load_attempt(str(CCSService.attempt.get("id", "")))
func _simulated_time_text(minute: int, include_day: bool = true) -> String:
var start_text := str(CCSService.attempt.get("simulatedStartUtc", "")).trim_suffix("Z")
var unix := Time.get_unix_time_from_datetime_string(start_text)
if unix <= 0:
return "Minute %d" % minute
var value := Time.get_datetime_dict_from_unix_time(unix + minute * 60)
var hour := int(value.hour)
var suffix := "AM" if hour < 12 else "PM"
var display_hour := hour % 12
if display_hour == 0:
display_hour = 12
var day_number := 1 + int(floor(float(minute) / 1440.0))
return "%s%d:%02d %s" % ["Day %d @ " % day_number if include_day else "", display_hour, int(value.minute), suffix]
func _elapsed_simulated_text(minutes: int) -> String:
@warning_ignore("integer_division")
return "%d Days %02d Hrs %02d Mins" % [minutes / 1440, (minutes % 1440) / 60, minutes % 60]
func _duration_text(seconds: int) -> String:
@warning_ignore("integer_division")
return "%02d:%02d" % [seconds / 60, seconds % 60]
func _ordered_vital_keys(vitals: Dictionary) -> Array[String]:
var ordered: Array[String] = []
for key in ["temperature", "bloodPressure", "pulse", "respirations", "oxygenSaturation", "height", "weight", "bmi"]:
if vitals.has(key):
ordered.append(key)
var remaining: Array[String] = []
for raw_key in vitals.keys():
var key := str(raw_key)
if not key in ordered:
remaining.append(key)
remaining.sort()
ordered.append_array(remaining)
return ordered
func _vital_label(key: String) -> String:
match key:
"bloodPressure": return "Blood Pressure"
"oxygenSaturation": return "Oxygen Saturation"
"bmi": return "Body Mass Index"
_: return _humanize_code(key)
func _clinical_text(region: String) -> RichTextLabel:
var label := RichTextLabel.new()
label.bbcode_enabled = true
label.selection_enabled = true
label.fit_content = true
label.scroll_active = false
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.add_theme_font_override("normal_font", Brand.font)
label.add_theme_font_override("bold_font", Brand.font)
label.set_meta("ccs_base_font_size", 16)
label.set_meta("ccs_region", region)
label.gui_input.connect(_text_input.bind(label))
clinical_labels.append(label)
return label
func _text_input(event: InputEvent, label: RichTextLabel) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
call_deferred("_commit_selection", label)
func _commit_selection(label: RichTextLabel) -> void:
var text := label.get_selected_text()
if text.is_empty():
return
var source := label.get_parsed_text()
var start := source.find(text)
if start >= 0:
CCSService.toggle_highlight(str(CCSService.attempt.get("id", "")), str(label.get_meta("ccs_region", "")), start, start + text.length())
func _zoom(delta: float) -> void:
CCSService.adjust_text_scale(delta)
_apply_text_scale()
func _apply_text_scale() -> void:
for label in clinical_labels:
var font_size := maxi(12, int(round(float(label.get_meta("ccs_base_font_size", 16)) * CCSService.text_scale)))
label.add_theme_font_size_override("normal_font_size", font_size)
label.add_theme_font_size_override("bold_font_size", font_size)
func _report() -> void:
if report_dialog != null:
report_dialog.open(CCSService.attempt)
func _report_submitted(_id: String) -> void:
_set_status("Case report submitted.", false)
func _request_failed(message: String) -> void:
if visible:
awaiting_exam_result = false
_set_status(message)
func _set_status(message: String, is_error: bool = true) -> void:
status.text = message
status.visible = not message.is_empty()
status.add_theme_color_override("font_color", Color("ff6b7f") if is_error else Brand.SUCCESS)
func _catalog_name(code: String) -> String:
if staged_order_names.has(code):
return str(staged_order_names[code])
for raw_order in CCSService.orders:
var order: Dictionary = raw_order
if str(order.get("code", "")) == code:
return str(order.get("canonicalName", _humanize_code(code)))
return _humanize_code(code)
func _tab_title(category: String) -> String:
for definition in CHART_TABS:
if str(definition[1]) == category:
return str(definition[0])
return _humanize_code(category)
func _humanize_code(value: String) -> String:
return value.replace("_", " ").capitalize()
func _escape_bbcode(value: String) -> String:
return value.replace("[", "[lb]").replace("]", "[rb]")
func _is_final_orders() -> bool:
return str(CCSService.attempt.get("state", "")) == "final_orders"
func _clear(parent: Node) -> void:
for child in parent.get_children():
child.queue_free()
func _update_responsive_layout() -> void:
if action_grid == null:
return
var narrow := size.x > 0 and size.x < RESPONSIVE_BREAKPOINT
header_layout.vertical = narrow
action_grid.columns = 2 if narrow else 4
toolbar.size_flags_horizontal = Control.SIZE_EXPAND_FILL if narrow else Control.SIZE_SHRINK_END
for button in action_buttons:
button.custom_minimum_size.x = 0 if narrow else 220
queue_sort()
func _on_theme_changed(_dark: bool) -> void:
call_deferred("_apply_theme")
func _apply_theme() -> void:
if header_panel == null:
return
var dark: bool = theme_service != null and theme_service.is_dark_mode()
header_panel.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
workspace.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
heading.add_theme_color_override("font_color", Brand.text_color(dark))
allotted_label.add_theme_color_override("font_color", Brand.ROYAL_BLUE if not dark else Brand.SKY_BLUE)
for label in [simulated_clock, elapsed_clock, remaining_clock]:
label.add_theme_color_override("font_color", Brand.text_color(dark))
final_orders_notice.add_theme_color_override("font_color", Brand.AMBER)
for button in toolbar_buttons + action_buttons:
button.add_theme_font_override("font", Brand.font)
button.add_theme_color_override("font_color", Brand.text_color(dark))
button.add_theme_color_override("font_hover_color", Brand.text_color(dark))
button.add_theme_stylebox_override("normal", Brand.content_card_box(dark, false))
button.add_theme_stylebox_override("hover", Brand.content_card_box(dark, true))
for label in clinical_labels:
label.add_theme_color_override("default_color", Brand.text_color(dark))
_apply_dialog_theme(dark)
Brand.set_theme_toggle_icon(theme_button, dark)
theme_button.tooltip_text = "Switch to light mode" if dark else "Switch to dark mode"
_apply_text_scale()
func _apply_dialog_theme(dark: bool) -> void:
var popup_theme := Theme.new()
var text := Brand.text_color(dark)
var muted := Brand.muted_color(dark)
var panel := Brand.content_card_box(dark, false)
panel.content_margin_left = 18
panel.content_margin_right = 18
panel.content_margin_top = 16
panel.content_margin_bottom = 16
for type_name in ["Label", "Button", "CheckBox", "OptionButton", "LineEdit", "PopupMenu"]:
popup_theme.set_font("font", type_name, Brand.font)
popup_theme.set_color("font_color", type_name, text)
popup_theme.set_font("normal_font", "RichTextLabel", Brand.font)
popup_theme.set_font("bold_font", "RichTextLabel", Brand.font)
popup_theme.set_color("default_color", "RichTextLabel", text)
popup_theme.set_color("font_selected_color", "RichTextLabel", text)
popup_theme.set_color("font_hover_color", "Button", text)
popup_theme.set_color("font_pressed_color", "Button", text)
popup_theme.set_color("font_disabled_color", "Button", muted)
popup_theme.set_color("font_hover_color", "OptionButton", text)
popup_theme.set_color("font_pressed_color", "OptionButton", text)
popup_theme.set_color("font_hover_color", "CheckBox", text)
popup_theme.set_color("font_pressed_color", "CheckBox", text)
popup_theme.set_color("font_hover_color", "PopupMenu", text)
popup_theme.set_color("font_accelerator_color", "PopupMenu", muted)
popup_theme.set_color("font_placeholder_color", "LineEdit", muted)
popup_theme.set_color("title_color", "Window", text)
popup_theme.set_font("title_font", "Window", Brand.font)
for type_name in ["Button", "OptionButton"]:
popup_theme.set_stylebox("normal", type_name, Brand.secondary_button_box(dark))
popup_theme.set_stylebox("hover", type_name, Brand.secondary_button_box(dark, true))
popup_theme.set_stylebox("pressed", type_name, Brand.secondary_button_box(dark, true))
popup_theme.set_stylebox("normal", "LineEdit", Brand.field_box(dark))
popup_theme.set_stylebox("focus", "LineEdit", Brand.field_box(dark, true))
for type_name in ["Window", "AcceptDialog", "ConfirmationDialog"]:
popup_theme.set_stylebox("panel", type_name, panel)
popup_theme.set_stylebox("panel", "PopupMenu", panel)
popup_theme.set_stylebox("hover", "PopupMenu", Brand.secondary_button_box(dark, true))
for dialog in _ccs_dialogs():
dialog.theme = popup_theme