From e75204ae20e629e7738f275e82390614984ab3ab Mon Sep 17 00:00:00 2001 From: pawelsa Date: Tue, 12 May 2026 21:07:40 +0200 Subject: [PATCH 01/11] feat(UICompositor): Change wrapping from ArrayList to Deque. --- .../view/components/ui/UICompositor.java | 37 ++++++++++--------- .../idatt/view/components/ui/UIFactory.java | 2 +- .../idatt/view/primary/stock/StockView.java | 13 ++++++- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UICompositor.java b/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UICompositor.java index 4d598d5..410a9e1 100644 --- a/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UICompositor.java +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UICompositor.java @@ -1,6 +1,7 @@ package edu.ntnu.idi.idatt.view.components.ui; -import java.util.ArrayList; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.List; import java.util.function.Consumer; @@ -26,8 +27,7 @@ public Parent makeUI() { public static class Builder { private Pane parent; - private ArrayList wrappers = new ArrayList<>(); - private boolean isWrapper = false; + private Deque wrappers = new ArrayDeque<>(); public Builder parent(Pane parent) { this.parent = parent; @@ -48,42 +48,45 @@ public Builder growWithAlignment(Pos position) { return this; } + private Pane getRoot() { + return wrappers.isEmpty() ? parent : wrappers.peek(); + } + public Builder addContent(Parent child) { - Pane root = isWrapper ? wrappers.getLast() : parent; + Pane root = getRoot(); root.getChildren().add(child); return this; } public Builder addAllContent(Parent... children) { - Pane root = isWrapper ? wrappers.getLast() : parent; + Pane root = getRoot(); root.getChildren().addAll(List.of(children)); return this; } - public Builder parentProperties(Consumer handle) { - handle.accept(this.parent); - return this; - } - public Builder wrap(Pane wrapper) { - isWrapper = true; - wrappers.add(wrapper); + wrappers.push(wrapper); return this; } public Builder unwrap() { - isWrapper = false; - parent.getChildren().add(wrappers.getLast()); + Pane current = wrappers.pop(); + + if (wrappers.isEmpty()) { + parent.getChildren().add(current); + } else { + wrappers.peek().getChildren().add(current); + } return this; } - public Builder wrapperProperties(Consumer handle) { - handle.accept(wrappers.getLast()); + public Builder properties(Consumer handle) { + handle.accept(getRoot()); return this; } public Builder filler() { - Pane root = isWrapper ? wrappers.getLast() : parent; + Pane root = getRoot(); Region filler = new Region(); if (root instanceof HBox) { diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIFactory.java b/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIFactory.java index 7d70fdd..3d58b7a 100644 --- a/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIFactory.java +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIFactory.java @@ -97,7 +97,7 @@ public static Parent createMenu(String title, List buttonLables, ActionE UICompositor.Builder menuBuilder = new UICompositor.Builder() .parent(new VBox()) .growWithAlignment(Pos.TOP_CENTER) - .parentProperties((menu) -> menu.setPrefSize(300, Double.MAX_VALUE)) + .properties((menu) -> menu.setPrefSize(300, Double.MAX_VALUE)) .addContent(menuIcon); buttons.forEach(b -> menuBuilder.addContent(b)); diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java index 106af66..f4600ca 100644 --- a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java +++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java @@ -20,6 +20,7 @@ import javafx.scene.Parent; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.TextField; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; @@ -55,7 +56,7 @@ public Parent createContent() { UICompositor userSection = new UICompositor.Builder() .parent(new HBox()) .growWithAlignment(Pos.TOP_CENTER) - .parentProperties((parent) -> parent.setPadding(new Insets(20.0))) + .properties((parent) -> parent.setPadding(new Insets(20.0))) .wrap(new VBox()) .addContent(stockPrice = new TextValueComponent("Current price: ")) .addContent(latestChange = new TextValueComponent("Latest Price Change: ")) @@ -64,10 +65,18 @@ public Parent createContent() { .addContent(totalProfits = new TextValueComponent("Total profits: ")) .unwrap() .filler() + .wrap(new VBox()) .wrap(new HBox()) .addContent(buyButton = new Button("Buy")) .addContent(portfolioButton = new Button("Sell in portfolio...")) - .wrapperProperties((wrapper) -> ((HBox) wrapper).setSpacing(20.0)) + .properties((wrapper) -> ((HBox) wrapper).setSpacing(20.0)) + .unwrap() + .wrap(new VBox()) + .addContent(new TextField()) + .addContent(new Label("Price: ")) + .addContent(new Label("Where taxes and commision: ")) + .addContent(new Label("Total: ")) + .unwrap() .unwrap() .build(); From 23debf0b50996a48218566a5e70fd5a091c1ece8 Mon Sep 17 00:00:00 2001 From: pawelsa Date: Wed, 13 May 2026 00:23:18 +0200 Subject: [PATCH 02/11] feat: Gson Adapters for abstract classes. --- .../idi/idatt/storage/SessionManager.java | 6 ++ .../storage/util/TransactionAdapter.java | 56 ++++++++++++++++++ .../util/TransactionCalculatorAdapter.java | 57 +++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionAdapter.java create mode 100644 src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionCalculatorAdapter.java diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java b/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java index d2f1e55..8b40489 100644 --- a/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java +++ b/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java @@ -18,8 +18,12 @@ import edu.ntnu.idi.idatt.model.Exchange; import edu.ntnu.idi.idatt.model.player.Player; +import edu.ntnu.idi.idatt.model.transaction.Transaction; +import edu.ntnu.idi.idatt.service.transaction.TransactionCalculator; import edu.ntnu.idi.idatt.session.UserSession; import edu.ntnu.idi.idatt.session.UserSession.SessionBundle; +import edu.ntnu.idi.idatt.storage.util.TransactionAdapter; +import edu.ntnu.idi.idatt.storage.util.TransactionCalculatorAdapter; /** * Class for managing user sessions. @@ -36,6 +40,8 @@ public class SessionManager { private static Gson gson = new GsonBuilder() .setPrettyPrinting() + .registerTypeAdapter(Transaction.class, new TransactionAdapter()) + .registerTypeAdapter(TransactionCalculator.class, new TransactionCalculatorAdapter()) .create(); // Static initiator to ensure persistent storage file. diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionAdapter.java b/src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionAdapter.java new file mode 100644 index 0000000..acc53be --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionAdapter.java @@ -0,0 +1,56 @@ +package edu.ntnu.idi.idatt.storage.util; + +import java.lang.reflect.Type; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import edu.ntnu.idi.idatt.model.transaction.Purchase; +import edu.ntnu.idi.idatt.model.transaction.Sale; +import edu.ntnu.idi.idatt.model.transaction.Transaction; + +public class TransactionAdapter implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(Transaction t, Type typeOfT, JsonSerializationContext context) { + + JsonObject obj = context.serialize(t).getAsJsonObject(); + + if (t instanceof Purchase) { + obj.addProperty("type", "purchase"); + } else if (t instanceof Sale) { + obj.addProperty("type", "sale"); + } + + return obj; + } + + @Override + public Transaction deserialize(JsonElement json, + Type typeOfT, + JsonDeserializationContext context) + throws JsonParseException { + + JsonObject obj = json.getAsJsonObject(); + + String type = obj.get("type").getAsString(); + + switch (type) { + case "purchase": + return context.deserialize(obj, Purchase.class); + + case "sale": + return context.deserialize(obj, Sale.class); + + default: + throw new UnsupportedOperationException("Unknown type " + type); + } + + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionCalculatorAdapter.java b/src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionCalculatorAdapter.java new file mode 100644 index 0000000..89f3204 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionCalculatorAdapter.java @@ -0,0 +1,57 @@ +package edu.ntnu.idi.idatt.storage.util; + +import java.lang.reflect.Type; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import edu.ntnu.idi.idatt.service.transaction.PurchaseCalculator; +import edu.ntnu.idi.idatt.service.transaction.SaleCalculator; +import edu.ntnu.idi.idatt.service.transaction.TransactionCalculator; + +public class TransactionCalculatorAdapter + implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(TransactionCalculator calc, Type typeOfT, JsonSerializationContext context) { + + JsonObject obj = context.serialize(calc).getAsJsonObject(); + + if (calc instanceof PurchaseCalculator) { + obj.addProperty("type", "purchase"); + } else if (calc instanceof SaleCalculator) { + obj.addProperty("type", "sale"); + } + + return obj; + } + + @Override + public TransactionCalculator deserialize(JsonElement json, + Type typeOfT, + JsonDeserializationContext context) + throws JsonParseException { + + JsonObject obj = json.getAsJsonObject(); + + String type = obj.get("type").getAsString(); + + switch (type) { + case "purchase": + return context.deserialize(obj, PurchaseCalculator.class); + + case "sale": + return context.deserialize(obj, SaleCalculator.class); + + default: + throw new UnsupportedOperationException("Unknown type " + type); + } + + } + +} From 6ce646b9228f85ad199585291dd6d81377688357 Mon Sep 17 00:00:00 2001 From: pawelsa Date: Wed, 13 May 2026 01:23:59 +0200 Subject: [PATCH 03/11] fix: Value calculations in Portfolio,Share --- .../edu/ntnu/idi/idatt/model/portfolio/Portfolio.java | 8 ++++++-- .../java/edu/ntnu/idi/idatt/model/portfolio/Share.java | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Portfolio.java index 070653d..5bfba56 100644 --- a/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Portfolio.java +++ b/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Portfolio.java @@ -1,6 +1,7 @@ package edu.ntnu.idi.idatt.model.portfolio; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; @@ -72,12 +73,15 @@ public BigDecimal getProfitFromStock(String symbol) { .reduce(BigDecimal.ZERO, BigDecimal::add); } - public double getChangeFromStock(String symbol) { + public BigDecimal getChangeFromStock(String symbol) { BigDecimal profitTotal = getProfitFromStock(symbol); BigDecimal costTotal = getShares(symbol).stream().map(s -> s.getPurchasePrice()) .reduce(BigDecimal.ZERO, BigDecimal::add); - return (profitTotal.subtract(costTotal).doubleValue()) / 100; + if (costTotal.compareTo(BigDecimal.ZERO) <= 0) + return BigDecimal.ZERO; + + return profitTotal.divide(costTotal, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)); } /** diff --git a/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Share.java b/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Share.java index eeaa73f..a00a65f 100644 --- a/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Share.java +++ b/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Share.java @@ -53,7 +53,8 @@ public BigDecimal getPurchasePrice() { // TODO: JAVADOCS, JUNIT public BigDecimal getProfit() { - return new SaleCalculator(this).calculateGross().subtract(purchasePrice); + BigDecimal totalCost = purchasePrice.multiply(quantity); + return new SaleCalculator(this).calculateGross().subtract(totalCost); } } From d19a7c4a113bf60ba1095ea393723fcf74d6ecd9 Mon Sep 17 00:00:00 2001 From: pawelsa Date: Wed, 13 May 2026 01:24:44 +0200 Subject: [PATCH 04/11] feat: Implement observable property for player's money --- .../java/edu/ntnu/idi/idatt/session/UserSession.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/edu/ntnu/idi/idatt/session/UserSession.java b/src/main/java/edu/ntnu/idi/idatt/session/UserSession.java index 841a22a..b9dbfba 100644 --- a/src/main/java/edu/ntnu/idi/idatt/session/UserSession.java +++ b/src/main/java/edu/ntnu/idi/idatt/session/UserSession.java @@ -1,7 +1,11 @@ package edu.ntnu.idi.idatt.session; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + import edu.ntnu.idi.idatt.model.Exchange; import edu.ntnu.idi.idatt.model.player.Player; +import javafx.beans.property.SimpleDoubleProperty; public class UserSession { @@ -21,6 +25,7 @@ public static UserSession getInstance() { private Player player; private Exchange exchange; + private final SimpleDoubleProperty moneyProperty = new SimpleDoubleProperty(); public Player getPlayer() { return player; @@ -28,6 +33,7 @@ public Player getPlayer() { public void setPlayer(Player player) { this.player = player; + moneyProperty.set(player.getMoney().doubleValue()); } public Exchange getExchange() { @@ -42,6 +48,10 @@ public SessionBundle getSession() { return new SessionBundle(player, exchange); } + public SimpleDoubleProperty moneyProperty() { + return moneyProperty; + } + public class SessionBundle { private Player player; From b2eab1fa0bc19f46a8504a969aeb655328ec4f13 Mon Sep 17 00:00:00 2001 From: pawelsa Date: Wed, 13 May 2026 01:25:04 +0200 Subject: [PATCH 05/11] feat: Hook player functions for balance changes. --- src/main/java/edu/ntnu/idi/idatt/model/player/Player.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/edu/ntnu/idi/idatt/model/player/Player.java b/src/main/java/edu/ntnu/idi/idatt/model/player/Player.java index f75b184..5648efb 100644 --- a/src/main/java/edu/ntnu/idi/idatt/model/player/Player.java +++ b/src/main/java/edu/ntnu/idi/idatt/model/player/Player.java @@ -2,6 +2,7 @@ import edu.ntnu.idi.idatt.model.portfolio.Portfolio; import edu.ntnu.idi.idatt.model.transaction.TransactionArchive; +import edu.ntnu.idi.idatt.session.UserSession; import java.math.BigDecimal; @@ -49,10 +50,13 @@ public TransactionArchive getTransactionArchive() { public void addMoney(BigDecimal amount) { this.money = this.money.add(amount); + UserSession.getInstance().moneyProperty().set(this.money.doubleValue()); // Workaround instead of PropertyChange due + // to JSON saving. } public void withdrawMoney(BigDecimal amount) { this.money = this.money.subtract(amount); + UserSession.getInstance().moneyProperty().set(this.money.doubleValue()); // Hook } /** From e72211e0ead6ad67d639e7a610809c75414e55a0 Mon Sep 17 00:00:00 2001 From: pawelsa Date: Wed, 13 May 2026 01:25:24 +0200 Subject: [PATCH 06/11] feat: Connect UI to money property. --- .../edu/ntnu/idi/idatt/view/components/ui/UIFactory.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIFactory.java b/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIFactory.java index 3d58b7a..00ec8b9 100644 --- a/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIFactory.java +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIFactory.java @@ -10,6 +10,7 @@ import edu.ntnu.idi.idatt.view.components.primitives.ActionEventHandler; import edu.ntnu.idi.idatt.view.util.CssUtils; import edu.ntnu.idi.idatt.view.util.ResourceUtils; +import javafx.beans.binding.Bindings; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Parent; @@ -109,7 +110,9 @@ public static Parent createMenu(String title, List buttonLables, ActionE } public static Parent createToolbar(ActionEventHandler menuHandle, ActionEventHandler quitHandle) { - Label balance = new Label(" Money: " + UserSession.getInstance().getPlayer().getMoney().toString() + " USD"); + Label balance = new Label(); + balance.textProperty().bind( + Bindings.concat("Balance: ", UserSession.getInstance().moneyProperty(), "USD")); Label playerStatus = new Label("Status:" + UserSession.getInstance().getPlayer().getStatus()); Label playerNetWorth = new Label( "Net Worth: " + UserSession.getInstance().getPlayer().getNetWorth().toString() + " USD"); From 1183c001da2dcb3b41c1e33617f66c0279227e56 Mon Sep 17 00:00:00 2001 From: pawelsa Date: Wed, 13 May 2026 01:25:57 +0200 Subject: [PATCH 07/11] feat: Implement finished version of StockView and functionality --- .../elements/TextValueComponent.java | 3 +- .../view/primary/stock/StockController.java | 88 ++++++++++++++++++- .../idatt/view/primary/stock/StockModel.java | 35 ++++++++ .../idatt/view/primary/stock/StockView.java | 39 ++++++-- .../ntnu/idi/idatt/view/util/CssUtils.java | 5 ++ 5 files changed, 161 insertions(+), 9 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/TextValueComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/TextValueComponent.java index dbc4c19..21b8617 100644 --- a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/TextValueComponent.java +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/TextValueComponent.java @@ -18,7 +18,8 @@ public TextValueComponent(String prefix) { CssUtils.apply(valueLabel, CssUtils.MED_TEXT_16); color.addListener((obs, oldVal, newVal) -> { - CssUtils.apply(valueLabel, newVal); + CssUtils.set(valueLabel, newVal); + CssUtils.apply(valueLabel, CssUtils.MED_TEXT_16); }); this.getChildren().addAll(prefixLabel, valueLabel); diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockController.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockController.java index 25974e9..b5d673a 100644 --- a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockController.java +++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockController.java @@ -1,7 +1,13 @@ package edu.ntnu.idi.idatt.view.primary.stock; +import java.math.BigDecimal; +import java.text.NumberFormat; + import edu.ntnu.idi.idatt.model.market.Stock; +import edu.ntnu.idi.idatt.model.portfolio.Share; +import edu.ntnu.idi.idatt.service.transaction.PurchaseCalculator; import edu.ntnu.idi.idatt.session.UserSession; +import edu.ntnu.idi.idatt.storage.SessionManager; import edu.ntnu.idi.idatt.view.components.AbstractController; import edu.ntnu.idi.idatt.view.util.CssUtils; import javafx.scene.chart.LineChart; @@ -13,22 +19,29 @@ public class StockController extends AbstractController { private UserSession session = UserSession.getInstance(); + private NumberFormat formatter = NumberFormat.getNumberInstance(); + private final Stock stock; + private Share share; + private PurchaseCalculator purchaseCalculator; public StockController(StockModel model, Stock stock) { super(model); + formatter.setMaximumFractionDigits(3); this.stock = stock; - initStock(); + initController(); } - public void initStock() { + private void initController() { renderGraph(); setCurrentPrice(); setLatestChange(); setAllTimeScore(); setOwnedAmount(); setTotalProfits(); + + initHooks(); } public String getSymbol() { @@ -58,6 +71,75 @@ public void renderGraph() { model.getGraphNodes().setAll(lineChart); } + private void initHooks() { + resetDisplayBuffers(); + model.getBuyInputField().addListener((obervable, oldVal, newVal) -> displayBuyInfo(newVal)); + } + + public void buyButtonClicked() { + if (share == null || purchaseCalculator == null) { + resetDisplayBuffers(); // Flush after color change and new press. + model.getResultMessage().set("Invalid purchase."); + return; + } + + BigDecimal purchase = session.getPlayer().getMoney().subtract(purchaseCalculator.calculateTotal()); + if (purchase.compareTo(BigDecimal.ZERO) <= 0) { + // Flush after color change and new press, while still calculating everything. + model.getResultMessageColorProperty().set(CssUtils.RED); + model.getResultMessage().set("Balance too low!"); + return; + } + + session.getExchange().buy(share.getStock().getSymbol(), share.getQuantity(), session.getPlayer()); + this.setOwnedAmount(); + this.setTotalProfits(); + model.getResultMessageColorProperty().set(CssUtils.GREEN); + model.getResultMessage().set("Purchase completed!"); + SessionManager.saveSession(); + + } + + private void resetDisplayBuffers() { + model.getBuyPrice().set("0 USD"); + model.getBuyCost().set("0 USD"); + model.getTotalPrice().set("0 USD"); + model.getResultMessage().set(""); + model.getResultMessageColorProperty().set(CssUtils.RED); + } + + private void displayBuyInfo(String amountString) { + resetDisplayBuffers(); // Reset buffers + share = null; + purchaseCalculator = null; + + if (amountString == null || amountString.isEmpty()) + return; + + BigDecimal value; + try { + value = new BigDecimal(amountString); + } catch (NumberFormatException e) { + model.getResultMessage().set("Only numbers allowed!"); + return; + } + + if (value.compareTo(BigDecimal.ZERO) <= 0) { + model.getResultMessage().set("Invalid amount!"); + return; + } + + share = new Share(session.getExchange().getStock(this.stock.getSymbol()), + value, + session.getExchange().getStock(this.getSymbol()).getSalesPrice()); + purchaseCalculator = new PurchaseCalculator(share); + + model.getBuyPrice().set(formatter.format(purchaseCalculator.calculateGross()) + " USD"); + model.getBuyCost() + .set(formatter.format(purchaseCalculator.calculateCommision().add(purchaseCalculator.calculateTax())) + " USD"); + model.getTotalPrice().set(formatter.format(purchaseCalculator.calculateTotal()) + " USD"); + } + public void setCurrentPrice() { model.getStockPrice().set(this.stock.getSalesPrice() + " USD"); } @@ -88,7 +170,7 @@ public void setTotalProfits() { double profit = session.getPlayer().getPortfolio().getProfitFromStock( this.stock.getSymbol()).doubleValue(); double profitPercent = session.getPlayer().getPortfolio().getChangeFromStock( - this.stock.getSymbol()); + this.stock.getSymbol()).doubleValue(); model.getTotalProfits().set(profit + " USD (" + profitPercent + "%)"); model.getTotalProfitsColorProperty().set(CssUtils.generateValueColors(profit)); diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockModel.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockModel.java index 2fc471a..8b5c6c8 100644 --- a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockModel.java +++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockModel.java @@ -8,7 +8,10 @@ public class StockModel implements Model { + // Graph private final ObservableList graphNodes = FXCollections.observableArrayList(); + + // Price properties private final SimpleStringProperty stockPrice = new SimpleStringProperty(); private final SimpleStringProperty latestChange = new SimpleStringProperty(); @@ -21,6 +24,14 @@ public class StockModel implements Model { private final SimpleStringProperty totalProfits = new SimpleStringProperty(); private final SimpleStringProperty totalProfitsColorProperty = new SimpleStringProperty(); + // Trade properties + private final SimpleStringProperty buyInputField = new SimpleStringProperty(); + private final SimpleStringProperty resultMessage = new SimpleStringProperty(); + private final SimpleStringProperty resultMessageColorProperty = new SimpleStringProperty(); + private final SimpleStringProperty buyPrice = new SimpleStringProperty(); + private final SimpleStringProperty buyCost = new SimpleStringProperty(); + private final SimpleStringProperty totalPrice = new SimpleStringProperty(); + public ObservableList getGraphNodes() { return this.graphNodes; } @@ -53,4 +64,28 @@ public SimpleStringProperty getTotalProfitsColorProperty() { return totalProfitsColorProperty; } + public SimpleStringProperty getBuyInputField() { + return buyInputField; + } + + public SimpleStringProperty getResultMessage() { + return resultMessage; + } + + public SimpleStringProperty getResultMessageColorProperty() { + return resultMessageColorProperty; + } + + public SimpleStringProperty getBuyPrice() { + return buyPrice; + } + + public SimpleStringProperty getBuyCost() { + return buyCost; + } + + public SimpleStringProperty getTotalPrice() { + return totalPrice; + } + } diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java index f4600ca..65d721c 100644 --- a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java +++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java @@ -37,9 +37,14 @@ public class StockView extends AbstractViewUI { TextValueComponent ownedAmount; TextValueComponent totalProfits; - // Buttons + // Trade menu Button buyButton; Button portfolioButton; + TextField buyInputField; + TextValueComponent buyPrice; + TextValueComponent buyCost; + TextValueComponent totalPrice; + TextValueComponent resultMessage; @Override public Parent createContent() { @@ -51,12 +56,14 @@ public Parent createContent() { graphContainer = new HBox(); graphContainer.setPadding(new Insets(40.0)); HBox.setHgrow(graphContainer, Priority.ALWAYS); + CssUtils.apply(graphContainer, CssUtils.SMALL_TEXT_12); UICompositor userSection = new UICompositor.Builder() .parent(new HBox()) .growWithAlignment(Pos.TOP_CENTER) .properties((parent) -> parent.setPadding(new Insets(20.0))) + .wrap(new VBox()) .addContent(stockPrice = new TextValueComponent("Current price: ")) .addContent(latestChange = new TextValueComponent("Latest Price Change: ")) @@ -64,22 +71,31 @@ public Parent createContent() { .addContent(ownedAmount = new TextValueComponent("Owned amount: ")) .addContent(totalProfits = new TextValueComponent("Total profits: ")) .unwrap() + .filler() + .wrap(new VBox()) + .wrap(new HBox()) .addContent(buyButton = new Button("Buy")) .addContent(portfolioButton = new Button("Sell in portfolio...")) .properties((wrapper) -> ((HBox) wrapper).setSpacing(20.0)) .unwrap() + .wrap(new VBox()) - .addContent(new TextField()) - .addContent(new Label("Price: ")) - .addContent(new Label("Where taxes and commision: ")) - .addContent(new Label("Total: ")) + .properties((wrapper) -> wrapper.setPadding(new Insets(20))) + .addContent(buyInputField = new TextField()) + .addContent(buyPrice = new TextValueComponent("Price: ")) + .addContent(buyCost = new TextValueComponent("Where taxes and commision: ")) + .addContent(totalPrice = new TextValueComponent("Total: ")) .unwrap() + .addContent(resultMessage = new TextValueComponent("")) .unwrap() .build(); + // Detailing + buyInputField.setPromptText("Amount of stocks..."); + root.getChildren().addAll(graphContainer, userSection.makeUI()); return root; } @@ -112,7 +128,10 @@ public Parent createMenu() { } public void setModel(StockModel model) { + // Graph Bindings.bindContent(graphContainer.getChildren(), model.getGraphNodes()); + + // Price display bindings stockPrice.valueProperty().bind(model.getStockPrice()); latestChange.valueProperty().bind(model.getLatestChange()); @@ -124,10 +143,20 @@ public void setModel(StockModel model) { totalProfits.valueProperty().bind(model.getTotalProfits()); totalProfits.colorProperty().bind(model.getTotalProfitsColorProperty()); + + // Trade menu bindings + buyInputField.textProperty().bindBidirectional(model.getBuyInputField()); + buyPrice.valueProperty().bind(model.getBuyPrice()); + buyCost.valueProperty().bind(model.getBuyCost()); + totalPrice.valueProperty().bind(model.getTotalPrice()); + resultMessage.valueProperty().bind(model.getResultMessage()); + resultMessage.colorProperty().bind(model.getResultMessageColorProperty()); + } public void setController(StockController controller) { title.setText(controller.getSymbol()); + buyButton.setOnAction((e) -> controller.buyButtonClicked()); } } diff --git a/src/main/java/edu/ntnu/idi/idatt/view/util/CssUtils.java b/src/main/java/edu/ntnu/idi/idatt/view/util/CssUtils.java index f4b87db..f689a60 100644 --- a/src/main/java/edu/ntnu/idi/idatt/view/util/CssUtils.java +++ b/src/main/java/edu/ntnu/idi/idatt/view/util/CssUtils.java @@ -14,6 +14,11 @@ public static void apply(Parent parent, String cssClass) { parent.getStyleClass().add(cssClass); } + public static void set(Parent parent, String cssClass) { + parent.getStyleClass().clear(); + parent.getStyleClass().add(cssClass); + } + public static String generateValueColors(double value) { return value >= 0 ? GREEN : RED; } From 989d98d936602c1397007e00e80c2cdcc660c99c Mon Sep 17 00:00:00 2001 From: pawelsa Date: Wed, 13 May 2026 16:27:19 +0200 Subject: [PATCH 08/11] feat(SaleCalculator): Implement method for getting total profit --- .../ntnu/idi/idatt/service/transaction/SaleCalculator.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/edu/ntnu/idi/idatt/service/transaction/SaleCalculator.java b/src/main/java/edu/ntnu/idi/idatt/service/transaction/SaleCalculator.java index 8bbe827..102f1a5 100644 --- a/src/main/java/edu/ntnu/idi/idatt/service/transaction/SaleCalculator.java +++ b/src/main/java/edu/ntnu/idi/idatt/service/transaction/SaleCalculator.java @@ -76,4 +76,9 @@ public BigDecimal calculateTotal() { return calculateGross().subtract(calculateCommision()).subtract(calculateTax()); } + // TODO: Javadocs, junit + public BigDecimal calculateProfit() { + return calculateTotal().divide(purchasePrice.multiply(quantity)); + } + } From 6830e375d1e6fa9b40342e4e1a92e87c0df87d4f Mon Sep 17 00:00:00 2001 From: pawelsa Date: Wed, 13 May 2026 16:28:38 +0200 Subject: [PATCH 09/11] chore: Scaling fixes --- .../idi/idatt/view/components/AbstractViewUI.java | 4 +++- .../ntnu/idi/idatt/view/primary/stock/StockView.java | 11 +++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java index 5d9fd48..8c1a119 100644 --- a/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java @@ -59,7 +59,9 @@ public void createUIComponents() { navigation = new VBox(); navigation.setMaxHeight(Double.MAX_VALUE); navigation.getStyleClass().add("dark"); - navigation.setMaxWidth(200); + navigation.setPrefWidth(150); // ScrollPane's affect this massively + navigation.setMaxWidth(150); + navigation.setMinWidth(150); header = new HBox(); header.getStyleClass().add("light"); diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java index 65d721c..3b43bf7 100644 --- a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java +++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java @@ -1,12 +1,6 @@ package edu.ntnu.idi.idatt.view.primary.stock; -import java.math.BigDecimal; import java.util.List; -import java.util.stream.Collectors; - -import edu.ntnu.idi.idatt.model.portfolio.Share; -import edu.ntnu.idi.idatt.service.transaction.SaleCalculator; -import edu.ntnu.idi.idatt.session.UserSession; import edu.ntnu.idi.idatt.view.SceneFactory; import edu.ntnu.idi.idatt.view.SceneManager; import edu.ntnu.idi.idatt.view.components.AbstractViewUI; @@ -84,9 +78,10 @@ public Parent createContent() { .wrap(new VBox()) .properties((wrapper) -> wrapper.setPadding(new Insets(20))) + .properties((wrapper) -> wrapper.setMaxWidth(400)) .addContent(buyInputField = new TextField()) .addContent(buyPrice = new TextValueComponent("Price: ")) - .addContent(buyCost = new TextValueComponent("Where taxes and commision: ")) + .addContent(buyCost = new TextValueComponent("Taxes & commision: ")) .addContent(totalPrice = new TextValueComponent("Total: ")) .unwrap() .addContent(resultMessage = new TextValueComponent("")) @@ -123,7 +118,7 @@ public Parent createMenu() { return UIFactory.createMenu("Account", List.of(" • Portfolio", " • Transactions"), () -> System.out.println("Portfolio clicked!"), - () -> System.out.println("Transaction clicked!")); + () -> SceneManager.switchTo(SceneFactory.createTransactionView())); } From c151758cf5a63b020d54d4e7919233aa5f4296bc Mon Sep 17 00:00:00 2001 From: pawelsa Date: Wed, 13 May 2026 16:28:54 +0200 Subject: [PATCH 10/11] feat: Implement TransactionComponent class --- .../elements/TransactionComponent.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/main/java/edu/ntnu/idi/idatt/view/components/elements/TransactionComponent.java diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/TransactionComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/TransactionComponent.java new file mode 100644 index 0000000..850af4a --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/TransactionComponent.java @@ -0,0 +1,94 @@ +package edu.ntnu.idi.idatt.view.components.elements; + +import java.util.List; + +import edu.ntnu.idi.idatt.model.market.Stock; +import edu.ntnu.idi.idatt.model.transaction.Purchase; +import edu.ntnu.idi.idatt.model.transaction.Sale; +import edu.ntnu.idi.idatt.model.transaction.Transaction; +import edu.ntnu.idi.idatt.service.transaction.PurchaseCalculator; +import edu.ntnu.idi.idatt.service.transaction.SaleCalculator; +import edu.ntnu.idi.idatt.view.components.ui.UICompositor; +import edu.ntnu.idi.idatt.view.util.CssUtils; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; + +public class TransactionComponent extends VBox { + + public TransactionComponent(Transaction transaction) { + + Stock stock = transaction.getShare().getStock(); + + Label title; + Label name = new Label(stock.getCompany() + " (" + stock.getSymbol() + ")"); + Label totalValue; + Label taxComissionValue; + Label grossValue; + Label amountOfShares; + Label saleProfit; + + if (transaction instanceof Purchase) { + title = new Label("PURCHASE"); + CssUtils.apply(title, CssUtils.GREEN); + + PurchaseCalculator calculator = (PurchaseCalculator) transaction.getCalculator(); + totalValue = new Label("Total: " + calculator.calculateTotal() + " USD"); + taxComissionValue = new Label( + "Tax & Comission: " + calculator.calculateTax().add(calculator.calculateCommision()) + " USD"); + grossValue = new Label("Gross: " + calculator.calculateGross() + " USD"); + amountOfShares = new Label("Shares: " + transaction.getShare().getQuantity() + " [" + stock.getSymbol() + "]"); + saleProfit = new Label(); + + } else if (transaction instanceof Sale) { + title = new Label("SALE"); + CssUtils.apply(title, CssUtils.RED); + + SaleCalculator calculator = (SaleCalculator) transaction.getCalculator(); + totalValue = new Label("Total: " + calculator.calculateTotal() + " USD"); + taxComissionValue = new Label( + "Tax & Comission: " + calculator.calculateTax().add(calculator.calculateCommision()) + " USD"); + grossValue = new Label("Gross: " + calculator.calculateGross() + " USD"); + amountOfShares = new Label("Shares: " + transaction.getShare().getQuantity() + " [" + stock.getSymbol() + "]"); + saleProfit = new Label("Profit: " + calculator.calculateProfit() + " USD"); + + CssUtils.apply(saleProfit, CssUtils.generateValueColors(calculator.calculateProfit().doubleValue())); + } + + else { + System.out.println("Failed to initialize transactionComponent!"); + return; + } + + this.setPrefSize(400, Double.MAX_VALUE); + this.setMinWidth(400); + this.setPadding(new Insets(40)); + this.setAlignment(Pos.TOP_CENTER); + + // TODO: CHANGE AND IN STOCKComponent + this.setStyle("-fx-background-color: #404950;"); + + CssUtils.apply(title, CssUtils.BIG_TEXT_32); + CssUtils.apply(name, CssUtils.BIG_TEXT_32); + List