Files
TheTree_mock/scripts/ui/staff_management_panel.gd
T

410 lines
18 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.
class_name StaffManagementPanel
extends VBoxContainer
## Roster and assignment editing only. Runtime owns Shift, staffing, movement,
## and persistence; this panel never edits a Staff or Shift instance directly.
signal status_requested(message: String, success: bool)
const ASSIGNMENT_COMMAND: StringName = &"set_staff_work_assignment"
const KEY_PREFIX := "ui.staff_assignment."
const ROLE_NAMES := {
&"attendant": "接客", &"bartender": "酒場", &"clerk": "会計",
&"cleaner": "清掃", &"cook": "調理", &"guard": "警備",
&"maintenance": "保守", &"masseur": "施術", &"medic": "医療",
&"porter": "物流", &"receptionist": "受付",
}
const STATE_NAMES := ["勤務外", "出勤中", "勤務中", "移動中", "作業中", "休憩中", "食事中", "睡眠中", "治療へ移動", "避難中", "行動不能", "ストライキ", "拘束中", "捕虜", "勤務不可"]
var summary_label: Label
var roster_list: VBoxContainer
var roster_buttons: Dictionary = {}
var editor_panel: VBoxContainer
var staff_detail_label: Label
var facility_option: OptionButton
var shift_option: OptionButton
var apply_button: Button
var result_label: Label
var selected_staff_id: StringName = &""
var _runtime: Variant
var _style_button: Callable
var _staff_rows: Array[Dictionary] = []
var _facility_rows: Array[Dictionary] = []
var _draft_facility_id: StringName = &""
var _draft_template_id: StringName = &"DAY"
var _draft_dirty := false
var _sequence := 1
var _pending_commands: Dictionary = {}
var _queue_instance_id := 0
var _built := false
func configure(runtime: Variant, style_button := Callable()) -> void:
_runtime = runtime
_style_button = style_button
func _ready() -> void:
name = "StaffManagementPanel"
size_flags_horizontal = Control.SIZE_EXPAND_FILL
add_theme_constant_override("separation", 5)
_build_interface()
_built = true
if _runtime != null:
if _runtime.has_signal(&"snapshot_published"):
_runtime.connect(&"snapshot_published", _on_snapshot_published)
if _runtime.has_signal(&"command_completed"):
_runtime.connect(&"command_completed", _on_command_completed)
refresh()
func _build_interface() -> void:
summary_label = _label("まだ従業員を雇っていません。候補から雇用してください。")
summary_label.name = "HiredStaffSummary"
add_child(summary_label)
var scroll := ScrollContainer.new()
scroll.name = "HiredStaffScroll"
scroll.custom_minimum_size.y = 120
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(scroll)
roster_list = VBoxContainer.new()
roster_list.name = "HiredStaffList"
roster_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
roster_list.add_theme_constant_override("separation", 3)
scroll.add_child(roster_list)
editor_panel = VBoxContainer.new()
editor_panel.name = "StaffAssignmentEditor"
editor_panel.add_theme_constant_override("separation", 5)
add_child(editor_panel)
staff_detail_label = _label("")
staff_detail_label.name = "StaffAssignmentDetail"
editor_panel.add_child(staff_detail_label)
editor_panel.add_child(_label("配属先"))
facility_option = OptionButton.new()
facility_option.name = "StaffFacilityOption"
facility_option.fit_to_longest_item = false
facility_option.custom_minimum_size = Vector2(0, 30)
facility_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
facility_option.item_selected.connect(_on_facility_changed)
_style(facility_option)
editor_panel.add_child(facility_option)
editor_panel.add_child(_label("勤務帯"))
shift_option = OptionButton.new()
shift_option.name = "StaffShiftOption"
shift_option.fit_to_longest_item = false
shift_option.custom_minimum_size.y = 30
shift_option.add_item("日勤 06:00〜18:00")
shift_option.set_item_metadata(0, &"DAY")
shift_option.add_item("夜勤 18:00〜翌06:00")
shift_option.set_item_metadata(1, &"NIGHT")
shift_option.item_selected.connect(_on_shift_changed)
_style(shift_option)
editor_panel.add_child(shift_option)
apply_button = Button.new()
apply_button.name = "ApplyStaffAssignment"
apply_button.text = "配属・勤務帯を変更"
apply_button.custom_minimum_size.y = 32
apply_button.pressed.connect(submit_assignment)
_style(apply_button)
editor_panel.add_child(apply_button)
result_label = _label("")
result_label.name = "StaffAssignmentResult"
add_child(result_label)
func refresh() -> void:
if not _built or _runtime == null:
return
var queue: Variant = _runtime.get(&"command_queue")
if queue != null and int(queue.get_instance_id()) != _queue_instance_id:
_queue_instance_id = int(queue.get_instance_id())
_draft_dirty = false
facility_option.get_popup().hide()
shift_option.get_popup().hide()
_synchronize_commands(queue)
_read_roster()
var live_ids: Array[StringName] = []
var unassigned := 0
for row: Dictionary in _staff_rows:
var staff_id := StringName(row.get(&"staff_id", &""))
live_ids.append(staff_id)
if StringName(row.get(&"assigned_facility_id", &"")).is_empty():
unassigned += 1
var button := roster_buttons.get(staff_id) as Button
if button == null:
button = Button.new()
button.name = "HiredStaff_%s" % staff_id
button.custom_minimum_size.y = 32
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
button.toggle_mode = true
button.pressed.connect(select_staff.bind(staff_id))
_style(button)
roster_list.add_child(button)
roster_buttons[staff_id] = button
button.text = "%s %s%s" % [row.get(&"display_name", staff_id), _role_name(row), "・未配属" if StringName(row.get(&"assigned_facility_id", &"")).is_empty() else ""]
button.tooltip_text = "%s / %s" % [_facility_name(StringName(row.get(&"assigned_facility_id", &""))), _state_name(row)]
for raw_id: Variant in roster_buttons.keys():
if not live_ids.has(StringName(raw_id)):
var stale := roster_buttons[raw_id] as Button
roster_list.remove_child(stale)
stale.queue_free()
roster_buttons.erase(raw_id)
if not live_ids.has(selected_staff_id):
selected_staff_id = live_ids[0] if not live_ids.is_empty() else &""
_draft_dirty = false
for raw_id: Variant in roster_buttons:
(roster_buttons[raw_id] as Button).set_pressed_no_signal(StringName(raw_id) == selected_staff_id)
summary_label.text = "雇用済み %d人 / 未配属 %d人" % [_staff_rows.size(), unassigned] if not _staff_rows.is_empty() else "従業員はまだいません。「候補を見る」から雇用してください。"
var scroll := find_child("HiredStaffScroll", true, false) as ScrollContainer
scroll.visible = not _staff_rows.is_empty()
scroll.custom_minimum_size.y = mini(maxi(_staff_rows.size(), 1) * 35, 140)
editor_panel.visible = not selected_staff_id.is_empty()
if editor_panel.visible:
_refresh_editor()
func select_staff(staff_id: StringName) -> void:
selected_staff_id = staff_id
_draft_dirty = false
result_label.text = ""
refresh()
func _refresh_editor() -> void:
if facility_option.get_popup().visible or shift_option.get_popup().visible:
return
var row := _selected_row()
if row.is_empty():
return
var current_facility := StringName(row.get(&"assigned_facility_id", &""))
var template := StringName(String(row.get(&"template_id", "DAY")).to_upper())
if not _draft_dirty:
_draft_facility_id = current_facility
_draft_template_id = &"NIGHT" if template == &"NIGHT" else &"DAY"
staff_detail_label.text = "%s(%s) 日給 G%d\n配属:%s / %s\n%s 疲労 %d / 未払 G%d" % [
row.get(&"display_name", selected_staff_id), _role_name(row), int(row.get(&"daily_wage", 0)),
_facility_name(current_facility), "夜勤" if template == &"NIGHT" else "日勤" if template == &"DAY" else "自由勤務",
_state_name(row), int(row.get(&"fatigue", 0)), int(row.get(&"unpaid_wages", 0)),
]
var effective_from := int(row.get(&"effective_from", -1))
var now := int(get_node("/root/GameClock").get("absolute_minute"))
if effective_from > now:
staff_detail_label.text += "\n反映:%sから" % _minute_text(effective_from)
if row.has(&"recurring") and bool(row.get(&"recurring", false)):
staff_detail_label.text += "\nこの勤務帯を毎日くり返します。"
facility_option.clear()
facility_option.add_item("未配属(施設担当を外す)")
facility_option.set_item_metadata(0, &"")
var selected_index := 0 if _draft_facility_id.is_empty() else -1
for facility: Dictionary in _facility_rows:
var facility_id := StringName(facility.get(&"facility_id", &""))
facility_option.add_item(_facility_name(facility_id))
var index := facility_option.item_count - 1
facility_option.set_item_metadata(index, facility_id)
if facility_id == _draft_facility_id:
selected_index = index
if selected_index < 0:
facility_option.add_item("選択した施設は現在存在しません")
selected_index = facility_option.item_count - 1
facility_option.set_item_metadata(selected_index, _draft_facility_id)
facility_option.set_item_disabled(selected_index, true)
facility_option.select(selected_index)
shift_option.select(1 if _draft_template_id == &"NIGHT" else 0)
var pending := _pending_commands.values().has(selected_staff_id)
facility_option.disabled = pending
shift_option.disabled = pending
var can_edit := bool(row.get(&"can_edit", true))
apply_button.disabled = pending or not can_edit or facility_option.is_item_disabled(selected_index)
apply_button.tooltip_text = "現在の作業や拘束が終わってから変更できます。" if not can_edit else "配属先と毎日の勤務帯をまとめて変更します。"
if not can_edit:
staff_detail_label.text += "\n現在の作業や拘束が終わってから変更できます。"
apply_button.text = "変更を受理済み…" if pending else "配属・勤務帯を変更"
func _on_facility_changed(index: int) -> void:
_draft_facility_id = StringName(facility_option.get_item_metadata(index))
_draft_dirty = true
func _on_shift_changed(index: int) -> void:
_draft_template_id = StringName(shift_option.get_item_metadata(index))
_draft_dirty = true
func submit_assignment() -> void:
if _runtime == null or selected_staff_id.is_empty():
return
if _pending_commands.values().has(selected_staff_id):
_set_result("この従業員の変更はすでに受理されています。", false)
return
var key := StringName("%s%08d" % [KEY_PREFIX, _sequence])
_sequence += 1
var result: Variant = _runtime.call(&"submit_command", ASSIGNMENT_COMMAND, {
&"staff_id": selected_staff_id,
&"facility_id": _draft_facility_id,
&"template_id": _draft_template_id,
}, &"player", selected_staff_id, 75, -1, key)
if result == null or not result.has_method(&"is_success"):
_set_result("配属変更に応答がありません。", false)
return
var pending := false
var queue: Variant = _runtime.get(&"command_queue")
if queue != null:
for command: Dictionary in queue.create_snapshot().get("pending", []):
if int(command.get("command_id", 0)) == int(result.command_id):
pending = true
break
if result.is_success() and pending:
_pending_commands[int(result.command_id)] = selected_staff_id
_set_result("変更を受理しました。次の時間更新で反映します。", true)
else:
_show_command_result(result)
refresh()
func _show_command_result(result: Variant, staff_id: StringName = &"") -> void:
var target_id := staff_id
if target_id.is_empty():
target_id = StringName(result.details.get(&"staff_id", selected_staff_id))
var employee_name := String(target_id)
for row: Dictionary in _staff_rows:
if StringName(row.get(&"staff_id", &"")) == target_id:
employee_name = String(row.get(&"display_name", target_id))
break
if result.is_success():
var details: Dictionary = result.details if result.details is Dictionary else {}
if target_id == selected_staff_id:
_draft_dirty = false
var facility_id := StringName(details.get(&"facility_id", _draft_facility_id))
var template := StringName(details.get(&"template_id", _draft_template_id))
var start := int(details.get(&"effective_from", -1))
var when := "%sから" % _minute_text(start) if start >= 0 else ""
_set_result("%s:%s%s・毎日%sに変更しました。" % [employee_name, when, _facility_name(facility_id), "夜勤" if template == &"NIGHT" else "日勤"], true)
else:
_set_result("%s:%s" % [employee_name, _failure_message(result)], false)
func _failure_message(result: Variant) -> String:
var details: Dictionary = result.details if result.details is Dictionary else {}
var reason := StringName(details.get(&"reason", details.get("reason", &"")))
var reasons := {
&"STAFF_NOT_FOUND": "従業員が見つかりません。",
&"FACILITY_NOT_FOUND": "配属先が見つかりません。施設が完成しているか確認してください。",
&"ROLE_MISMATCH": "この施設には役割が合いません。必要な職種を確認してください。",
&"NO_ACCESS": "従業員が配属先へ移動できません。通路と従業員ドアを確認してください。",
&"NO_PATH": "従業員が配属先へ移動できません。通路と従業員ドアを確認してください。",
&"STAFF_BUSY": "作業中のため変更できません。作業完了後に再試行してください。",
&"CAPACITY_EXCEEDED": "配属先の人員上限を超えています。",
&"FACILITY_FULL": "配属先の人員上限を超えています。",
&"NO_ROUTE": "配属先へ移動できません。通路と従業員ドアを確認してください。",
&"INVALID_TEMPLATE": "日勤または夜勤を選択してください。",
&"STAFF_UNAVAILABLE": "この従業員は現在、勤務を変更できません。状態をご確認ください。",
&"DEFENSE_LOCKED": "防衛参加中は勤務を変更できません。",
&"EVENT_LOCKED": "イベント参加中は勤務を変更できません。",
&"FACILITY_UNSTAFFED": "この施設は人員を配属する施設ではありません。",
&"SHIFT_CONFLICT": "既存の勤務と時間が重なります。勤務予定を確認してください。",
&"BANKRUPT": "運営終了後は配属を変更できません。",
}
if reasons.has(reason):
return reasons[reason]
return "変更できませんでした:%s" % String(result.message) if not String(result.message).is_empty() else "変更できませんでした。配属先と勤務帯を確認してください。"
func _synchronize_commands(queue: Variant) -> void:
_pending_commands.clear()
for metadata: Dictionary in queue.get_accepted_command_metadata():
var key := String(metadata.get("idempotency_key", ""))
if key.begins_with(KEY_PREFIX) and key.trim_prefix(KEY_PREFIX).is_valid_int():
_sequence = maxi(_sequence, int(key.trim_prefix(KEY_PREFIX)) + 1)
for command: Dictionary in queue.create_snapshot().get("pending", []):
if StringName(command.get("command_type", &"")) != ASSIGNMENT_COMMAND:
continue
var payload: Dictionary = command.get("payload", {})
_pending_commands[int(command.get("command_id", 0))] = StringName(payload.get(&"staff_id", payload.get("staff_id", &"")))
func _on_snapshot_published(_snapshot: Dictionary, _digest: String) -> void:
if is_visible_in_tree():
refresh()
func _on_command_completed(result: Variant) -> void:
if not _pending_commands.has(int(result.command_id)):
return
var staff_id := StringName(_pending_commands[int(result.command_id)])
_pending_commands.erase(int(result.command_id))
_show_command_result(result, staff_id)
refresh()
func _set_result(message: String, success: bool) -> void:
result_label.text = message
result_label.add_theme_color_override("font_color", Color("397346") if success else Color("a7402e"))
status_requested.emit(message, success)
func _read_roster() -> void:
_staff_rows.clear()
_facility_rows.clear()
if _runtime.has_method(&"get_staff_management_report"):
var report: Dictionary = _runtime.call(&"get_staff_management_report")
_staff_rows.assign(report.get(&"staff", []))
_facility_rows.assign(report.get(&"facilities", []))
return
_set_result("従業員データを読み込めません。", false)
func _selected_row() -> Dictionary:
for row: Dictionary in _staff_rows:
if StringName(row.get(&"staff_id", &"")) == selected_staff_id:
return row
return {}
func _facility_name(facility_id: StringName) -> String:
if facility_id.is_empty():
return "未配属"
for facility: Dictionary in _facility_rows:
if StringName(facility.get(&"facility_id", &"")) == facility_id:
var suffix := String(facility_id).trim_prefix("facility_").lstrip("0")
var location := ""
var origin: Variant = facility.get(&"origin", null)
if origin is Vector3i:
var floor_name := "%dF" % (origin.y + 1) if origin.y >= 0 else "B%d" % -origin.y
location = " %s・%s" % [floor_name, "客用" if int(facility.get(&"layer", 0)) == 0 else "裏方"]
return "%s%s #%s" % [facility.get(&"display_name", facility_id), location, suffix if not suffix.is_empty() else facility_id]
return "撤去済みの施設"
func _role_name(row: Dictionary) -> String:
var role := StringName(row.get(&"role_id", &""))
return String(ROLE_NAMES.get(role, String(role)))
func _state_name(row: Dictionary) -> String:
var value := int(row.get(&"state", 0))
return STATE_NAMES[value] if value >= 0 and value < STATE_NAMES.size() else "状態を確認中"
func _minute_text(absolute_minute: int) -> String:
var day := absolute_minute / 1440 + 1
var hour := posmod(absolute_minute, 1440) / 60
var minute := posmod(absolute_minute, 60)
return "%d日目 %02d:%02d" % [day, hour, minute]
func _label(text_value: String) -> Label:
var label := Label.new()
label.text = text_value
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.add_theme_font_size_override("font_size", 12)
label.add_theme_color_override("font_color", Color("394937"))
return label
func _style(button: Button) -> void:
button.add_theme_font_size_override("font_size", 12)
if _style_button.is_valid():
_style_button.call(button)