extends Control class_name WalletUI ## Wallet & Transaction History UI panel. ## ## Displays: ## - Current Credit balance ## - Credit pack quick purchases (100, 500, 1000, 5000 CR) ## - Paged ledger transaction history with status/amounts/references ## ## docs/planning/economy.md US-6 (task 34c725431645). signal back_requested @onready var back_button: Button = $VBox/HeaderHBox/BackButton @onready var refresh_button: Button = $VBox/HeaderHBox/RefreshButton @onready var status_label: Label = $VBox/StatusLabel @onready var balance_label: Label = $VBox/BalanceCard/Margin/HBox/BalanceLabel @onready var buy_100_btn: Button = $VBox/BalanceCard/Margin/HBox/PacksHBox/Buy100Btn @onready var buy_500_btn: Button = $VBox/BalanceCard/Margin/HBox/PacksHBox/Buy500Btn @onready var buy_1000_btn: Button = $VBox/BalanceCard/Margin/HBox/PacksHBox/Buy1000Btn @onready var buy_5000_btn: Button = $VBox/BalanceCard/Margin/HBox/PacksHBox/Buy5000Btn @onready var tx_container: VBoxContainer = $VBox/LedgerSection/Scroll/TxList @onready var prev_page_btn: Button = $VBox/LedgerSection/PagingHBox/PrevPageBtn @onready var next_page_btn: Button = $VBox/LedgerSection/PagingHBox/NextPageBtn @onready var page_label: Label = $VBox/LedgerSection/PagingHBox/PageLabel var _api_client: Node = null var current_page: int = 0 const PAGE_SIZE: int = 20 var total_tx_count: int = 0 func _ready() -> void: _api_client = _get_api() _wire_signals() if _api_client != null and _api_client.has_token(): refresh_all() func _get_api() -> Node: if has_node("/root/ApiClient"): return get_node("/root/ApiClient") return null func _wire_signals() -> void: if back_button and not back_button.pressed.is_connected(_on_back_pressed): back_button.pressed.connect(_on_back_pressed) if refresh_button and not refresh_button.pressed.is_connected(refresh_all): refresh_button.pressed.connect(refresh_all) if buy_100_btn and not buy_100_btn.pressed.is_connected(_buy_pack.bind(100)): buy_100_btn.pressed.connect(_buy_pack.bind(100)) if buy_500_btn and not buy_500_btn.pressed.is_connected(_buy_pack.bind(500)): buy_500_btn.pressed.connect(_buy_pack.bind(500)) if buy_1000_btn and not buy_1000_btn.pressed.is_connected(_buy_pack.bind(1000)): buy_1000_btn.pressed.connect(_buy_pack.bind(1000)) if buy_5000_btn and not buy_5000_btn.pressed.is_connected(_buy_pack.bind(5000)): buy_5000_btn.pressed.connect(_buy_pack.bind(5000)) if prev_page_btn and not prev_page_btn.pressed.is_connected(_on_prev_page): prev_page_btn.pressed.connect(_on_prev_page) if next_page_btn and not next_page_btn.pressed.is_connected(_on_next_page): next_page_btn.pressed.connect(_on_next_page) func _on_back_pressed() -> void: back_requested.emit() if get_tree() and get_tree().current_scene == self: get_tree().change_scene_to_file("res://scenes/main_menu.tscn") func show_status(text: String, is_error: bool = false) -> void: if not status_label: return status_label.text = text if is_error: status_label.add_theme_color_override("font_color", Color(1.0, 0.3, 0.3)) else: status_label.add_theme_color_override("font_color", Color(0.3, 0.9, 0.3)) func refresh_all() -> void: refresh_wallet() refresh_transactions() func refresh_wallet() -> void: var api = _get_api() if not api or not api.has_token(): return var res: Dictionary = await EconomyApiClient.get_wallet(api) if res.ok and res.data is Dictionary: var balance: int = int(res.data.get("balance", 0)) if balance_label: balance_label.text = "%d Buffalo Chips" % balance else: show_status("Failed to fetch wallet: " + res.detail, true) func refresh_transactions() -> void: var api = _get_api() if not api or not api.has_token(): return var offset: int = current_page * PAGE_SIZE var res: Dictionary = await EconomyApiClient.get_wallet_transactions(api, PAGE_SIZE, offset) if res.ok and res.data is Dictionary: var items: Array = res.data.get("items", []) total_tx_count = int(res.data.get("total", 0)) render_transactions(items) _update_paging_controls() else: show_status("Failed to fetch transactions: " + res.detail, true) func _buy_pack(credits: int) -> void: var api = _get_api() if not api or not api.has_token(): show_status("Please login to purchase Buffalo Chips", true) return show_status("Processing purchase of %d Buffalo Chips..." % credits) var res: Dictionary = await EconomyApiClient.purchase_credits(api, credits) if res.ok and res.data is Dictionary: var new_bal: int = int(res.data.get("new_balance", 0)) if balance_label: balance_label.text = "%d Buffalo Chips" % new_bal show_status("Successfully purchased %d Buffalo Chips! Balance: %d Buffalo Chips" % [credits, new_bal]) refresh_transactions() else: show_status("Purchase failed: " + res.detail, true) func render_transactions(tx_list: Array) -> void: if not tx_container: return for c in tx_container.get_children(): tx_container.remove_child(c) c.queue_free() if tx_list.is_empty(): var empty_lbl := Label.new() empty_lbl.text = "No transactions found." empty_lbl.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6)) tx_container.add_child(empty_lbl) return for tx in tx_list: if not tx is Dictionary: continue var row := PanelContainer.new() var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 12) row.add_child(hbox) var date_str: String = str(tx.get("created_at", "")) if date_str.length() > 19: date_str = date_str.substr(0, 19).replace("T", " ") var date_lbl := Label.new() date_lbl.text = date_str date_lbl.custom_minimum_size = Vector2(140, 0) date_lbl.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7)) hbox.add_child(date_lbl) var type_lbl := Label.new() type_lbl.text = str(tx.get("type", "")).replace("_", " ").capitalize() type_lbl.custom_minimum_size = Vector2(160, 0) hbox.add_child(type_lbl) var amount: int = int(tx.get("amount", 0)) var amt_lbl := Label.new() if amount > 0: amt_lbl.text = "+%d Buffalo Chips" % amount amt_lbl.add_theme_color_override("font_color", Color(0.2, 0.9, 0.2)) else: amt_lbl.text = "%d Buffalo Chips" % amount amt_lbl.add_theme_color_override("font_color", Color(0.9, 0.3, 0.3)) amt_lbl.custom_minimum_size = Vector2(100, 0) hbox.add_child(amt_lbl) var bal_lbl := Label.new() bal_lbl.text = "Bal: %s Buffalo Chips" % str(tx.get("balance_after", 0)) bal_lbl.custom_minimum_size = Vector2(100, 0) bal_lbl.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8)) hbox.add_child(bal_lbl) var ref_lbl := Label.new() var ref_type: String = str(tx.get("reference_type", "")) var ref_id: String = str(tx.get("reference_id", "")) if ref_type != "": ref_lbl.text = "%s #%s" % [ref_type, ref_id] else: ref_lbl.text = "-" ref_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL ref_lbl.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5)) hbox.add_child(ref_lbl) tx_container.add_child(row) func _update_paging_controls() -> void: var total_pages: int = int(ceil(float(total_tx_count) / float(PAGE_SIZE))) if total_pages < 1: total_pages = 1 if page_label: page_label.text = "Page %d of %d" % [current_page + 1, total_pages] if prev_page_btn: prev_page_btn.disabled = current_page <= 0 if next_page_btn: next_page_btn.disabled = current_page >= total_pages - 1 func _on_prev_page() -> void: if current_page > 0: current_page -= 1 refresh_transactions() func _on_next_page() -> void: var total_pages: int = int(ceil(float(total_tx_count) / float(PAGE_SIZE))) if current_page < total_pages - 1: current_page += 1 refresh_transactions()