package market.guess.ui.desktop.controllers; import java.math.BigDecimal; import java.util.ArrayList; import java.util.function.Consumer; import javafx.collections.ListChangeListener; import javafx.css.PseudoClass; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.control.TextField; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import market.guess.model.event.EventStatus; import market.guess.model.event.MechanismType; import market.guess.ui.desktop.AppState; import market.guess.ui.desktop.components.TradeSide; import market.guess.ui.desktop.components.UserEventItemView; import market.guess.ui.desktop.components.UserListCell; import market.guess.ui.desktop.components.graphic.Graphic; import market.guess.ui.desktop.components.graphic.SparklineGraphic; import market.guess.ui.desktop.model.UserData; import market.guess.ui.desktop.model.UserEventRow; import market.guess.ui.desktop.util.Charts; import market.guess.ui.desktop.util.Format; import market.guess.ui.desktop.util.Views; public class UsersTabController { private static final PseudoClass BLOCKED = PseudoClass.getPseudoClass("blocked"); private static final PseudoClass MM = PseudoClass.getPseudoClass("mm"); private static final PseudoClass TRADER = PseudoClass.getPseudoClass("trader"); private static final PseudoClass NEGATIVE = PseudoClass.getPseudoClass("negative"); private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error"); @FunctionalInterface public interface OrderPlacer { void placeOrder( String eventKey, String optionKey, String side, BigDecimal price, long quantity); } private AppState state; private Runnable onCreateNewEvent; private Consumer onToast; private OrderPlacer onPlaceOrder; private Graphic sparkline; private boolean updatingSelection = false; @FXML private StackPane sparklineBox; @FXML private ListView userListContainer; @FXML private VBox userDetailCard; @FXML private Label userNameLabel; @FXML private Label userRolePill; @FXML private Label userSubLabel; @FXML private Label userBalanceLabel; @FXML private HBox blockedBanner; @FXML private LineChart balanceChart; @FXML private VBox participationRowsContainer; @FXML private VBox tradePanel; @FXML private Label noTradeEventLabel; @FXML private VBox tradeContentBox; @FXML private Label tradeEventTitle; @FXML private Label tradeEventSub; @FXML private VBox tradeYesOptCard; @FXML private Label tradeYesPriceLabel; @FXML private Label tradeYesHeldLabel; @FXML private VBox tradeNoOptCard; @FXML private Label tradeNoPriceLabel; @FXML private Label tradeNoHeldLabel; @FXML private Button tradeBuyBtn; @FXML private Button tradeSellBtn; @FXML private TextField tradeQtyField; @FXML private TextField tradePriceField; @FXML private Label tradeCostLabel; @FXML private Button tradeSubmitBtn; @FXML private Label tradeHintLabel; public void init(AppState state) { this.state = state; sparkline = SparklineGraphic.create(); sparklineBox.getChildren().setAll(sparkline.node); userListContainer.setCellFactory(lv -> new UserListCell()); userListContainer .getSelectionModel() .selectedItemProperty() .addListener( (obs, oldVal, newVal) -> { if (updatingSelection) return; if (newVal != null && state != null) { state.setActingUser(newVal); } }); state .actingUserProperty() .addListener( (obs, oldVal, newVal) -> { if (newVal != null) { if (!newVal.equals(userListContainer.getSelectionModel().getSelectedItem())) { updatingSelection = true; try { userListContainer.getSelectionModel().select(newVal); } finally { updatingSelection = false; } } } refreshUserDetail(newVal); }); state .selectedEventProperty() .addListener( (obs, oldVal, newVal) -> { refreshTradePanel(state.actingUser()); }); state .tradeOptionYesProperty() .addListener((obs, oldVal, newVal) -> refreshTradePanel(state.actingUser())); state .tradeSideProperty() .addListener((obs, oldVal, newVal) -> refreshTradePanel(state.actingUser())); state .getUsers() .addListener( (ListChangeListener) c -> { refreshUserList(); refreshUserDetail(state.actingUser()); }); state.animationsOnProperty().addListener((obs, oldVal, newVal) -> sparkline.setPlaying(newVal)); tradeQtyField .textProperty() .addListener( (obs, o, n) -> { if (state != null) state.setTradeQty(n); validateTradeInputs(); }); tradePriceField .textProperty() .addListener( (obs, o, n) -> { if (state != null) state.setTradePrice(n); validateTradeInputs(); }); } public void setOnCreateNewEvent(Runnable onCreateNewEvent) { this.onCreateNewEvent = onCreateNewEvent; } public void setOnToast(Consumer onToast) { this.onToast = onToast; } public void setOnPlaceOrder(OrderPlacer onPlaceOrder) { this.onPlaceOrder = onPlaceOrder; } public void refresh() { if (state == null) return; refreshUserList(); refreshUserDetail(state.actingUser()); } private void refreshUserList() { if (state == null) return; updatingSelection = true; try { userListContainer.getItems().setAll(state.getUsers()); var actor = state.actingUser(); UserData match = null; if (actor != null) { for (var u : state.getUsers()) { if (u.name.equals(actor.name)) { match = u; break; } } } if (match != null) { userListContainer.getSelectionModel().select(match); } else if (!state.getUsers().isEmpty()) { userListContainer.getSelectionModel().select(0); } } finally { updatingSelection = false; } } private void refreshUserDetail(UserData actor) { if (actor == null) { userDetailCard.setVisible(false); return; } userDetailCard.setVisible(true); userNameLabel.setText(actor.name); userRolePill.setText(actor.blocked ? "BLOCKED" : actor.isMm ? "MARKET MAKER" : "TRADER"); userRolePill.pseudoClassStateChanged(BLOCKED, actor.blocked); userRolePill.pseudoClassStateChanged(MM, !actor.blocked && actor.isMm); userRolePill.pseudoClassStateChanged(TRADER, !actor.blocked && !actor.isMm); userSubLabel.setText("Active in " + actor.events.size() + " events"); userBalanceLabel.setText(Format.money(actor.balance)); userBalanceLabel.pseudoClassStateChanged(NEGATIVE, actor.balance.signum() < 0); sparkline.node.pseudoClassStateChanged(NEGATIVE, actor.balance.signum() < 0); sparkline.setPlaying(state.isAnimationsOn()); // Blocked Banner blockedBanner.setVisible(actor.blocked); blockedBanner.setManaged(actor.blocked); // Balance Chart Charts.setSingleSeries(balanceChart, "balance", actor.balanceHistory); if (balanceChart.getYAxis() instanceof NumberAxis yAxis) { Charts.scaleYAxis(yAxis, actor.balanceHistory, actor.balance.doubleValue()); } // Participation Table var evRows = new ArrayList(actor.events.size()); for (var ev : actor.events) { evRows.add(buildParticipationRow(ev)); } participationRowsContainer.getChildren().setAll(evRows); // Trade Panel refreshTradePanel(actor); } private void refreshTradePanel(UserData actor) { if (state == null) return; var e = state.selectedEvent(); if (e == null) { noTradeEventLabel.setVisible(true); noTradeEventLabel.setManaged(true); tradeContentBox.setVisible(false); tradeContentBox.setManaged(false); return; } noTradeEventLabel.setVisible(false); noTradeEventLabel.setManaged(false); tradeContentBox.setVisible(true); tradeContentBox.setManaged(true); tradeEventTitle.setText(e.num + ". " + e.name); tradeEventSub.setText( (e.type == MechanismType.LMSR ? "LMSR" : "Order Book") + "  Status: " + e.status + "  Fee: " + e.feeText()); int yesHeld = 0, noHeld = 0; if (actor != null && actor.events != null) { for (var ev : actor.events) { if (ev.eventId().equals(e.id)) { yesHeld = ev.yes(); noHeld = ev.no(); break; } } } tradeYesPriceLabel.setText(Format.money(e.yesPrice)); tradeYesHeldLabel.setText("You hold " + yesHeld + " shares"); tradeNoPriceLabel.setText(Format.money(e.noPrice)); tradeNoHeldLabel.setText("You hold " + noHeld + " shares"); Views.setSelected(tradeYesOptCard, state.isTradeOptionYes()); Views.setSelected(tradeNoOptCard, !state.isTradeOptionYes()); Views.setSelected(tradeBuyBtn, state.getTradeSide() == TradeSide.BUY); Views.setSelected(tradeSellBtn, state.getTradeSide() == TradeSide.SELL); if (tradeQtyField.getText().isEmpty()) tradeQtyField.setText(state.getTradeQty()); if (tradePriceField.getText().isEmpty()) tradePriceField.setText(state.getTradePrice()); tradePriceField.setVisible(e.type == MechanismType.ORDER_BOOK); tradePriceField.setManaged(e.type == MechanismType.ORDER_BOOK); tradeSubmitBtn.setText( state.getTradeSide() == TradeSide.BUY ? "Place Buy Order" : "Place Sell Order"); validateTradeInputs(); } private boolean validateTradeInputs() { if (state == null) return false; var e = state.selectedEvent(); var actor = state.actingUser(); if (e == null || actor == null) return false; boolean valid = true; String qtyText = tradeQtyField.getText() != null ? tradeQtyField.getText().trim() : ""; long qty = 0; if (qtyText.isEmpty()) { valid = false; tradeQtyField.pseudoClassStateChanged(ERROR, true); tradeHintLabel.setText("Please enter the number of shares."); } else { try { qty = Long.parseLong(qtyText); if (qty <= 0) { valid = false; tradeQtyField.pseudoClassStateChanged(ERROR, true); tradeHintLabel.setText("Shares must be a positive number."); } else { tradeQtyField.pseudoClassStateChanged(ERROR, false); } } catch (NumberFormatException nfe) { valid = false; tradeQtyField.pseudoClassStateChanged(ERROR, true); tradeHintLabel.setText("Shares must be a valid integer."); } } BigDecimal price = null; if (e.type == MechanismType.ORDER_BOOK) { String priceText = tradePriceField.getText() != null ? tradePriceField.getText().trim() : ""; if (priceText.isEmpty()) { valid = false; tradePriceField.pseudoClassStateChanged(ERROR, true); if (valid) { tradeHintLabel.setText("Please enter a price per share."); } } else { try { price = new BigDecimal(priceText); if (price.compareTo(BigDecimal.ZERO) <= 0) { valid = false; tradePriceField.pseudoClassStateChanged(ERROR, true); tradeHintLabel.setText("Price per share must be positive."); } else { tradePriceField.pseudoClassStateChanged(ERROR, false); } } catch (Exception nfe) { valid = false; tradePriceField.pseudoClassStateChanged(ERROR, true); tradeHintLabel.setText("Price must be a valid decimal number."); } } } else { tradePriceField.pseudoClassStateChanged(ERROR, false); price = state.isTradeOptionYes() ? e.yesPrice : e.noPrice; } if (valid && qty > 0 && price != null) { BigDecimal cost = price.multiply(BigDecimal.valueOf(qty)); tradeCostLabel.setText(Format.money(cost)); if (actor.blocked) { tradeHintLabel.setText("Trading is disabled because this account is blocked."); tradeSubmitBtn.setDisable(true); } else if (e.status != EventStatus.ACTIVE) { tradeHintLabel.setText("Trading is only available for ACTIVE events."); tradeSubmitBtn.setDisable(true); } else if (state.getTradeSide() == TradeSide.SELL) { int held = 0; if (actor.events != null) { for (var evRow : actor.events) { if (evRow.eventId().equals(e.id)) { held = state.isTradeOptionYes() ? evRow.yes() : evRow.no(); break; } } } if (!actor.isMm && qty > held) { tradeHintLabel.setText("Cannot sell " + qty + " shares; you only hold " + held + "."); tradeSubmitBtn.setDisable(true); } else { tradeHintLabel.setText("Sell order will match existing bids or rest as an ask."); tradeSubmitBtn.setDisable(false); } } else { if (actor.balance.compareTo(cost) < 0) { tradeHintLabel.setText( "Insufficient balance. Required: " + Format.money(cost) + ", available: " + Format.money(actor.balance)); tradeSubmitBtn.setDisable(true); } else { tradeHintLabel.setText( e.type == MechanismType.ORDER_BOOK ? "Buy order will match existing asks or rest as a bid." : "Orders execute immediately against the market maker."); tradeSubmitBtn.setDisable(false); } } } else { tradeCostLabel.setText("—"); tradeSubmitBtn.setDisable(true); } return valid; } private Node buildParticipationRow(UserEventRow ev) { var row = new UserEventItemView(ev); row.setOnMouseClicked( e -> { if (state != null) { state.setSelectedEventId(ev.eventId()); } }); return row; } // ---- FXML Handlers ---------------------------------------------------- @FXML private void handleCreateNewEvent() { if (onCreateNewEvent != null) { onCreateNewEvent.run(); } } @FXML private void handleSelectTradeYes() { if (state != null) state.setTradeOptionYes(true); } @FXML private void handleSelectTradeNo() { if (state != null) state.setTradeOptionYes(false); } @FXML private void handleTradeBuy() { if (state != null) state.setTradeSide(TradeSide.BUY); } @FXML private void handleTradeSell() { if (state != null) state.setTradeSide(TradeSide.SELL); } @FXML private void handleTradeSubmit() { if (state == null) return; var e = state.selectedEvent(); var u = state.actingUser(); if (e == null || u == null) return; if (!validateTradeInputs()) return; long qty = Long.parseLong(tradeQtyField.getText().trim()); BigDecimal price = (e.type == MechanismType.ORDER_BOOK) ? new BigDecimal(tradePriceField.getText().trim()) : (state.isTradeOptionYes() ? e.yesPrice : e.noPrice); String optionKey = state.isTradeOptionYes() ? "YES" : "NO"; String side = state.getTradeSide() == TradeSide.SELL ? "SELL" : "BUY"; if (onPlaceOrder != null) { onPlaceOrder.placeOrder(e.id, optionKey, side, price, qty); } else if (onToast != null) { onToast.accept("Order placed: " + side + " " + qty + " " + optionKey + " on #" + e.num); } } }