netWorthProperty() {
+ return netWorthProperty;
+ }
+
+ public void updateGameState() {
+ moneyProperty.set(player.getMoney());
+ netWorthProperty.set(player.getNetWorth());
+ SessionManager.saveSession();
+ }
+
+ public SessionBundle getSession() {
+ return new SessionBundle(player, exchange);
+ }
+
+ public class SessionBundle {
+
+ private Player player;
+ private Exchange exchange;
+
+ public SessionBundle(Player player, Exchange exchange) {
+ this.player = player;
+ this.exchange = exchange;
+ }
+
+ public Player getPlayer() {
+ return player;
+ }
+
+ public Exchange getExchange() {
+ return exchange;
+ }
+
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java b/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java
new file mode 100644
index 0000000..94626d1
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java
@@ -0,0 +1,153 @@
+package edu.ntnu.idi.idatt.storage;
+
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.reflect.Type;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+
+import edu.ntnu.idi.idatt.model.Exchange;
+import edu.ntnu.idi.idatt.model.market.Stock;
+import edu.ntnu.idi.idatt.model.player.Player;
+import edu.ntnu.idi.idatt.model.portfolio.Portfolio;
+import edu.ntnu.idi.idatt.model.portfolio.Share;
+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.
+ *
+ *
+ * Utilizes gson serialization and deserialization to manage
+ * player and game state.
+ */
+public class SessionManager {
+
+ // Gson type to list format. Instead of using wrapper class.
+ private static Type SESSION_BUNDLE_TYPE = new TypeToken>() {
+ }.getType();
+
+ 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.
+ static {
+ StorageFile.ensureAppDataDirectoryExists();
+ }
+
+ /**
+ * Method for setting new session.
+ *
+ * @see UserSession
+ */
+ public static void newSession(Player player, Exchange exchange) {
+ UserSession.getInstance().setPlayer(player);
+ UserSession.getInstance().setExchange(exchange);
+ }
+
+ /**
+ * Method for saving current session.
+ */
+ public static void saveSession() {
+ // don't save if current session is null accidentally
+ if (UserSession.getInstance().getPlayer() == null || UserSession.getInstance().getExchange() == null) {
+ return;
+ }
+
+ // Load all sessions
+ List bundles = loadAllSessions();
+
+ try (Writer writer = new FileWriter(StorageFile.getStorageFile().toFile())) {
+
+ // Append current session
+ SessionBundle existing = bundles.stream()
+ .filter(s -> s.getPlayer().getName().equals(UserSession.getInstance().getPlayer().getName()))
+ .findFirst().orElse(null);
+
+ if (existing != null) {
+ bundles.set(bundles.indexOf(existing), UserSession.getInstance().getSession());
+ } else {
+ bundles.add(UserSession.getInstance().getSession());
+ }
+
+ gson.toJson(bundles, writer);
+
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to save current session!", e);
+ }
+ }
+
+ /**
+ * Method for loading a user session by player name.
+ *
+ * @return if session was found or not.
+ */
+ public static boolean loadSession(String playerName) {
+
+ List bundles = loadAllSessions();
+
+ for (SessionBundle session : bundles) {
+ if (session.getPlayer().getName().equals(playerName)) {
+ UserSession.getInstance().setPlayer(session.getPlayer());
+ UserSession.getInstance().setExchange(session.getExchange());
+
+ // Reseed portfolio based on exchange's stock instances.
+ // This has to be done due to Gson loading via reflection.
+ Portfolio portfolio = UserSession.getInstance().getPlayer().getPortfolio();
+ ArrayList validatedShares = new ArrayList<>();
+
+ for (Share oldShare : portfolio.getShares()) { // Create new instances
+ Stock stock = UserSession.getInstance().getExchange().getStock(oldShare.getStock().getSymbol());
+ Share newShare = new Share(stock, oldShare.getQuantity(), oldShare.getPurchasePrice());
+ validatedShares.add(newShare);
+ }
+
+ portfolio.removeShares(); // Remove all old
+ validatedShares.forEach(share -> portfolio.addShare(share)); // Add new
+
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Method for serialization of all sessions.
+ *
+ * @return List or empty list if none entires were made.
+ */
+ private static List loadAllSessions() {
+ try {
+ if (!Files.exists(StorageFile.getStorageFile()) || Files.size(StorageFile.getStorageFile()) == 0) {
+ return new ArrayList<>();
+ }
+ } catch (IOException e) {
+ return new ArrayList<>();
+ }
+
+ try {
+ return gson.fromJson(new FileReader(StorageFile.getStorageFile().toString()), SESSION_BUNDLE_TYPE);
+ } catch (JsonSyntaxException | JsonIOException | FileNotFoundException e) {
+ return new ArrayList<>();
+ }
+
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/StockParser.java b/src/main/java/edu/ntnu/idi/idatt/storage/StockParser.java
new file mode 100644
index 0000000..a4936dc
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/storage/StockParser.java
@@ -0,0 +1,76 @@
+package edu.ntnu.idi.idatt.storage;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+import edu.ntnu.idi.idatt.model.market.Stock;
+
+/**
+ * Utility class for parsing stocks.
+ *
+ *
+ * Decomponents files into the .csv (comma separated valuus)
+ * format, and creats stocks out of them.
+ *
+ */
+public class StockParser {
+
+ // Disable initialization.
+ private StockParser() {
+
+ }
+
+ /**
+ * Method for loading from stocks from file
+ *
+ * @param path - The path to the .csv file.
+ *
+ * @return a list of loaded stocks.
+ * @throws IOException on BufferedReader error
+ */
+ public static List load(String path) throws IOException {
+ File file = new File(path.toString());
+ if (!file.exists()) {
+ throw new IOException("File at this path doesn't exist!");
+ }
+
+ if (!Files.probeContentType(Paths.get(file.getPath())).equals("text/csv")) {
+ throw new IOException("Please choose a .csv file!");
+ }
+
+ ArrayList stocks = new ArrayList<>();
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+ List stockStringList = new ArrayList<>(reader.readAllLines());
+
+ // Remove comments and inproper syntax
+ stockStringList.removeIf(s -> s.isBlank());
+ stockStringList.removeIf(s -> s.startsWith("#"));
+ stockStringList.removeIf(s -> !s.contains(","));
+
+ for (String stockString : stockStringList) {
+ String[] stockValues = stockString.split(",");
+ if (stockValues.length != 3) {
+ throw new IOException("Invalid CSV format!");
+ }
+
+ Stock stock = new Stock(stockValues[0], stockValues[1], List.of(new BigDecimal(stockValues[2])));
+ stocks.add(stock);
+ }
+
+ } catch (IOException e) {
+ throw new IOException("File loading failed!");
+ }
+
+ return stocks;
+
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/StorageFile.java b/src/main/java/edu/ntnu/idi/idatt/storage/StorageFile.java
new file mode 100644
index 0000000..9fa36e7
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/storage/StorageFile.java
@@ -0,0 +1,75 @@
+package edu.ntnu.idi.idatt.storage;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * Utility class for system-specific path obtaining.
+ */
+public class StorageFile {
+ private static final String STORAGE_FOLDER = "Millions";
+
+ /**
+ * Method for obtaining storage file path.
+ *
+ * @return Path object of storage file.
+ */
+ public static Path getStorageFile() {
+ return getAppDataDirectory().resolve("storage.json");
+ }
+
+ /**
+ * Method for ensuring that storage directory exists.
+ *
+ *
+ * Attempts to create the application data directory if
+ * one does not exist.
+ *
+ */
+ public static void ensureAppDataDirectoryExists() {
+ Path dir = getAppDataDirectory();
+ try {
+ Files.createDirectories(dir);
+
+ Path storageFile = dir.resolve("storage.json");
+ if (!Files.exists(storageFile)) {
+ Files.createFile(storageFile);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Could not create app data directory: " + dir, e);
+ }
+ }
+
+ /**
+ * Method for obtaining system-specific directory for application data.
+ *
+ * @return Path object of the found directory.
+ */
+ private static Path getAppDataDirectory() {
+ String userHome = System.getProperty("user.home");
+ String os = System.getProperty("os.name").toLowerCase();
+
+ // AppData folder for windows
+ if (os.contains("win")) {
+ String appData = System.getenv("APPDATA");
+
+ if (appData != null) {
+ return Paths.get(appData, STORAGE_FOLDER);
+ }
+ // If AppData not a environmental variable, sets to user directory.
+ return Paths.get(userHome, STORAGE_FOLDER);
+
+ }
+ // Library folder in MacOS
+ if (os.contains("mac")) {
+ return Paths.get(userHome, "Library", "Application Support", STORAGE_FOLDER);
+ }
+
+ // All linux and unix-like systems.
+ return Paths.get(userHome, ".local", "share", STORAGE_FOLDER);
+
+ }
+
+}
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);
+ }
+
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/SceneFactory.java b/src/main/java/edu/ntnu/idi/idatt/view/SceneFactory.java
new file mode 100644
index 0000000..165182a
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/SceneFactory.java
@@ -0,0 +1,149 @@
+package edu.ntnu.idi.idatt.view;
+
+import edu.ntnu.idi.idatt.model.market.Stock;
+import edu.ntnu.idi.idatt.session.UserSession;
+import edu.ntnu.idi.idatt.view.entry.StartController;
+import edu.ntnu.idi.idatt.view.entry.StartModel;
+import edu.ntnu.idi.idatt.view.entry.StartView;
+import edu.ntnu.idi.idatt.view.primary.newspaper.NewspaperController;
+import edu.ntnu.idi.idatt.view.primary.newspaper.NewspaperModel;
+import edu.ntnu.idi.idatt.view.primary.newspaper.NewspaperView;
+import edu.ntnu.idi.idatt.view.primary.portfolio.PortfolioController;
+import edu.ntnu.idi.idatt.view.primary.portfolio.PortfolioModel;
+import edu.ntnu.idi.idatt.view.primary.portfolio.PortfolioView;
+import edu.ntnu.idi.idatt.view.primary.exchange.ExchangeController;
+import edu.ntnu.idi.idatt.view.primary.exchange.ExchangeModel;
+import edu.ntnu.idi.idatt.view.primary.exchange.ExchangeView;
+import edu.ntnu.idi.idatt.view.primary.stock.StockController;
+import edu.ntnu.idi.idatt.view.primary.stock.StockModel;
+import edu.ntnu.idi.idatt.view.primary.stock.StockView;
+import edu.ntnu.idi.idatt.view.primary.transactions.TransactionController;
+import edu.ntnu.idi.idatt.view.primary.transactions.TransactionModel;
+import edu.ntnu.idi.idatt.view.primary.transactions.TransactionView;
+import javafx.scene.Parent;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+public class SceneFactory {
+
+ @FunctionalInterface
+ public interface MVCInitInterface {
+ Parent execute();
+ }
+
+ private static Deque navigation = new ArrayDeque<>();
+ private static boolean navigatingBack = false;
+
+ public static void goBack() {
+ if (navigation.size() > 1) {
+ navigation.pop();
+ navigatingBack = true;
+ SceneManager.switchTo(navigation.peek().execute());
+ navigatingBack = false;
+ }
+ }
+
+ public static void reloadCurrent() {
+ navigatingBack = true;
+ SceneManager.switchTo(navigation.peek().execute());
+ navigatingBack = false;
+ }
+
+ private static void mark(MVCInitInterface initializer) {
+ if (!navigatingBack) {
+ navigation.push(initializer);
+ }
+ }
+
+ public static boolean isFinal() {
+ return navigation.size() == 1;
+ }
+
+ public static Parent createStartView() {
+
+ navigation.clear();
+
+ StartModel model = new StartModel();
+ StartView view = new StartView();
+ StartController controller = new StartController(model);
+
+ view.setModel(model);
+ view.setController(controller);
+
+ return view.getInstance();
+
+ }
+
+ public static Parent createPortfolioView() {
+
+ mark(() -> createPortfolioView());
+
+ PortfolioModel model = new PortfolioModel();
+ PortfolioView view = new PortfolioView();
+ PortfolioController controller = new PortfolioController(model);
+
+ view.setModel(model);
+ view.setController(controller);
+
+ return view.getInstance();
+ }
+
+ public static Parent createExchangeView() {
+
+ mark(() -> createExchangeView());
+
+ ExchangeModel model = new ExchangeModel();
+ ExchangeView view = new ExchangeView();
+ ExchangeController controller = new ExchangeController(model);
+
+ view.setModel(model);
+ view.setController(controller);
+
+ return view.getInstance();
+
+ }
+
+ public static Parent createStockView(String symbol) {
+
+ mark(() -> createStockView(symbol));
+ Stock stock = UserSession.getInstance().getExchange().getStock(symbol);
+
+ StockModel model = new StockModel();
+ StockView view = new StockView();
+ StockController controller = new StockController(model, stock);
+
+ view.setModel(model);
+ view.setController(controller);
+
+ return view.getInstance();
+ }
+
+ public static Parent createTransactionView() {
+
+ mark(() -> createTransactionView());
+
+ TransactionModel model = new TransactionModel();
+ TransactionView view = new TransactionView();
+ TransactionController controller = new TransactionController(model);
+
+ view.setModel(model);
+ view.setController(controller);
+
+ return view.getInstance();
+ }
+ public static Parent createNewspaperView() {
+
+ mark(() -> createNewspaperView());
+
+ NewspaperModel model = new NewspaperModel();
+ NewspaperView view = new NewspaperView();
+ NewspaperController controller = new NewspaperController(model);
+
+ view.setModel(model);
+ view.setController(controller);
+
+ return view.getInstance();
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/SceneManager.java b/src/main/java/edu/ntnu/idi/idatt/view/SceneManager.java
new file mode 100644
index 0000000..be87d13
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/SceneManager.java
@@ -0,0 +1,55 @@
+package edu.ntnu.idi.idatt.view;
+
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+/**
+ * SceneManager class for managing objects to the stage.
+ *
+ *
+ * This class provides methods for changing the root object to
+ * the scene, providing simple use of components to change between the
+ * pages
+ *
+ *
+ * @author Pawel Sapula
+ */
+public final class SceneManager {
+
+ // Store scene reference.
+ private static Scene scene;
+
+ // Disable initialization.
+ private SceneManager() {
+ }
+
+ /**
+ * Method for intialization of the Scene Manager.
+ *
+ *
+ * This method enables initialization of the primary scene,
+ * based on the stage from the program entry point, and a
+ * page based on the page's root object.
+ *
+ *
+ * @param stage - Application's stage
+ * @param root - The root object / fundamental object of a page
+ */
+ public static void init(Stage stage, Parent root) {
+ scene = new Scene(root);
+ // Add styling sheet permamently for the scene.
+ scene.getStylesheets().add(SceneManager.class.getResource("/themes/default.css").toExternalForm());
+ stage.setScene(scene);
+ }
+
+ /**
+ * Method for switching the scene's root variable.
+ *
+ * @param root - Parent JavaFX object.
+ */
+ public static void switchTo(Parent root) {
+ scene.setRoot(root);
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractController.java b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractController.java
new file mode 100644
index 0000000..a334769
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractController.java
@@ -0,0 +1,11 @@
+package edu.ntnu.idi.idatt.view.components;
+
+public abstract class AbstractController {
+
+ protected final M model;
+
+ public AbstractController(M model) {
+ this.model = model;
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractView.java b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractView.java
new file mode 100644
index 0000000..b4f96ec
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractView.java
@@ -0,0 +1,72 @@
+package edu.ntnu.idi.idatt.view.components;
+
+import javafx.scene.Parent;
+import javafx.scene.layout.Pane;
+
+/**
+ * INFO!
+ *
+ *
+ * This class is based on the generic Type parameter - T. It's experimental
+ * for the time of creating the class (24.04.26), since parent don't have access
+ * to the
+ * getChildren() method which could provide usefuleness in creating a standalone
+ * abstract class.
+ *
+ * Since most views will be based on the Pane subclasses, it is fully
+ * functional, but
+ * may throw errors if the typeparameter isn't of this type.
+ *
+ *
+ * Reference: https://docs.oracle.com/javase/8/javafx/api/overview-tree.html
+ */
+
+/**
+ * AbstractView class
+ *
+ *
+ * Provides a simple abstraction of the Pane subclasses,
+ * making it a useful framework for creating different views.
+ *
+ */
+public abstract class AbstractView {
+
+ private T instance;
+
+ /**
+ * Constructor for AbstractView.
+ */
+ protected AbstractView(T type) {
+ instance = type;
+ instance.getChildren().add(this.createContent());
+ }
+
+ /**
+ * Constructor for AbstractView without initialization.
+ */
+ protected AbstractView() {
+
+ }
+
+ /**
+ * Getter for current view instance.
+ *
+ * @return Instance of current view.
+ */
+ public T getInstance() {
+ return instance;
+ }
+
+ /**
+ * Setter for current view instance.
+ */
+ public void setInstance(T instance) {
+ this.instance = instance;
+ }
+
+ /**
+ * Abstract method for creating view content.
+ */
+ public abstract Parent createContent();
+
+}
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
new file mode 100644
index 0000000..8c1a119
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java
@@ -0,0 +1,106 @@
+package edu.ntnu.idi.idatt.view.components;
+
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.geometry.Pos;
+import javafx.scene.Parent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+
+public abstract class AbstractViewUI extends AbstractView {
+
+ private VBox navigation;
+ private HBox header;
+ private HBox toolbar;
+ private VBox menu;
+ private SimpleBooleanProperty isMenuVisible = new SimpleBooleanProperty(false);
+
+ public AbstractViewUI() {
+ HBox wrapper = new HBox();
+ BorderPane layout = new BorderPane();
+
+ createUIComponents();
+ navigation.getChildren().add(createNavigation());
+ header.getChildren().add(createHeader());
+ toolbar.getChildren().add(createToolbar());
+
+ layout.setTop(header);
+ layout.setBottom(toolbar);
+ layout.setCenter(createContent());
+
+ HBox.setHgrow(layout, Priority.ALWAYS);
+ wrapper.getChildren().addAll(navigation, layout);
+
+ this.setInstance(new StackPane());
+
+ menu.getChildren().add(createMenu());
+ menu.setVisible(false);
+
+ Region disableMenu = new Region();
+ disableMenu.setVisible(false);
+
+ disableMenu.setOnMouseClicked(e -> {
+ menu.setVisible(false);
+ disableMenu.setVisible(false);
+ });
+
+ isMenuVisible.addListener((observer, oldVal, newVal) -> {
+ menu.setVisible(true);
+ disableMenu.setVisible(true);
+ });
+
+ this.getInstance().getChildren().addAll(wrapper, disableMenu, menu);
+ }
+
+ public void createUIComponents() {
+ navigation = new VBox();
+ navigation.setMaxHeight(Double.MAX_VALUE);
+ navigation.getStyleClass().add("dark");
+ navigation.setPrefWidth(150); // ScrollPane's affect this massively
+ navigation.setMaxWidth(150);
+ navigation.setMinWidth(150);
+
+ header = new HBox();
+ header.getStyleClass().add("light");
+ header.setMaxHeight(80);
+
+ toolbar = new HBox();
+ toolbar.getStyleClass().add("light");
+ toolbar.setMaxHeight(80);
+
+ menu = new VBox();
+ StackPane.setAlignment(menu, Pos.CENTER_RIGHT);
+ menu.getStyleClass().add("dark");
+ menu.setMaxWidth(300);
+ }
+
+ public abstract Parent createContent();
+
+ public abstract Parent createNavigation();
+
+ public abstract Parent createHeader();
+
+ public abstract Parent createToolbar();
+
+ public abstract Parent createMenu();
+
+ public VBox getNavigation() {
+ return navigation;
+ }
+
+ public HBox getHeader() {
+ return header;
+ }
+
+ public HBox getToolbar() {
+ return toolbar;
+ }
+
+ public void toggleMenu() {
+ isMenuVisible.set(!isMenuVisible.get());
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/Model.java b/src/main/java/edu/ntnu/idi/idatt/view/components/Model.java
new file mode 100644
index 0000000..2465fd7
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/Model.java
@@ -0,0 +1,4 @@
+package edu.ntnu.idi.idatt.view.components;
+
+public interface Model {
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/IconComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/IconComponent.java
new file mode 100644
index 0000000..a3ef67a
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/IconComponent.java
@@ -0,0 +1,32 @@
+package edu.ntnu.idi.idatt.view.components.elements;
+
+import edu.ntnu.idi.idatt.view.components.primitives.ActionEventHandler;
+import javafx.scene.control.Button;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.StackPane;
+
+public class IconComponent extends StackPane {
+
+ private Button icon;
+
+ public IconComponent(Image image, String description, int size) {
+
+ ImageView iv = new ImageView();
+ iv.setImage(image);
+ iv.setFitHeight(size); // TODO: Fix?
+ iv.setFitWidth(size);
+
+ icon = new Button(description, iv);
+ icon.getStyleClass().clear(); // Remove parent buffers in case.
+ icon.getStyleClass().add("icon");
+ icon.setMaxHeight(Double.MAX_VALUE);
+
+ this.getChildren().add(icon);
+ }
+
+ public void onIconClick(ActionEventHandler handler) {
+ this.icon.setOnAction(e -> handler.handle());
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/NewspaperComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/NewspaperComponent.java
new file mode 100644
index 0000000..8661c9b
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/NewspaperComponent.java
@@ -0,0 +1,33 @@
+package edu.ntnu.idi.idatt.view.components.elements;
+
+import edu.ntnu.idi.idatt.model.portfolio.Share;
+import edu.ntnu.idi.idatt.view.components.ui.UICompositor;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+
+public class NewspaperComponent extends HBox {
+ private final Label newsTitle;
+ private final Label newsText;
+
+ public NewspaperComponent(Share share) {
+ this.setPadding(new Insets(30));
+ this.getStyleClass().add("newspaper-article");
+
+ newsTitle = new Label("Earthquake in Norway");
+ newsText = new Label("Intel are crashing");
+ newsTitle.getStyleClass().add("newspaper-title");
+ newsText.getStyleClass().add("newspaper-med-text");
+
+ UICompositor shareComponent = new UICompositor.Builder()
+ .parent(new VBox())
+ .growWithAlignment(Pos.TOP_CENTER)
+ .addAllContent(newsTitle, newsText)
+ .build();
+
+ this.getChildren().add(shareComponent.makeUI());
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java
new file mode 100644
index 0000000..0e56051
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java
@@ -0,0 +1,45 @@
+package edu.ntnu.idi.idatt.view.components.elements;
+
+import edu.ntnu.idi.idatt.view.components.primitives.ActionEventHandler;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.geometry.Pos;
+import javafx.scene.control.TextField;
+import javafx.scene.image.Image;
+import javafx.scene.layout.HBox;
+
+public class SearchBarComponent extends HBox {
+
+ private final StringProperty query = new SimpleStringProperty();
+ private final IconComponent searchIcon;
+
+ public SearchBarComponent(String placeholder) {
+
+ HBox wrapper = new HBox();
+ wrapper.getStyleClass().add("searchbar");
+ wrapper.setAlignment(Pos.CENTER);
+
+ TextField searchBar = new TextField();
+ searchBar.getStyleClass().add("searchbar-field");
+ searchBar.setPromptText(placeholder);
+ searchBar.setMaxHeight(Double.MAX_VALUE);
+ searchBar.textProperty().bindBidirectional(query);
+ searchBar.setFocusTraversable(false);
+
+ Image image = new Image(this.getClass().getResource("/icons/search.png/").toExternalForm());
+ searchIcon = new IconComponent(image, null, 32);
+
+ wrapper.getChildren().addAll(searchBar, searchIcon);
+
+ this.getChildren().addAll(wrapper);
+ }
+
+ public String getQuery() {
+ return query.get();
+ }
+
+ public void onSearchQuery(ActionEventHandler handler) {
+ this.searchIcon.onIconClick(() -> handler.handle());
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/ShareComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/ShareComponent.java
new file mode 100644
index 0000000..486b994
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/ShareComponent.java
@@ -0,0 +1,64 @@
+package edu.ntnu.idi.idatt.view.components.elements;
+
+import edu.ntnu.idi.idatt.model.portfolio.Share;
+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.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+public class ShareComponent extends HBox {
+
+ private final Share share;
+ private Button sellButton;
+
+ public ShareComponent(Share share) {
+ this.share = share;
+
+ this.setMaxSize(Double.MAX_VALUE, 300);
+ this.setPadding(new Insets(30));
+ this.getStyleClass().add("rowBox");
+
+ Label title = new Label(share.getStock().toString());
+ Label quantity = new Label("Owned shares: " + share.getQuantity().toString());
+ Label latestValueLabel = new Label("Net profit:");
+ Label latestValue = new Label(String.format("%.2f $", share.getProfit()));
+ Label latestValueChange = new Label(String.format("%.2f %%", share.getProfitPercent()));
+
+ sellButton = new Button("Sell");
+
+ List labels = List.of(title, quantity, latestValueLabel, latestValue, latestValueChange);
+ labels.forEach(e -> e.getStyleClass().add("portfolio-box-text"));
+ sellButton.getStyleClass().add("button");
+ String color = CssUtils.generateValueColors(share.getProfit());
+ CssUtils.apply(latestValue, color);
+ CssUtils.apply(latestValueChange, color);
+
+ UICompositor shareComponent = new UICompositor.Builder()
+ .parent(new HBox())
+ .growWithAlignment(Pos.CENTER)
+ .addContent(title)
+ .filler()
+ .wrap(new VBox())
+ .addAllContent(latestValueLabel, latestValue, latestValueChange)
+ .unwrap()
+ .filler()
+ .addContent(quantity)
+ .filler()
+ .addContent(sellButton)
+ .build();
+
+ this.getChildren().add(shareComponent.makeUI());
+ }
+
+ public void onShareSellButton(Consumer handler) {
+ sellButton.setOnAction((e) -> handler.accept(share));
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/StockComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/StockComponent.java
new file mode 100644
index 0000000..fe29fdc
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/StockComponent.java
@@ -0,0 +1,69 @@
+package edu.ntnu.idi.idatt.view.components.elements;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+import edu.ntnu.idi.idatt.model.market.Stock;
+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.HBox;
+import javafx.scene.layout.VBox;
+
+public class StockComponent extends HBox {
+
+ private final Stock stock;
+
+ public StockComponent(Stock stock) {
+ this.stock = stock;
+ this.setMaxSize(Double.MAX_VALUE, 100);
+ this.setPadding(new Insets(40));
+ this.setAlignment(Pos.BASELINE_CENTER);
+
+ this.setStyle("-fx-background-color: #404950;");
+
+ Label title = new Label(stock.toString());
+
+ Label latestText = new Label("Latest");
+ Label latestValue = new Label(String.format("%.2f USD", stock.getSalesPrice()));
+
+ Label changeText = new Label("Change");
+ Label changeValue = new Label(String.format("%.2f USD", stock.getLatestPriceChange()));
+ Label changeValuePercent = new Label(
+ String.valueOf(stock.getLatestPriceChangePercent()) + "%");
+
+ List labels = List.of(latestText, latestValue, changeText, changeValue, changeValuePercent);
+
+ CssUtils.apply(title, CssUtils.BIG_TEXT_32);
+ labels.forEach(l -> CssUtils.apply(l, CssUtils.MED_TEXT_16));
+
+ String color = CssUtils.generateValueColors(stock.getLatestPriceChange().doubleValue());
+ CssUtils.apply(changeValue, color);
+ CssUtils.apply(changeValuePercent, color);
+
+ UICompositor stockComponent = new UICompositor.Builder()
+ .parent(new HBox())
+ .growWithAlignment(Pos.CENTER)
+ .addContent(title)
+ .filler()
+ .filler()
+ .filler()
+ .wrap(new VBox())
+ .addAllContent(latestText, latestValue)
+ .unwrap()
+ .filler()
+ .wrap(new VBox())
+ .addAllContent(changeText, changeValue, changeValuePercent)
+ .unwrap()
+ .build();
+
+ this.getChildren().addAll(stockComponent.makeUI());
+ }
+
+ public void onStockClick(Consumer handler) {
+ this.setOnMouseClicked((e) -> handler.accept(stock.getSymbol()));
+ }
+
+}
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
new file mode 100644
index 0000000..21b8617
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/TextValueComponent.java
@@ -0,0 +1,36 @@
+package edu.ntnu.idi.idatt.view.components.elements;
+
+import edu.ntnu.idi.idatt.view.util.CssUtils;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+
+public class TextValueComponent extends HBox {
+
+ private final SimpleStringProperty value = new SimpleStringProperty();
+ private final SimpleStringProperty color = new SimpleStringProperty();
+
+ public TextValueComponent(String prefix) {
+ Label prefixLabel = new Label(prefix);
+ Label valueLabel = new Label();
+ valueLabel.textProperty().bind(value);
+ CssUtils.apply(prefixLabel, CssUtils.MED_TEXT_16);
+ CssUtils.apply(valueLabel, CssUtils.MED_TEXT_16);
+
+ color.addListener((obs, oldVal, newVal) -> {
+ CssUtils.set(valueLabel, newVal);
+ CssUtils.apply(valueLabel, CssUtils.MED_TEXT_16);
+ });
+
+ this.getChildren().addAll(prefixLabel, valueLabel);
+ }
+
+ public SimpleStringProperty valueProperty() {
+ return value;
+ }
+
+ public SimpleStringProperty colorProperty() {
+ return color;
+ }
+
+}
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..4939ebd
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/TransactionComponent.java
@@ -0,0 +1,103 @@
+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.service.transaction.TransactionCalculator;
+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.toString());
+ Label totalValue;
+ Label taxComissionValue;
+ Label grossValue;
+ Label amountOfShares;
+ Label saleProfit;
+
+ amountOfShares = new Label(
+ String.format("Shares: %.2f [%s]", transaction.getShare().getQuantity(), stock.getSymbol()));
+
+ if (transaction instanceof Purchase) {
+ title = new Label("PURCHASE");
+ PurchaseCalculator pCalc = (PurchaseCalculator) transaction.getCalculator();
+
+ totalValue = new Label(String.format("Total: %.2f USD", pCalc.calculateTotal()));
+
+ taxComissionValue = new Label(String.format(
+ "Tax & Comission: %.2f USD", pCalc.calculateTax().add(pCalc.calculateCommision())));
+
+ grossValue = new Label(String.format("Gross: %.2f USD", pCalc.calculateGross()));
+
+ saleProfit = new Label();
+ CssUtils.apply(title, CssUtils.GREEN);
+
+ } else if (transaction instanceof Sale) {
+ title = new Label("SALE");
+ CssUtils.apply(title, CssUtils.RED);
+
+ SaleCalculator sCalc = (SaleCalculator) transaction.getCalculator();
+
+ totalValue = new Label(String.format("Total: %.2f USD", sCalc.calculateTotal()));
+
+ taxComissionValue = new Label(String.format(
+ "Tax & Comission: %.2f USD", sCalc.calculateTax().add(sCalc.calculateCommision())));
+
+ grossValue = new Label(String.format("Gross: %.2f USD", sCalc.calculateGross()));
+
+ saleProfit = new Label(String.format("Profit: %.2f USD", sCalc.calculateProfit()));
+
+ CssUtils.apply(saleProfit, CssUtils.generateValueColors(sCalc.calculateProfit()));
+ }
+
+ 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 labels = List.of(totalValue, taxComissionValue, grossValue, amountOfShares, saleProfit);
+ labels.forEach(l -> CssUtils.apply(l, CssUtils.MED_TEXT_16));
+
+ UICompositor transactionComponent = new UICompositor.Builder()
+ .parent(new VBox())
+ .growWithAlignment(Pos.CENTER)
+ .addContent(title)
+ .addContent(name)
+ .filler()
+ .addContent(totalValue)
+ .addContent(taxComissionValue)
+ .addContent(grossValue)
+ .addContent(amountOfShares)
+ .addContent(saleProfit)
+ .build();
+
+ this.getChildren().add(transactionComponent.makeUI());
+
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/primitives/ActionEventHandler.java b/src/main/java/edu/ntnu/idi/idatt/view/components/primitives/ActionEventHandler.java
new file mode 100644
index 0000000..a42993d
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/primitives/ActionEventHandler.java
@@ -0,0 +1,6 @@
+package edu.ntnu.idi.idatt.view.components.primitives;
+
+@FunctionalInterface
+public interface ActionEventHandler {
+ void handle();
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIAlert.java b/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIAlert.java
new file mode 100644
index 0000000..78c7341
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIAlert.java
@@ -0,0 +1,26 @@
+package edu.ntnu.idi.idatt.view.components.ui;
+
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.control.ButtonType;
+
+public class UIAlert {
+
+ Alert alert = new Alert(AlertType.CONFIRMATION);
+ ButtonType confirm = new ButtonType("Confirm");
+ ButtonType cancel = new ButtonType("Cancel");
+
+ public UIAlert(String title, String header, String content) {
+ alert.setTitle(title);
+ alert.setHeaderText(header);
+ alert.setContentText(content);
+
+ alert.getButtonTypes().setAll(confirm, cancel);
+ }
+
+ public boolean displayAwaitResponse() {
+ ButtonType type = alert.showAndWait().orElse(cancel);
+ return type == confirm ? true : false;
+ }
+
+}
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
new file mode 100644
index 0000000..ebf229c
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UICompositor.java
@@ -0,0 +1,109 @@
+package edu.ntnu.idi.idatt.view.components.ui;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.List;
+import java.util.function.Consumer;
+
+import javafx.geometry.Pos;
+import javafx.scene.Parent;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+
+public class UICompositor {
+
+ private final Pane parent;
+
+ public UICompositor(Builder builder) {
+ this.parent = builder.parent;
+ }
+
+ public Parent makeUI() {
+ return parent;
+ }
+
+ public static class Builder {
+ private Pane parent;
+ private Deque wrappers = new ArrayDeque<>();
+
+ public Builder parent(Pane parent) {
+ this.parent = parent;
+ return this;
+ }
+
+ public Builder growWithAlignment(Pos position) {
+ Pane root = getRoot();
+ if (root instanceof HBox) {
+ ((HBox) root).setAlignment(position);
+ root.setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(root, Priority.ALWAYS);
+ }
+ if (root instanceof VBox) {
+ ((VBox) root).setAlignment(position);
+ root.setMaxHeight(Double.MAX_VALUE);
+ VBox.setVgrow(root, Priority.ALWAYS);
+ }
+ return this;
+ }
+
+ private Pane getRoot() {
+ return wrappers.isEmpty() ? parent : wrappers.peek();
+ }
+
+ public Builder addContent(Parent child) {
+ Pane root = getRoot();
+ root.getChildren().add(child);
+ return this;
+ }
+
+ public Builder addAllContent(Parent... children) {
+ Pane root = getRoot();
+ root.getChildren().addAll(List.of(children));
+ return this;
+ }
+
+ public Builder wrap(Pane wrapper) {
+ wrappers.push(wrapper);
+ return this;
+ }
+
+ public Builder unwrap() {
+ Pane current = wrappers.pop();
+
+ if (wrappers.isEmpty()) {
+ parent.getChildren().add(current);
+ } else {
+ wrappers.peek().getChildren().add(current);
+ }
+ return this;
+ }
+
+ public Builder properties(Consumer handle) {
+ handle.accept(getRoot());
+ return this;
+ }
+
+ public Builder filler() {
+ Pane root = getRoot();
+
+ Region filler = new Region();
+ if (root instanceof HBox) {
+ HBox.setHgrow(filler, Priority.ALWAYS);
+ }
+ if (root instanceof VBox) {
+ VBox.setVgrow(filler, Priority.ALWAYS);
+ }
+ root.getChildren().add(filler);
+ return this;
+ }
+
+ public UICompositor build() {
+ return new UICompositor(this);
+ }
+
+ }
+
+}
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
new file mode 100644
index 0000000..2be519b
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIFactory.java
@@ -0,0 +1,198 @@
+package edu.ntnu.idi.idatt.view.components.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import edu.ntnu.idi.idatt.session.UserSession;
+import edu.ntnu.idi.idatt.view.SceneFactory;
+import edu.ntnu.idi.idatt.view.components.elements.IconComponent;
+import edu.ntnu.idi.idatt.view.components.elements.SearchBarComponent;
+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.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Parent;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.MenuButton;
+import javafx.scene.control.MenuItem;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+
+public class UIFactory {
+
+ public static Parent createHeader(String placeholder, Consumer onSearchQuery) {
+ return createDefaultHeader(placeholder, onSearchQuery);
+ }
+
+ public static Parent createHeader(String placeholder, Consumer onSearchQuery, List sortItems,
+ ActionEventHandler... sortHandlers) {
+
+ if (sortItems.size() != sortHandlers.length) {
+ System.out.println("Failed to build header!");
+ return null;
+ }
+
+ MenuButton menu = new MenuButton("Sort by..");
+ HBox.setMargin(menu, new Insets(20));
+
+ sortItems.forEach(item -> {
+ MenuItem m = new MenuItem(item);
+ m.setOnAction((e) -> sortHandlers[sortItems.indexOf(item)].handle());
+ menu.getItems().add(m);
+ });
+
+ return createDefaultHeader(placeholder, onSearchQuery, menu);
+ }
+
+ private static Parent createDefaultHeader(String placeholder, Consumer onSearchQuery, Parent... addons) {
+ SearchBarComponent bar = new SearchBarComponent(placeholder);
+
+ HBox.setMargin(bar, new Insets(20));
+
+ UICompositor header = new UICompositor.Builder()
+ .parent(new HBox())
+ .growWithAlignment(Pos.CENTER)
+ .addAllContent(addons)
+ .filler()
+ .addContent(bar)
+ .filler()
+ .build();
+
+ bar.onSearchQuery(() -> onSearchQuery.accept(bar.getQuery())); // Remake?
+
+ return header.makeUI();
+ }
+
+ public static Parent createNavigation(String title, List buttonLables, ActionEventHandler... handlers) {
+ Label titleLabel = new Label(title);
+ return createDefaultNavigation(titleLabel, buttonLables, handlers);
+ }
+
+ public static Parent createNavigation(Label titleLabel, List buttonLables,
+ ActionEventHandler... handlers) {
+ return createDefaultNavigation(titleLabel, buttonLables, handlers);
+ }
+
+ private static Parent createDefaultNavigation(Label titleLabel, List buttonLables,
+ ActionEventHandler... handlers) {
+ if (buttonLables.size() != handlers.length) {
+ System.out.println("Failed to build navigation!");
+ return null;
+ }
+
+ CssUtils.apply(titleLabel, CssUtils.BIG_TEXT_32);
+
+ ArrayList buttons = new ArrayList<>();
+ for (String buttonLabel : buttonLables) {
+ buttons.add(new Button(buttonLabel));
+ }
+ buttons.forEach(b -> {
+ CssUtils.apply(b, CssUtils.MED_TEXT_16);
+ b.setOnAction((e) -> handlers[buttons.indexOf(b)].handle());
+ });
+
+ UICompositor.Builder navigationBuilder = new UICompositor.Builder()
+ .parent(new VBox())
+ .growWithAlignment(Pos.CENTER)
+ .addContent(titleLabel);
+
+ buttons.forEach(b -> navigationBuilder.addContent(b));
+
+ navigationBuilder.filler();
+
+ if (!SceneFactory.isFinal()) {
+ Button previous = new Button("Previous");
+ previous.setOnAction((e) -> SceneFactory.goBack());
+ navigationBuilder.addContent(previous);
+ }
+
+ UICompositor navigation = navigationBuilder.build();
+
+ return navigation.makeUI();
+
+ }
+
+ public static Parent createMenu(String title, List buttonLables, ActionEventHandler... handlers) {
+ if (buttonLables.size() != handlers.length) {
+ System.out.println("Failed to build menu!");
+ }
+
+ IconComponent menuIcon = new IconComponent(ResourceUtils.MENU_ICON, title, 60);
+ CssUtils.apply(menuIcon, CssUtils.BIG_TEXT_32);
+
+ ArrayList buttons = new ArrayList<>();
+ for (String buttonLabel : buttonLables) {
+ buttons.add(new Button(buttonLabel));
+ }
+ buttons.forEach(b -> {
+ CssUtils.apply(b, CssUtils.MED_TEXT_16);
+ b.setOnAction((e) -> handlers[buttons.indexOf(b)].handle());
+ });
+
+ UICompositor.Builder menuBuilder = new UICompositor.Builder()
+ .parent(new VBox())
+ .growWithAlignment(Pos.CENTER)
+ .addContent(menuIcon);
+
+ buttons.forEach(b -> menuBuilder.addContent(b));
+
+ menuBuilder.filler();
+
+ Button progress = new Button("Advance to next week");
+ progress.setOnAction((e) -> {
+ UserSession.getInstance().getExchange().advance();
+ UserSession.getInstance().updateGameState();
+ SceneFactory.reloadCurrent();
+ });
+
+ menuBuilder.addContent(progress);
+
+ UICompositor menu = menuBuilder.build();
+
+ return menu.makeUI();
+
+ }
+
+ public static Parent createToolbar(ActionEventHandler menuHandle, ActionEventHandler quitHandle) {
+ Label balance = new Label();
+ balance.textProperty().bind(
+ UserSession.getInstance().moneyProperty().asString("Balance: %.2f $"));
+ Label playerStatus = new Label("Status:" + UserSession.getInstance().getPlayer().getStatus());
+ Label playerNetWorth = new Label();
+ playerNetWorth.textProperty().bind(
+ UserSession.getInstance().netWorthProperty().asString("Net Worth: %.2f $"));
+
+ CssUtils.apply(balance, CssUtils.MED_TEXT_16);
+ CssUtils.apply(playerStatus, CssUtils.MED_TEXT_16);
+ CssUtils.apply(playerNetWorth, CssUtils.MED_TEXT_16);
+
+ VBox infoWrapper = new VBox();
+ infoWrapper.setAlignment(Pos.CENTER);
+ infoWrapper.getChildren().addAll(playerStatus, playerNetWorth);
+
+ IconComponent userIcon = new IconComponent(ResourceUtils.MENU_ICON, null, 44);
+ IconComponent quitIcon = new IconComponent(ResourceUtils.QUIT_ICON, null, 44);
+
+ HBox iconWrapper = new HBox();
+ iconWrapper.setAlignment(Pos.CENTER);
+ iconWrapper.getChildren().addAll(userIcon, quitIcon);
+
+ UICompositor toolbar = new UICompositor.Builder()
+ .parent(new HBox())
+ .growWithAlignment(Pos.CENTER)
+ .addContent(balance)
+ .filler()
+ .addAllContent(infoWrapper, iconWrapper)
+ .build();
+
+ userIcon.onIconClick(() -> menuHandle.handle());
+ quitIcon.onIconClick(() -> quitHandle.handle());
+
+ return toolbar.makeUI();
+
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartController.java b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartController.java
new file mode 100644
index 0000000..3767920
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartController.java
@@ -0,0 +1,92 @@
+package edu.ntnu.idi.idatt.view.entry;
+
+import java.io.File;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.List;
+
+import edu.ntnu.idi.idatt.model.Exchange;
+import edu.ntnu.idi.idatt.model.market.Stock;
+import edu.ntnu.idi.idatt.model.player.Player;
+import edu.ntnu.idi.idatt.storage.SessionManager;
+import edu.ntnu.idi.idatt.storage.StockParser;
+import edu.ntnu.idi.idatt.view.SceneFactory;
+import edu.ntnu.idi.idatt.view.SceneManager;
+import edu.ntnu.idi.idatt.view.components.AbstractController;
+import javafx.stage.FileChooser;
+
+public class StartController extends AbstractController {
+
+ private File csv;
+
+ public StartController(StartModel model) {
+ super(model);
+ }
+
+ public void obtainCSVFile() {
+ csv = new FileChooser().showOpenDialog(null);
+ if (csv != null) {
+ model.getFileName().set(csv.getName());
+ }
+ }
+
+ public void initializeGame() {
+ model.getError().set(" "); // Empty buffers
+
+ if (model.getName().get() == null) {
+ model.getError().set("Name field can't be empty");
+ return;
+ }
+
+ if (model.getName().get().isBlank()) {
+ model.getError().set("Name field can't be empty!");
+ return;
+ }
+
+ boolean loadResult = SessionManager.loadSession(model.getName().get());
+ if (loadResult) {
+ SceneManager.switchTo(SceneFactory.createExchangeView());
+ return;
+ } else {
+ model.isNewGame().set(true);
+ }
+
+ if (model.getBalance().get() == null) {
+ model.getError().set("Balance field can't be empty");
+ return;
+ }
+
+ if (model.getBalance().get().isBlank()) {
+ model.getError().set("Balance field can't be empty!");
+ return;
+ }
+
+ BigDecimal balance = null;
+ try {
+ balance = new BigDecimal(model.getBalance().get());
+ } catch (NumberFormatException e) {
+ model.getError().set("The balance must be a number format!");
+ return;
+ }
+
+ if (csv == null) {
+ model.getError().set("Specify the stocks .csv file!");
+ return;
+ }
+
+ List stocks;
+ try {
+ stocks = StockParser.load(csv.getPath());
+ } catch (IOException e) {
+ model.getError().set(e.getMessage());
+ return;
+ }
+
+ Player player = new Player(model.getName().get(), balance);
+ Exchange exchange = new Exchange(player.getName(), stocks);
+ SessionManager.newSession(player, exchange);
+ SessionManager.saveSession();
+ SceneManager.switchTo(SceneFactory.createExchangeView());
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartModel.java b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartModel.java
new file mode 100644
index 0000000..3e4c4ed
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartModel.java
@@ -0,0 +1,38 @@
+package edu.ntnu.idi.idatt.view.entry;
+
+import edu.ntnu.idi.idatt.view.components.Model;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+
+public class StartModel implements Model {
+
+ private final StringProperty name = new SimpleStringProperty();
+ private final StringProperty balance = new SimpleStringProperty();
+ private final StringProperty error = new SimpleStringProperty();
+ private final StringProperty fileName = new SimpleStringProperty();
+
+ private final BooleanProperty newGame = new SimpleBooleanProperty();
+
+ public StringProperty getName() {
+ return name;
+ }
+
+ public StringProperty getBalance() {
+ return balance;
+ }
+
+ public StringProperty getError() {
+ return error;
+ }
+
+ public StringProperty getFileName() {
+ return fileName;
+ }
+
+ public BooleanProperty isNewGame() {
+ return newGame;
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartView.java b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartView.java
new file mode 100644
index 0000000..f32cc87
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartView.java
@@ -0,0 +1,125 @@
+package edu.ntnu.idi.idatt.view.entry;
+
+import java.util.List;
+
+import edu.ntnu.idi.idatt.view.components.AbstractView;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+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.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+
+public class StartView extends AbstractView {
+
+ // Globlal variables for model implementation
+ private TextField nameField;
+ private TextField balanceField;
+ private Label fileLabel;
+ private Label errorLabel;
+
+ // Global buttons for controller implementation
+ private VBox newGameWrapper;
+ private Button csvButton;
+ private Button startButton;
+
+ public StartView() {
+ super(new StackPane());
+ this.getInstance().getStyleClass().add("dark");
+ }
+
+ @Override
+ public Parent createContent() {
+ VBox root = new VBox();
+ root.getStyleClass().add("light");
+ root.setAlignment(Pos.TOP_CENTER);
+ StackPane.setMargin(root, new Insets(40.0));
+
+ Label title = new Label("Millions");
+ title.getStyleClass().add("title");
+
+ // Create and style wrappers
+ VBox wrapper = new VBox();
+ newGameWrapper = new VBox();
+ HBox playerWrapper = new HBox();
+ HBox balanceWrapper = new HBox();
+ playerWrapper.setAlignment(Pos.CENTER_LEFT);
+ balanceWrapper.setAlignment(Pos.CENTER_LEFT);
+
+ // Player name label and input field
+ Label playerName = new Label("Player name:");
+ playerName.getStyleClass().add("big-text-32");
+
+ nameField = new TextField();
+ nameField.setPromptText("Leonardo Dicaprio");
+ nameField.setFocusTraversable(false);
+ nameField.getStyleClass().add("button");
+ playerWrapper.getChildren().addAll(List.of(playerName, nameField));
+
+ // Start balance label and input field
+ Label startBalance = new Label("Start balance:");
+ startBalance.getStyleClass().add("big-text-32");
+
+ balanceField = new TextField();
+ balanceField.setPromptText("2500");
+ balanceField.setFocusTraversable(false);
+ balanceField.getStyleClass().add("button");
+ balanceWrapper.getChildren().addAll(List.of(startBalance, balanceField));
+
+ // CSV import button and label
+
+ HBox csvWrapper = new HBox();
+ csvWrapper.setAlignment(Pos.CENTER_LEFT);
+
+ csvButton = new Button("Import CSV");
+ csvButton.getStyleClass().add("button");
+
+ fileLabel = new Label();
+ fileLabel.getStyleClass().add("med-text-16");
+
+ csvWrapper.getChildren().addAll(csvButton, fileLabel);
+ VBox.setMargin(csvWrapper, new Insets(0.0, 0.0, 0.0, 50.0));
+
+ // Start game button and error log
+ startButton = new Button("Start Game");
+ startButton.getStyleClass().add("button");
+ startButton.setMaxWidth(200);
+ VBox.setMargin(startButton, new Insets(40.0));
+
+ errorLabel = new Label();
+ errorLabel.getStyleClass().add("med-text-16");
+ errorLabel.setStyle("-fx-text-fill: red;");
+
+ // Region filler
+ Region filler = new Region();
+ VBox.setVgrow(filler, Priority.ALWAYS);
+
+ // Wrap components together and adjust positioning
+ newGameWrapper.getChildren().addAll(balanceWrapper, csvWrapper);
+ wrapper.getChildren().addAll(playerWrapper, newGameWrapper);
+ wrapper.setAlignment(Pos.BASELINE_LEFT);
+
+ root.getChildren().addAll(List.of(title, wrapper, filler, errorLabel, startButton));
+
+ return root;
+ }
+
+ public void setModel(StartModel model) {
+ this.nameField.textProperty().bindBidirectional(model.getName());
+ this.balanceField.textProperty().bindBidirectional(model.getBalance());
+ this.errorLabel.textProperty().bind(model.getError());
+ this.fileLabel.textProperty().bind(model.getFileName());
+ this.newGameWrapper.visibleProperty().bind(model.isNewGame());
+ }
+
+ public void setController(StartController controller) {
+ this.csvButton.setOnAction(e -> controller.obtainCSVFile());
+ this.startButton.setOnAction(e -> controller.initializeGame());
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/exchange/ExchangeController.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/exchange/ExchangeController.java
new file mode 100644
index 0000000..8528815
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/exchange/ExchangeController.java
@@ -0,0 +1,83 @@
+package edu.ntnu.idi.idatt.view.primary.exchange;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import edu.ntnu.idi.idatt.model.market.Stock;
+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.AbstractController;
+import edu.ntnu.idi.idatt.view.components.elements.StockComponent;
+
+public class ExchangeController extends AbstractController {
+
+ public enum SortAction {
+ NONE,
+ GAINERS,
+ LOSERS
+ }
+
+ private UserSession session = UserSession.getInstance();
+ private ArrayList stocksSorted = new ArrayList<>();
+
+ public ExchangeController(ExchangeModel model) {
+ super(model);
+ List initialStocksLoad = session.getExchange().getStocks();
+ stocksSorted.addAll(initialStocksLoad);
+ setStocksModel(initialStocksLoad);
+ }
+
+ public void setStocksModel(List stockList) {
+ model.getStockList().clear();
+ for (Stock stock : stockList) {
+ StockComponent stockComponent = new StockComponent(stock);
+ stockComponent.onStockClick((symbol) -> redirectView(symbol));
+ model.getStockList().add(stockComponent);
+ }
+ }
+
+ public void handleSearchQuery(String query) {
+ if (query == null || query.isBlank()) {
+ setStocksModel(stocksSorted); // Get back to "no search"
+ return;
+ }
+
+ List stocksFound = stocksSorted.stream()
+ .filter(stock -> stock.toString().contains(query)).toList();
+
+ setStocksModel(stocksFound);
+
+ }
+
+ public void sortStocksBy(SortAction action) {
+ stocksSorted.clear();
+
+ switch (action) {
+
+ case NONE: {
+ stocksSorted.addAll(session.getExchange().getStocks());
+ break;
+ }
+
+ case GAINERS: {
+ stocksSorted.addAll(session.getExchange().getGainers(Integer.MAX_VALUE)); // TODO: fix?
+ break;
+ }
+
+ case LOSERS: {
+ stocksSorted.addAll(session.getExchange().getLosers(Integer.MAX_VALUE));
+ break;
+ }
+
+ }
+
+ setStocksModel(stocksSorted);
+
+ }
+
+ public void redirectView(String symbol) {
+ SceneManager.switchTo(SceneFactory.createStockView(symbol));
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/exchange/ExchangeModel.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/exchange/ExchangeModel.java
new file mode 100644
index 0000000..4495ea9
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/exchange/ExchangeModel.java
@@ -0,0 +1,16 @@
+package edu.ntnu.idi.idatt.view.primary.exchange;
+
+import edu.ntnu.idi.idatt.view.components.Model;
+import edu.ntnu.idi.idatt.view.components.elements.StockComponent;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+public class ExchangeModel implements Model {
+
+ private final ObservableList stockList = FXCollections.observableArrayList();
+
+ public ObservableList getStockList() {
+ return stockList;
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/exchange/ExchangeView.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/exchange/ExchangeView.java
new file mode 100644
index 0000000..e69a94e
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/exchange/ExchangeView.java
@@ -0,0 +1,89 @@
+package edu.ntnu.idi.idatt.view.primary.exchange;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+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;
+import edu.ntnu.idi.idatt.view.components.ui.UIFactory;
+import edu.ntnu.idi.idatt.view.primary.exchange.ExchangeController.SortAction;
+import javafx.beans.binding.Bindings;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Parent;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+
+public class ExchangeView extends AbstractViewUI {
+
+ Consumer searchQueryHandler;
+ Consumer sortHandle;
+ private VBox root;
+
+ @Override
+ public Parent createContent() {
+ root = new VBox();
+ root.getStyleClass().add("primary");
+ VBox.setVgrow(root, Priority.ALWAYS);
+ root.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+ root.setAlignment(Pos.CENTER);
+ root.setSpacing(20.0);
+
+ ScrollPane wrapper = new ScrollPane();
+ wrapper.setContent(root);
+ wrapper.setPadding(new Insets(0, 20.0, 0, 20.0));
+ wrapper.setFitToWidth(true);
+ wrapper.setFitToHeight(true);
+ wrapper.getStyleClass().add("primary");
+
+ return wrapper;
+ }
+
+ @Override
+ public Parent createNavigation() {
+ return UIFactory.createNavigation(UserSession.getInstance().getExchange().getName(),
+ List.of(" • Newspaper"),
+ () -> SceneManager.switchTo(SceneFactory.createNewspaperView()));
+ }
+
+ @Override
+ public Parent createHeader() {
+ return UIFactory.createHeader("Search for stocks",
+ query -> searchQueryHandler.accept(query),
+ List.of(
+ "None",
+ "Gainers (week)",
+ "Losers (week)"),
+ () -> sortHandle.accept(SortAction.NONE),
+ () -> sortHandle.accept(SortAction.GAINERS),
+ () -> sortHandle.accept(SortAction.LOSERS));
+ }
+
+ @Override
+ public Parent createToolbar() {
+ return UIFactory.createToolbar(() -> this.toggleMenu(),
+ () -> SceneManager.switchTo(SceneFactory.createStartView()));
+ }
+
+ @Override
+ public Parent createMenu() {
+ return UIFactory.createMenu("Account",
+ List.of(" • Portfolio", " • Transactions"),
+ () -> SceneManager.switchTo(SceneFactory.createPortfolioView()),
+ () -> SceneManager.switchTo(SceneFactory.createTransactionView()));
+
+ }
+
+ public void setModel(ExchangeModel model) {
+ Bindings.bindContent(root.getChildren(), model.getStockList());
+ }
+
+ public void setController(ExchangeController controller) {
+ searchQueryHandler = (query) -> controller.handleSearchQuery(query);
+ sortHandle = (sortAction) -> controller.sortStocksBy(sortAction);
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/newspaper/NewspaperController.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/newspaper/NewspaperController.java
new file mode 100644
index 0000000..674758a
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/newspaper/NewspaperController.java
@@ -0,0 +1,24 @@
+package edu.ntnu.idi.idatt.view.primary.newspaper;
+
+import edu.ntnu.idi.idatt.model.portfolio.Share;
+import edu.ntnu.idi.idatt.session.UserSession;
+import edu.ntnu.idi.idatt.view.components.AbstractController;
+import edu.ntnu.idi.idatt.view.components.elements.NewspaperComponent;
+
+import java.util.ArrayList;
+
+public class NewspaperController extends AbstractController {
+
+ private final ArrayList loadedNewspaperComponents = new ArrayList<>();
+ private UserSession session = UserSession.getInstance();
+
+ public NewspaperController(NewspaperModel model) {
+ super(model);
+ for (Share share : session.getPlayer().getPortfolio().getShares()) {
+ NewspaperComponent newspaperComponent = new NewspaperComponent(share);
+ loadedNewspaperComponents.add(newspaperComponent);
+ }
+ model.getNewspaperComponentsList().setAll(loadedNewspaperComponents);
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/newspaper/NewspaperModel.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/newspaper/NewspaperModel.java
new file mode 100644
index 0000000..090db9b
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/newspaper/NewspaperModel.java
@@ -0,0 +1,14 @@
+package edu.ntnu.idi.idatt.view.primary.newspaper;
+
+import edu.ntnu.idi.idatt.view.components.Model;
+import edu.ntnu.idi.idatt.view.components.elements.NewspaperComponent;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+public class NewspaperModel implements Model {
+ private final ObservableList newspaperComponentsList = FXCollections.observableArrayList();
+
+ public ObservableList getNewspaperComponentsList() {
+ return newspaperComponentsList;
+ }
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/newspaper/NewspaperView.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/newspaper/NewspaperView.java
new file mode 100644
index 0000000..ab1e29b
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/newspaper/NewspaperView.java
@@ -0,0 +1,61 @@
+package edu.ntnu.idi.idatt.view.primary.newspaper;
+
+import edu.ntnu.idi.idatt.view.SceneFactory;
+import edu.ntnu.idi.idatt.view.SceneManager;
+import edu.ntnu.idi.idatt.view.components.AbstractViewUI;
+import edu.ntnu.idi.idatt.view.components.ui.UIFactory;
+import javafx.beans.binding.Bindings;
+import javafx.geometry.Pos;
+import javafx.scene.Parent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+
+import java.util.List;
+
+public class NewspaperView extends AbstractViewUI {
+ private VBox article;
+ @Override
+ public Parent createContent() {
+ BorderPane root = new BorderPane();
+ root.getStyleClass().add("primary");
+ article = new VBox();
+ article.setFillWidth(false);
+ article.setAlignment(Pos.TOP_CENTER);
+ root.setCenter(article);
+ return root;
+ }
+
+ @Override
+ public Parent createNavigation() {
+ return UIFactory.createNavigation(" Newspaper",
+ List.of(" • Newspaper"),
+ () -> SceneManager.switchTo(SceneFactory.createNewspaperView()));
+ }
+
+ @Override
+ public Parent createHeader() {
+ return UIFactory.createHeader("Search",
+ query -> System.out.println());
+ }
+
+ @Override
+ public Parent createToolbar() {
+ return UIFactory.createToolbar(() -> this.toggleMenu(),
+ () -> SceneManager.switchTo(SceneFactory.createStartView()));
+ }
+
+ @Override
+ public Parent createMenu() {
+ return UIFactory.createMenu("Account",
+ List.of(" • Portfolio", " • Transactions"),
+ () -> SceneManager.switchTo(SceneFactory.createPortfolioView()),
+ () -> SceneManager.switchTo(SceneFactory.createTransactionView()));
+ }
+
+ public void setModel(NewspaperModel model){
+ Bindings.bindContent(article.getChildren(), model.getNewspaperComponentsList());
+ }
+ public void setController(NewspaperController controller){
+
+ }
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/PortfolioController.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/PortfolioController.java
new file mode 100644
index 0000000..051228f
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/PortfolioController.java
@@ -0,0 +1,140 @@
+package edu.ntnu.idi.idatt.view.primary.portfolio;
+
+import edu.ntnu.idi.idatt.model.portfolio.Portfolio;
+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.components.AbstractController;
+import edu.ntnu.idi.idatt.view.components.elements.ShareComponent;
+import edu.ntnu.idi.idatt.view.components.ui.UIAlert;
+import edu.ntnu.idi.idatt.view.util.CssUtils;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+public class PortfolioController extends AbstractController {
+
+ public enum SortAction {
+ OLDEST,
+ NEWEST,
+ PROFIT,
+ LOSS
+ }
+
+ private final ArrayList sharesSorted = new ArrayList<>();
+ private UserSession session = UserSession.getInstance();
+
+ public PortfolioController(PortfolioModel model) {
+ super(model);
+ List initialSharesLoad = session.getPlayer().getPortfolio().getShares();
+ sharesSorted.addAll(initialSharesLoad);
+ setShareModel(initialSharesLoad);
+
+ initController();
+ }
+
+ public void setShareModel(List shareList) {
+ model.getShareList().clear();
+ for (Share share : shareList) {
+ ShareComponent shareComponent = new ShareComponent(share);
+ shareComponent.onShareSellButton((sellShare) -> holdingSale(sellShare));
+ model.getShareList().add(shareComponent);
+ }
+ }
+
+ public void initController() {
+
+ setupPlayerInfo();
+
+ }
+
+ public void holdingSale(Share share) {
+
+ UIAlert sellConfirmation = new UIAlert("Confirmation required",
+ "Do you wish to sell this holding?",
+ String.format("Selling %.2f [%s] for %.2f $",
+ share.getQuantity(), share.getStock().getSymbol(), new SaleCalculator(share).calculateTotal()));
+
+ boolean result = sellConfirmation.displayAwaitResponse();
+
+ if (!result) {
+ return;
+ }
+
+ session.getExchange().sell(share, session.getPlayer());
+ session.updateGameState();
+ setupPlayerInfo(); // Reload player information section
+ sortSharesBy(SortAction.OLDEST); // Reset loaded shares.
+
+ }
+
+ public void setupPlayerInfo() {
+ Portfolio portfolio = session.getPlayer().getPortfolio();
+
+ String title = String.format("%s's Portfolio", session.getPlayer().getName());
+ model.playerInfoModel().getTitle().set(title);
+
+ String netWorth = String.format("Portfolio net worth: %.2f $", portfolio.getNetWorth());
+ model.playerInfoModel().getNetWorth().set(netWorth);
+
+ BigDecimal totalNetWorthChangeValue = portfolio.getChangeFromStock();
+ model.playerInfoModel().getNetWorthTotalChange().set(totalNetWorthChangeValue.toPlainString() + " %");
+ model.playerInfoModel().getNetWorthTotalChangeColor().set(CssUtils.generateValueColors(totalNetWorthChangeValue));
+
+ String playerStatus = String.format("Player status: %s", session.getPlayer().getStatus());
+ model.playerInfoModel().getPlayerStatus().set(playerStatus);
+
+ String ownedShares = String.format("Total shares owned: %.2f", portfolio.getOwnedAmount());
+ model.playerInfoModel().getTotalSharesOwned().set(ownedShares);
+ }
+
+ public void handleSearchQuery(String query) {
+ if (query == null || query.isBlank()) {
+ setShareModel(sharesSorted); // Get back to "no search"
+ return;
+ }
+
+ List sharesFound = sharesSorted.stream()
+ .filter(share -> share.getStock().toString().contains(query)).toList();
+
+ setShareModel(sharesFound);
+
+ }
+
+ public void sortSharesBy(SortAction action) {
+ sharesSorted.clear();
+ sharesSorted.addAll(session.getPlayer().getPortfolio().getShares());
+
+ switch (action) {
+
+ case OLDEST: {
+ break;
+ }
+
+ case NEWEST: {
+ List reversed = new ArrayList<>(sharesSorted.reversed());
+ sharesSorted.clear();
+ sharesSorted.addAll(reversed);
+ break;
+ }
+
+ case PROFIT: {
+ sharesSorted.sort(Comparator.comparing(Share::getProfit).reversed());
+ break;
+ }
+
+ case LOSS: {
+
+ sharesSorted.sort(Comparator.comparing(Share::getProfit));
+ break;
+ }
+
+ }
+
+ setShareModel(sharesSorted);
+
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/PortfolioModel.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/PortfolioModel.java
new file mode 100644
index 0000000..36f0dd3
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/PortfolioModel.java
@@ -0,0 +1,22 @@
+package edu.ntnu.idi.idatt.view.primary.portfolio;
+
+import edu.ntnu.idi.idatt.view.components.Model;
+import edu.ntnu.idi.idatt.view.components.elements.ShareComponent;
+import edu.ntnu.idi.idatt.view.primary.portfolio.viewmodel.PlayerInfoModel;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+public class PortfolioModel implements Model {
+
+ private final ObservableList shareList = FXCollections.observableArrayList();
+ private final PlayerInfoModel playerInfoModel = new PlayerInfoModel();
+
+ public ObservableList getShareList() {
+ return shareList;
+ }
+
+ public PlayerInfoModel playerInfoModel() {
+ return playerInfoModel;
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/PortfolioView.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/PortfolioView.java
new file mode 100644
index 0000000..f2c1ba1
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/PortfolioView.java
@@ -0,0 +1,104 @@
+package edu.ntnu.idi.idatt.view.primary.portfolio;
+
+import edu.ntnu.idi.idatt.view.SceneFactory;
+import edu.ntnu.idi.idatt.view.SceneManager;
+import edu.ntnu.idi.idatt.view.components.AbstractViewUI;
+import edu.ntnu.idi.idatt.view.components.ui.UIFactory;
+import edu.ntnu.idi.idatt.view.primary.portfolio.PortfolioController.SortAction;
+import edu.ntnu.idi.idatt.view.primary.portfolio.sections.PlayerInfoSection;
+import edu.ntnu.idi.idatt.view.primary.portfolio.viewmodel.PlayerInfoModel;
+import javafx.beans.binding.Bindings;
+import javafx.geometry.Insets;
+import javafx.scene.Parent;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.layout.*;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+public class PortfolioView extends AbstractViewUI {
+
+ Consumer searchQueryHandler;
+ Consumer sortHandle;
+
+ private VBox shareList;
+ private PlayerInfoSection playerInfoSection;
+
+ @Override
+ public Parent createContent() {
+ BorderPane root = new BorderPane();
+ root.getStyleClass().add("primary");
+
+ ScrollPane scroll = new ScrollPane();
+ scroll.setPadding(new Insets(0, 20, 0, 20));
+
+ scroll.setFitToWidth(true);
+ scroll.getStyleClass().add("viewport-colour");
+ scroll.getStyleClass().add("primary");
+ scroll.setContent(shareList = new VBox(20));
+
+ root.setTop(playerInfoSection = new PlayerInfoSection());
+ root.setCenter(scroll);
+ return root;
+ }
+
+ @Override
+ public Parent createNavigation() {
+ return UIFactory.createNavigation("Portfolio",
+ List.of(" • Title"),
+ () -> System.out.println("Newspaper clicked"));
+ }
+
+ @Override
+ public Parent createHeader() {
+ return UIFactory.createHeader("Search after holdings..",
+ query -> searchQueryHandler.accept(query),
+ List.of(
+ "Oldest first",
+ "Newest first",
+ "Highest profit",
+ "Highest loss"),
+ () -> sortHandle.accept(SortAction.OLDEST),
+ () -> sortHandle.accept(SortAction.NEWEST),
+ () -> sortHandle.accept(SortAction.PROFIT),
+ () -> sortHandle.accept(SortAction.LOSS));
+ }
+
+ @Override
+ public Parent createToolbar() {
+ return UIFactory.createToolbar(() -> this.toggleMenu(),
+ () -> SceneManager.switchTo(SceneFactory.createStartView()));
+ }
+
+ @Override
+ public Parent createMenu() {
+ return UIFactory.createMenu("Account",
+ List.of(" • Portfolio", " • Transactions"),
+ () -> {
+ },
+ () -> SceneManager.switchTo(SceneFactory.createTransactionView()));
+ }
+
+ public void setModel(PortfolioModel model) {
+ Bindings.bindContent(shareList.getChildren(), model.getShareList());
+
+ setPlayerInfoSectionModel(model.playerInfoModel());
+ }
+
+ public void setPlayerInfoSectionModel(PlayerInfoModel model) {
+ playerInfoSection.getTitle().textProperty().bind(model.getTitle());
+ playerInfoSection.getNetWorth().textProperty().bind(model.getNetWorth());
+
+ playerInfoSection.getNetWorthTotalChange().valueProperty().bind(model.getNetWorthTotalChange());
+ playerInfoSection.getNetWorthTotalChange().colorProperty().bind(model.getNetWorthTotalChangeColor());
+
+ playerInfoSection.getPlayerStatus().textProperty().bind(model.getPlayerStatus());
+ playerInfoSection.getTotalSharesOwned().textProperty().bind(model.getTotalSharesOwned());
+ }
+
+ public void setController(PortfolioController controller) {
+ searchQueryHandler = (query) -> controller.handleSearchQuery(query);
+ sortHandle = (sortAction) -> controller.sortSharesBy(sortAction);
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/sections/PlayerInfoSection.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/sections/PlayerInfoSection.java
new file mode 100644
index 0000000..8546969
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/sections/PlayerInfoSection.java
@@ -0,0 +1,75 @@
+package edu.ntnu.idi.idatt.view.primary.portfolio.sections;
+
+import java.util.List;
+
+import edu.ntnu.idi.idatt.view.components.elements.TextValueComponent;
+import edu.ntnu.idi.idatt.view.components.ui.UICompositor;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+
+public class PlayerInfoSection extends VBox {
+
+ Label title = new Label();
+ Label netWorth = new Label();
+ TextValueComponent netWorthTotalChange = new TextValueComponent("Total Net Worth change: ");
+ Label playerStatus = new Label();
+ Label totalSharesOwned = new Label();
+
+ public PlayerInfoSection() {
+
+ title.getStyleClass().add("portfolio-box-title");
+
+ List labels = List.of(netWorth, playerStatus, totalSharesOwned);
+ labels.forEach(l -> l.getStyleClass().add("portfolio-box-text"));
+
+ new UICompositor.Builder()
+ .parent(this)
+ .properties((parent) -> {
+ VBox.setVgrow(parent, Priority.ALWAYS);
+ parent.setPadding(new Insets(20));
+ })
+ .wrap(new HBox())
+ .properties((parent) -> {
+ parent.setMaxSize(Double.MAX_VALUE, 500);
+ parent.getStyleClass().add("light");
+ parent.setPadding(new Insets(20));
+ })
+ .growWithAlignment(Pos.CENTER)
+ .wrap(new VBox())
+ .addAllContent(title, netWorth, netWorthTotalChange)
+ .unwrap()
+ .filler()
+ .wrap(new VBox())
+ .growWithAlignment(Pos.BOTTOM_CENTER)
+ .addAllContent(playerStatus, totalSharesOwned)
+ .unwrap()
+ .unwrap()
+ .build();
+
+ }
+
+ public Label getTitle() {
+ return title;
+ }
+
+ public Label getNetWorth() {
+ return netWorth;
+ }
+
+ public TextValueComponent getNetWorthTotalChange() {
+ return netWorthTotalChange;
+ }
+
+ public Label getPlayerStatus() {
+ return playerStatus;
+ }
+
+ public Label getTotalSharesOwned() {
+ return totalSharesOwned;
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/viewmodel/PlayerInfoModel.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/viewmodel/PlayerInfoModel.java
new file mode 100644
index 0000000..6afc3e3
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/portfolio/viewmodel/PlayerInfoModel.java
@@ -0,0 +1,40 @@
+package edu.ntnu.idi.idatt.view.primary.portfolio.viewmodel;
+
+import javafx.beans.property.SimpleStringProperty;
+
+public class PlayerInfoModel {
+
+ private final SimpleStringProperty title = new SimpleStringProperty();
+ private final SimpleStringProperty netWorth = new SimpleStringProperty();
+
+ private final SimpleStringProperty netWorthTotalChange = new SimpleStringProperty();
+ private final SimpleStringProperty netWorthTotalChangeColor = new SimpleStringProperty();
+
+ private final SimpleStringProperty playerStatus = new SimpleStringProperty();
+ private final SimpleStringProperty totalSharesOwned = new SimpleStringProperty();
+
+ public SimpleStringProperty getTitle() {
+ return title;
+ }
+
+ public SimpleStringProperty getNetWorth() {
+ return netWorth;
+ }
+
+ public SimpleStringProperty getNetWorthTotalChange() {
+ return netWorthTotalChange;
+ }
+
+ public SimpleStringProperty getNetWorthTotalChangeColor() {
+ return netWorthTotalChangeColor;
+ }
+
+ public SimpleStringProperty getPlayerStatus() {
+ return playerStatus;
+ }
+
+ public SimpleStringProperty getTotalSharesOwned() {
+ return totalSharesOwned;
+ }
+
+}
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
new file mode 100644
index 0000000..fbda060
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockController.java
@@ -0,0 +1,204 @@
+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.components.ui.UIAlert;
+import edu.ntnu.idi.idatt.view.util.CssUtils;
+import javafx.scene.chart.LineChart;
+import javafx.scene.chart.NumberAxis;
+import javafx.scene.chart.XYChart;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+
+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;
+
+ initController();
+ }
+
+ private void initController() {
+ renderGraph();
+ setCurrentPrice();
+ setLatestChange();
+ setAllTimeScore();
+ setOwnedAmount();
+ setTotalProfits();
+
+ initHooks();
+ }
+
+ public String getSymbol() {
+ return this.stock.getSymbol();
+ }
+
+ public void renderGraph() {
+ NumberAxis xAxis = new NumberAxis();
+ NumberAxis yAxis = new NumberAxis();
+
+ xAxis.setLabel("Week");
+ yAxis.setLabel("Price [USD]");
+
+ LineChart lineChart = new LineChart<>(xAxis, yAxis);
+ lineChart.setTitle(stock.getCompany() + " (" + stock.getSymbol() + ") - Prices");
+ XYChart.Series series = new XYChart.Series<>();
+
+ for (int i = 0; i < stock.getHistoricalPrices().size(); i++) {
+ series.getData()
+ .add(new XYChart.Data(i, stock.getHistoricalPrices().get(i).intValue()));
+ }
+
+ lineChart.getData().add(series);
+ lineChart.setLegendVisible(false);
+ lineChart.setCreateSymbols(false);
+ HBox.setHgrow(lineChart, Priority.ALWAYS);
+ model.getGraphNodes().setAll(lineChart);
+ }
+
+ private void resetDisplayBuffers() {
+ model.tradeModel().getBuyPrice().set("0 $");
+ model.tradeModel().getBuyCost().set("0 $");
+ model.tradeModel().getTotalPrice().set("0 $");
+ model.tradeModel().getResultMessage().set("");
+ model.tradeModel().getResultMessageColorProperty().set(CssUtils.RED);
+ }
+
+ private void initHooks() {
+ resetDisplayBuffers();
+ model.tradeModel().getBuyInputField().addListener((obervable, oldVal, newVal) -> displayBuyInfo(newVal));
+ }
+
+ public void buyButtonClicked() {
+ if (share == null || purchaseCalculator == null) {
+ resetDisplayBuffers(); // Flush after color change and new press.
+ model.tradeModel().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.tradeModel().getResultMessageColorProperty().set(CssUtils.RED);
+ model.tradeModel().getResultMessage().set("Balance too low.");
+ return;
+ }
+
+ UIAlert buyConfirmation = new UIAlert("Confirmation required",
+ "Do you wish to proceed the purchase?",
+ String.format("Purchasing %.2f [%s] for %.2f $",
+ share.getQuantity(), share.getStock().getSymbol(), purchaseCalculator.calculateTotal()));
+
+ boolean result = buyConfirmation.displayAwaitResponse();
+
+ if (!result) {
+ model.tradeModel().getResultMessageColorProperty().set(CssUtils.RED);
+ model.tradeModel().getResultMessage().set("Transaction canceled.");
+ return;
+ }
+
+ session.getExchange().buy(share.getStock().getSymbol(), share.getQuantity(), session.getPlayer());
+ this.setOwnedAmount();
+ this.setTotalProfits();
+ model.tradeModel().getResultMessageColorProperty().set(CssUtils.GREEN);
+ model.tradeModel().getResultMessage().set("Purchase completed!");
+ session.updateGameState();
+ }
+
+ 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.tradeModel().getResultMessage().set("Only numbers allowed!");
+ return;
+ }
+
+ if (value.compareTo(BigDecimal.ZERO) <= 0) {
+ model.tradeModel().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);
+
+ String gross = String.format("%s $", formatter.format(purchaseCalculator.calculateGross()));
+ String cost = String.format("%s $",
+ formatter.format(purchaseCalculator.calculateCommision().add(purchaseCalculator.calculateTax())));
+ String total = String.format("%s $", formatter.format(purchaseCalculator.calculateTotal()));
+
+ model.tradeModel().getBuyPrice().set(gross);
+ model.tradeModel().getBuyCost().set(cost);
+ model.tradeModel().getTotalPrice().set(total);
+ }
+
+ public void setCurrentPrice() {
+ model.priceInfoModel().getStockPrice().set(String.format("%.2f $", this.stock.getSalesPrice()));
+ }
+
+ public void setLatestChange() {
+ BigDecimal change = this.stock.getLatestPriceChange();
+ BigDecimal changePercent = this.stock.getLatestPriceChangePercent();
+
+ String format = String.format("%.2f $ ( %.2f %%)", change, changePercent);
+
+ model.priceInfoModel().getLatestChange().set(format);
+ model.priceInfoModel().getLatestChangeColorProperty().set(CssUtils.generateValueColors(change.doubleValue()));
+ }
+
+ public void setAllTimeScore() {
+ BigDecimal highest = this.stock.getHighestPrice();
+ BigDecimal lowest = this.stock.getLowestPrice();
+
+ String format = String.format("%.2f / %.2f", highest, lowest);
+
+ model.priceInfoModel().getAllTimeScore().set(format);
+ }
+
+ public void setOwnedAmount() {
+ BigDecimal ownedAmount = session.getPlayer().getPortfolio().getOwnedAmount(
+ this.stock.getSymbol());
+ String symbol = this.stock.getSymbol();
+
+ String format = String.format("%.2f [%s]", ownedAmount, symbol);
+
+ model.priceInfoModel().getOwnedAmount().set(format);
+ }
+
+ public void setTotalProfits() {
+ BigDecimal profit = session.getPlayer().getPortfolio().getProfitFromStock(
+ this.stock.getSymbol());
+ BigDecimal profitPercent = session.getPlayer().getPortfolio().getChangeFromStock(
+ this.stock.getSymbol());
+
+ String format = String.format("%.2f $ (%.2f %%)", profit, profitPercent);
+
+ model.priceInfoModel().getTotalProfits().set(format);
+ model.priceInfoModel().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
new file mode 100644
index 0000000..6890812
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockModel.java
@@ -0,0 +1,33 @@
+package edu.ntnu.idi.idatt.view.primary.stock;
+
+import edu.ntnu.idi.idatt.view.components.Model;
+import edu.ntnu.idi.idatt.view.primary.stock.viewmodel.PriceInfoModel;
+import edu.ntnu.idi.idatt.view.primary.stock.viewmodel.TradeModel;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+
+public class StockModel implements Model {
+
+ // Graph
+ private final ObservableList graphNodes = FXCollections.observableArrayList();
+
+ // Price properties
+ private final PriceInfoModel priceInfoModel = new PriceInfoModel();
+
+ // Trade properties
+ private final TradeModel tradeModel = new TradeModel();
+
+ public ObservableList getGraphNodes() {
+ return this.graphNodes;
+ }
+
+ public PriceInfoModel priceInfoModel() {
+ return priceInfoModel;
+ }
+
+ public TradeModel tradeModel() {
+ return tradeModel;
+ }
+
+}
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
new file mode 100644
index 0000000..fe2da56
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/StockView.java
@@ -0,0 +1,128 @@
+package edu.ntnu.idi.idatt.view.primary.stock;
+
+import java.util.List;
+import edu.ntnu.idi.idatt.view.SceneFactory;
+import edu.ntnu.idi.idatt.view.SceneManager;
+import edu.ntnu.idi.idatt.view.components.AbstractViewUI;
+import edu.ntnu.idi.idatt.view.components.ui.UICompositor;
+import edu.ntnu.idi.idatt.view.components.ui.UIFactory;
+import edu.ntnu.idi.idatt.view.primary.stock.sections.PriceInfoSection;
+import edu.ntnu.idi.idatt.view.primary.stock.sections.TradeSection;
+import edu.ntnu.idi.idatt.view.primary.stock.viewmodel.PriceInfoModel;
+import edu.ntnu.idi.idatt.view.primary.stock.viewmodel.TradeModel;
+import edu.ntnu.idi.idatt.view.util.CssUtils;
+import javafx.beans.binding.Bindings;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Parent;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+
+public class StockView extends AbstractViewUI {
+
+ private HBox graphContainer;
+ private Label title;
+
+ // Prices
+ private PriceInfoSection priceInfoSection;
+
+ // Trade menu
+ private TradeSection tradeSection;
+
+ @Override
+ public Parent createContent() {
+ VBox root = new VBox();
+ root.getStyleClass().add("primary");
+ root.setAlignment(Pos.TOP_LEFT);
+ root.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+
+ 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)))
+ .addContent(priceInfoSection = new PriceInfoSection())
+ .filler()
+ .addContent(tradeSection = new TradeSection())
+ .build();
+
+ root.getChildren().addAll(graphContainer, userSection.makeUI());
+ return root;
+ }
+
+ @Override
+ public Parent createNavigation() {
+ return UIFactory.createNavigation(title = new Label(),
+ List.of(" • Newspaper"),
+ () -> System.out.println("Newspaper clicked"));
+ }
+
+ @Override
+ public Parent createHeader() {
+ return new HBox();
+ }
+
+ @Override
+ public Parent createToolbar() {
+ return UIFactory.createToolbar(() -> this.toggleMenu(),
+ () -> SceneManager.switchTo(SceneFactory.createStartView()));
+ }
+
+ @Override
+ public Parent createMenu() {
+ return UIFactory.createMenu("Account",
+ List.of(" • Portfolio", " • Transactions"),
+ () -> SceneManager.switchTo(SceneFactory.createPortfolioView()),
+ () -> SceneManager.switchTo(SceneFactory.createTransactionView()));
+
+ }
+
+ public void setModel(StockModel model) {
+ // Graph
+ Bindings.bindContent(graphContainer.getChildren(), model.getGraphNodes());
+
+ // Price display bindings
+ setPriceInfoSectionModel(model.priceInfoModel());
+
+ // Trade section bindings
+ setTradeSectionModel(model.tradeModel());
+
+ }
+
+ public void setPriceInfoSectionModel(PriceInfoModel model) {
+ priceInfoSection.getStockPrice().valueProperty().bind(model.getStockPrice());
+
+ priceInfoSection.getLatestChange().valueProperty().bind(model.getLatestChange());
+ priceInfoSection.getLatestChange().colorProperty().bind(model.getLatestChangeColorProperty());
+
+ priceInfoSection.getAllTimeScore().valueProperty().bind(model.getAllTimeScore());
+
+ priceInfoSection.getOwnedAmount().valueProperty().bind(model.getOwnedAmount());
+
+ priceInfoSection.getTotalProfits().valueProperty().bind(model.getTotalProfits());
+ priceInfoSection.getTotalProfits().colorProperty().bind(model.getTotalProfitsColorProperty());
+ }
+
+ public void setTradeSectionModel(TradeModel model) {
+ tradeSection.getBuyInputField().textProperty().bindBidirectional(model.getBuyInputField());
+ tradeSection.getBuyPrice().valueProperty().bind(model.getBuyPrice());
+ tradeSection.getBuyCost().valueProperty().bind(model.getBuyCost());
+ tradeSection.getTotalPrice().valueProperty().bind(model.getTotalPrice());
+
+ tradeSection.getResultMessage().valueProperty().bind(model.getResultMessage());
+ tradeSection.getResultMessage().colorProperty().bind(model.getResultMessageColorProperty());
+ }
+
+ public void setController(StockController controller) {
+ title.setText(controller.getSymbol());
+ tradeSection.getBuyButton().setOnAction((e) -> controller.buyButtonClicked());
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/sections/PriceInfoSection.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/sections/PriceInfoSection.java
new file mode 100644
index 0000000..d375fde
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/sections/PriceInfoSection.java
@@ -0,0 +1,38 @@
+package edu.ntnu.idi.idatt.view.primary.stock.sections;
+
+import edu.ntnu.idi.idatt.view.components.elements.TextValueComponent;
+import javafx.scene.layout.VBox;
+
+public class PriceInfoSection extends VBox {
+
+ TextValueComponent stockPrice = new TextValueComponent("Current price: ");
+ TextValueComponent latestChange = new TextValueComponent("Latest Price Change: ");
+ TextValueComponent allTimeScore = new TextValueComponent("All time H/L: ");
+ TextValueComponent ownedAmount = new TextValueComponent("Owned amount: ");
+ TextValueComponent totalProfits = new TextValueComponent("Total profits: ");
+
+ public PriceInfoSection() {
+ this.getChildren().addAll(stockPrice, latestChange, allTimeScore, ownedAmount, totalProfits);
+ }
+
+ public TextValueComponent getStockPrice() {
+ return stockPrice;
+ }
+
+ public TextValueComponent getLatestChange() {
+ return latestChange;
+ }
+
+ public TextValueComponent getAllTimeScore() {
+ return allTimeScore;
+ }
+
+ public TextValueComponent getOwnedAmount() {
+ return ownedAmount;
+ }
+
+ public TextValueComponent getTotalProfits() {
+ return totalProfits;
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/sections/TradeSection.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/sections/TradeSection.java
new file mode 100644
index 0000000..19a599d
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/sections/TradeSection.java
@@ -0,0 +1,63 @@
+package edu.ntnu.idi.idatt.view.primary.stock.sections;
+
+import edu.ntnu.idi.idatt.view.components.elements.TextValueComponent;
+import javafx.geometry.Insets;
+import javafx.scene.control.Button;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+
+public class TradeSection extends VBox {
+
+ Button buyButton = new Button("Buy");
+ Button portfolioButton = new Button("Sell in portfolio...");
+ TextField buyInputField = new TextField();
+ TextValueComponent buyPrice = new TextValueComponent("Price: ");
+ TextValueComponent buyCost = new TextValueComponent("Taxes & Comission: ");
+ TextValueComponent totalPrice = new TextValueComponent("Total: ");
+ TextValueComponent resultMessage = new TextValueComponent("");
+
+ public TradeSection() {
+
+ HBox wrapper = new HBox();
+ wrapper.setSpacing(20.0);
+ wrapper.getChildren().addAll(buyButton, portfolioButton);
+
+ // Detailing
+ buyInputField.setPromptText("Amount of stocks...");
+
+ this.setPadding(new Insets(20));
+ this.setMaxWidth(400);
+ this.getChildren().addAll(wrapper, buyInputField, buyPrice, buyCost, totalPrice, resultMessage);
+
+ }
+
+ public Button getBuyButton() {
+ return buyButton;
+ }
+
+ public Button getPortfolioButton() {
+ return portfolioButton;
+ }
+
+ public TextField getBuyInputField() {
+ return buyInputField;
+ }
+
+ public TextValueComponent getBuyPrice() {
+ return buyPrice;
+ }
+
+ public TextValueComponent getBuyCost() {
+ return buyCost;
+ }
+
+ public TextValueComponent getTotalPrice() {
+ return totalPrice;
+ }
+
+ public TextValueComponent getResultMessage() {
+ return resultMessage;
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/viewmodel/PriceInfoModel.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/viewmodel/PriceInfoModel.java
new file mode 100644
index 0000000..67c4599
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/viewmodel/PriceInfoModel.java
@@ -0,0 +1,46 @@
+package edu.ntnu.idi.idatt.view.primary.stock.viewmodel;
+
+import javafx.beans.property.SimpleStringProperty;
+
+public class PriceInfoModel {
+ private final SimpleStringProperty stockPrice = new SimpleStringProperty();
+
+ private final SimpleStringProperty latestChange = new SimpleStringProperty();
+ private final SimpleStringProperty latestChangeColorProperty = new SimpleStringProperty();
+
+ private final SimpleStringProperty allTimeScore = new SimpleStringProperty();
+
+ private final SimpleStringProperty ownedAmount = new SimpleStringProperty();
+
+ private final SimpleStringProperty totalProfits = new SimpleStringProperty();
+ private final SimpleStringProperty totalProfitsColorProperty = new SimpleStringProperty();
+
+ public SimpleStringProperty getStockPrice() {
+ return stockPrice;
+ }
+
+ public SimpleStringProperty getLatestChange() {
+ return latestChange;
+ }
+
+ public SimpleStringProperty getLatestChangeColorProperty() {
+ return latestChangeColorProperty;
+ }
+
+ public SimpleStringProperty getAllTimeScore() {
+ return allTimeScore;
+ }
+
+ public SimpleStringProperty getOwnedAmount() {
+ return ownedAmount;
+ }
+
+ public SimpleStringProperty getTotalProfits() {
+ return totalProfits;
+ }
+
+ public SimpleStringProperty getTotalProfitsColorProperty() {
+ return totalProfitsColorProperty;
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/viewmodel/TradeModel.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/viewmodel/TradeModel.java
new file mode 100644
index 0000000..4c55bf7
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/stock/viewmodel/TradeModel.java
@@ -0,0 +1,38 @@
+package edu.ntnu.idi.idatt.view.primary.stock.viewmodel;
+
+import javafx.beans.property.SimpleStringProperty;
+
+public class TradeModel {
+
+ 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 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/transactions/TransactionController.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/transactions/TransactionController.java
new file mode 100644
index 0000000..5de890c
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/transactions/TransactionController.java
@@ -0,0 +1,122 @@
+package edu.ntnu.idi.idatt.view.primary.transactions;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+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.SaleCalculator;
+import edu.ntnu.idi.idatt.session.UserSession;
+import edu.ntnu.idi.idatt.view.components.AbstractController;
+import edu.ntnu.idi.idatt.view.components.elements.TransactionComponent;
+
+public class TransactionController extends AbstractController {
+
+ public enum SortAction {
+ NONE,
+ NEWEST_DESCENDING,
+ OLDEST_ASCENDING,
+ PURCHASE,
+ SALE,
+ PROFIT,
+ LOSS
+ }
+
+ private UserSession session = UserSession.getInstance();
+ private ArrayList transactionsSorted = new ArrayList<>();
+
+ public TransactionController(TransactionModel model) {
+ super(model);
+ List initialTransactionLoad = session.getPlayer().getTransactionArchive().getTransactions();
+ transactionsSorted.addAll(initialTransactionLoad);
+ setTransactionModel(initialTransactionLoad);
+ }
+
+ public void setTransactionModel(List list) {
+ model.getTransactionList().clear();
+ for (Transaction transaction : list) {
+ model.getTransactionList().add(new TransactionComponent(transaction));
+ }
+ }
+
+ public void handleSearchQuery(String query) {
+ if (query == null || query.isBlank()) {
+ setTransactionModel(transactionsSorted);
+ return;
+ }
+
+ List transactionsFound = transactionsSorted.stream()
+ .filter(transaction -> transaction.getShare().getStock().toString().contains(query)).toList();
+
+ setTransactionModel(transactionsFound);
+ }
+
+ public void sortTransactionsBy(SortAction action) {
+ int currentWeek = session.getExchange().getWeek();
+ transactionsSorted.clear(); // Clear buffers
+
+ switch (action) {
+
+ case NONE: {
+ transactionsSorted.addAll(session.getPlayer().getTransactionArchive().getTransactions());
+ break;
+ }
+
+ case NEWEST_DESCENDING: {
+ for (int i = currentWeek; i >= 1; i--) { // Exchanges start at week 1.
+ List getCurrentTransactions = session.getPlayer().getTransactionArchive().getTransactions(i);
+ transactionsSorted.addAll(getCurrentTransactions);
+ }
+ break;
+ }
+
+ case OLDEST_ASCENDING: {
+ for (int i = 1; i <= currentWeek; i++) {
+ List getCurrentTransactions = session.getPlayer().getTransactionArchive().getTransactions(i);
+ transactionsSorted.addAll(getCurrentTransactions);
+ }
+ break;
+ }
+
+ case PURCHASE: {
+ for (int i = currentWeek; i >= 1; i--) {
+ List getCurrentTransactions = session.getPlayer().getTransactionArchive().getPurchases(i);
+ transactionsSorted.addAll(getCurrentTransactions);
+ }
+ break;
+ }
+
+ case SALE: {
+ for (int i = currentWeek; i >= 1; i--) {
+ List getCurrentTransactions = session.getPlayer().getTransactionArchive().getSales(i);
+ transactionsSorted.addAll(getCurrentTransactions);
+ }
+ break;
+ }
+
+ case PROFIT: {
+ List getCurrentTransactions = new ArrayList<>(session.getPlayer().getTransactionArchive().getSales());
+ getCurrentTransactions
+ .sort(Comparator.comparing((Sale sale) -> ((SaleCalculator) sale.getCalculator()).calculateProfit())
+ .reversed());
+ transactionsSorted.addAll(getCurrentTransactions);
+ break;
+ }
+
+ case LOSS: {
+ List getCurrentTransactions = new ArrayList<>(session.getPlayer().getTransactionArchive().getSales());
+ getCurrentTransactions
+ .sort(Comparator.comparing(sale -> ((SaleCalculator) sale.getCalculator()).calculateProfit()));
+ transactionsSorted.addAll(getCurrentTransactions);
+ break;
+ }
+
+ }
+
+ setTransactionModel(transactionsSorted);
+
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/transactions/TransactionModel.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/transactions/TransactionModel.java
new file mode 100644
index 0000000..6622bf5
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/transactions/TransactionModel.java
@@ -0,0 +1,16 @@
+package edu.ntnu.idi.idatt.view.primary.transactions;
+
+import edu.ntnu.idi.idatt.view.components.Model;
+import edu.ntnu.idi.idatt.view.components.elements.TransactionComponent;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+public class TransactionModel implements Model {
+
+ private final ObservableList transactionList = FXCollections.observableArrayList();
+
+ public ObservableList getTransactionList() {
+ return transactionList;
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/transactions/TransactionView.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/transactions/TransactionView.java
new file mode 100644
index 0000000..49e2e50
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/transactions/TransactionView.java
@@ -0,0 +1,93 @@
+package edu.ntnu.idi.idatt.view.primary.transactions;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+import edu.ntnu.idi.idatt.view.SceneFactory;
+import edu.ntnu.idi.idatt.view.SceneManager;
+import edu.ntnu.idi.idatt.view.components.AbstractViewUI;
+import edu.ntnu.idi.idatt.view.components.ui.UIFactory;
+import edu.ntnu.idi.idatt.view.primary.transactions.TransactionController.SortAction;
+import javafx.beans.binding.Bindings;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Parent;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+
+public class TransactionView extends AbstractViewUI {
+
+ HBox root;
+ Consumer searchQueryHandler;
+ Consumer sortHandle;
+
+ @Override
+ public Parent createContent() {
+ root = new HBox();
+ root.getStyleClass().add("primary");
+ root.setMinWidth(Region.USE_PREF_SIZE);
+ root.setAlignment(Pos.CENTER);
+ root.setSpacing(20.0);
+
+ ScrollPane wrapper = new ScrollPane();
+ wrapper.setContent(root);
+ wrapper.setPadding(new Insets(20.0, 0, 20.0, 0));
+ wrapper.setFitToWidth(true);
+ wrapper.setFitToHeight(true);
+ wrapper.getStyleClass().add("primary");
+
+ return wrapper;
+ }
+
+ @Override
+ public Parent createNavigation() {
+ return UIFactory.createNavigation("Transactions", List.of());
+ }
+
+ @Override
+ public Parent createHeader() {
+ return UIFactory.createHeader("Search for transactions...",
+ (query) -> searchQueryHandler.accept(query),
+ List.of(
+ "None",
+ "Week (newest - oldest)",
+ "Week (oldest - newest)",
+ "Purchases only (newest)",
+ "Sales only (newest)",
+ "Highest profit",
+ "Highest loss"),
+ () -> sortHandle.accept(SortAction.NONE),
+ () -> sortHandle.accept(SortAction.NEWEST_DESCENDING),
+ () -> sortHandle.accept(SortAction.OLDEST_ASCENDING),
+ () -> sortHandle.accept(SortAction.PURCHASE),
+ () -> sortHandle.accept(SortAction.SALE),
+ () -> sortHandle.accept(SortAction.PROFIT),
+ () -> sortHandle.accept(SortAction.LOSS));
+ }
+
+ @Override
+ public Parent createToolbar() {
+ return UIFactory.createToolbar(() -> this.toggleMenu(),
+ () -> SceneManager.switchTo(SceneFactory.createStartView()));
+ }
+
+ @Override
+ public Parent createMenu() {
+ return UIFactory.createMenu("Account",
+ List.of(" • Portfolio", " • Transactions"),
+ () -> SceneManager.switchTo(SceneFactory.createPortfolioView()),
+ () -> SceneManager.switchTo(SceneFactory.createTransactionView()));
+
+ }
+
+ public void setModel(TransactionModel model) {
+ Bindings.bindContent(root.getChildren(), model.getTransactionList());
+ }
+
+ public void setController(TransactionController controller) {
+ searchQueryHandler = (query) -> controller.handleSearchQuery(query);
+ sortHandle = (sortAction) -> controller.sortTransactionsBy(sortAction);
+ }
+
+}
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
new file mode 100644
index 0000000..4615da6
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/util/CssUtils.java
@@ -0,0 +1,32 @@
+package edu.ntnu.idi.idatt.view.util;
+
+import java.math.BigDecimal;
+
+import javafx.scene.Parent;
+
+public class CssUtils {
+
+ public static final String BIG_TEXT_32 = "big-text-32";
+ public static final String MED_TEXT_16 = "med-text-16";
+ public static final String SMALL_TEXT_12 = "small-text-12";
+ public static final String RED = "red";
+ public static final String GREEN = "green";
+
+ 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(BigDecimal value) {
+ return value.compareTo(BigDecimal.ZERO) >= 0 ? GREEN : RED;
+ }
+
+ public static String generateValueColors(double value) {
+ return value >= 0 ? GREEN : RED;
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/util/ResourceUtils.java b/src/main/java/edu/ntnu/idi/idatt/view/util/ResourceUtils.java
new file mode 100644
index 0000000..d40bfce
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/util/ResourceUtils.java
@@ -0,0 +1,11 @@
+package edu.ntnu.idi.idatt.view.util;
+
+import javafx.scene.image.Image;
+
+public class ResourceUtils {
+ public static final Image MENU_ICON = new Image(ResourceUtils.class.getResource("/icons/user.png").toExternalForm());
+ public static final Image SEARCH_ICON = new Image(
+ ResourceUtils.class.getResource("/icons/search.png").toExternalForm());
+ public static final Image QUIT_ICON = new Image(ResourceUtils.class.getResource("/icons/quit.png").toExternalForm());
+
+}
diff --git a/src/main/resources/edu.ntnu.idi.idatt/logo.png b/src/main/resources/edu.ntnu.idi.idatt/logo.png
deleted file mode 100644
index a8980fe..0000000
Binary files a/src/main/resources/edu.ntnu.idi.idatt/logo.png and /dev/null differ
diff --git a/src/main/resources/edu.ntnu.idi.idatt/stonks.png b/src/main/resources/edu.ntnu.idi.idatt/stonks.png
deleted file mode 100644
index db95b50..0000000
Binary files a/src/main/resources/edu.ntnu.idi.idatt/stonks.png and /dev/null differ
diff --git a/src/main/resources/icons/portfolio.png b/src/main/resources/icons/portfolio.png
new file mode 100644
index 0000000..2f33093
Binary files /dev/null and b/src/main/resources/icons/portfolio.png differ
diff --git a/src/main/resources/icons/quit.png b/src/main/resources/icons/quit.png
new file mode 100644
index 0000000..f577227
Binary files /dev/null and b/src/main/resources/icons/quit.png differ
diff --git a/src/main/resources/icons/search.png b/src/main/resources/icons/search.png
new file mode 100644
index 0000000..a206015
Binary files /dev/null and b/src/main/resources/icons/search.png differ
diff --git a/src/main/resources/icons/user.png b/src/main/resources/icons/user.png
new file mode 100644
index 0000000..0534bdd
Binary files /dev/null and b/src/main/resources/icons/user.png differ
diff --git a/src/main/resources/save.json b/src/main/resources/save.json
new file mode 100644
index 0000000..07089da
--- /dev/null
+++ b/src/main/resources/save.json
@@ -0,0 +1 @@
+"# Ticker,Name,Price AMD,Advanced Micro Devices,202.68 MSI,Micro-Star International,92.80 INTC,Intel,45.58 NVDA,Nvidia,182.65 EQNR,Equinor,32.43"
\ No newline at end of file
diff --git a/src/main/resources/stocks.csv b/src/main/resources/stocks.csv
new file mode 100644
index 0000000..f9b7ff5
--- /dev/null
+++ b/src/main/resources/stocks.csv
@@ -0,0 +1,7 @@
+# Ticker,Name,Price
+
+AMD,Advanced Micro Devices,202.68
+MSI,Micro-Star International,92.80
+INTC,Intel,45.58
+NVDA,Nvidia,182.65
+EQNR,Equinor,32.43
diff --git a/src/main/resources/themes/default.css b/src/main/resources/themes/default.css
new file mode 100644
index 0000000..c64bfcd
--- /dev/null
+++ b/src/main/resources/themes/default.css
@@ -0,0 +1,132 @@
+.dark {
+ -fx-background-color: #0B2B40;
+}
+
+.light {
+ -fx-background-color: #1E5959;
+}
+
+.primary {
+ -fx-background-color: #3B8C6E;
+}
+
+.title {
+ -fx-font-size: 56px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #EEEEEE;
+ -fx-padding: 30px;
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 10, 0.5, 0, 5);
+}
+
+.big-text-32{
+ -fx-font-size: 32px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #EEEEEE;
+ -fx-padding: 30px;
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 10, 0.5, 0, 5);
+}
+
+.med-text-16{
+ -fx-font-size: 16px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #EEEEEE;
+}
+
+.small-text-12{
+ -fx-font-size: 12px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #EEEEEE;
+}
+
+.portfolio-box-text{
+ -fx-font-size: 20px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #EEEEEE;
+ -fx-padding: 10;
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 10, 0.5, 0, 5);
+}
+.portfolio-box-title{
+ -fx-font-size: 32px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #EEEEEE;
+ -fx-padding: 10;
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 10, 0.5, 0, 5);
+}
+
+.red {
+ -fx-text-fill: #FF0000;
+}
+
+.green {
+ -fx-text-fill: #00FF00;
+}
+
+/* =======================
+ BUTTON STYLE
+ ======================= */
+.button {
+ -fx-font-size: 16px;
+ -fx-text-fill: black;
+ -fx-background-color: #FFFFFF;
+ -fx-background-radius: 20;
+ -fx-border-radius: 20;
+ -fx-padding: 10 20 10 20;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 6, 0.3, 0, 2);
+}
+
+.button:hover {
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.4), 8, 0.4, 0, 4);
+ -fx-translate-y: 1;
+}
+
+.icon {
+ -fx-border-radius: 20;
+ -fx-padding: 5;
+ -fx-cursor: hand;
+}
+
+.viewport-colour > .viewport {
+ -fx-background-color: #3B8C6E;
+}
+
+.rowBox {
+ -fx-background-color: #404950;
+ -fx-alignment: center;
+}
+
+.searchbar {
+ -fx-background-color: #FFFFFF;
+ -fx-background-radius: 20;
+ -fx-border-radius: 20;
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 6, 0.3, 0, 2);
+}
+
+.searchbar-field {
+ -fx-font-size: 16px;
+ -fx-text-fill: black;
+ -fx-background-color: #FFFFFF;
+ -fx-background-radius: 20;
+ -fx-border-radius: 20;
+ -fx-cursor: hand;
+}
+
+.newspaper-title {
+ -fx-font-size: 56px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #000000;
+ -fx-padding: 30px;
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 10, 0.5, 0, 5);
+}
+
+.newspaper-med-text{
+ -fx-font-size: 22px;
+ -fx-font-weight: 700;
+ -fx-text-fill: #000000;
+}
+
+.newspaper-article{
+ -fx-background-color: #EEEEEE;
+}
+
+
diff --git a/src/main/resources/edu.ntnu.idi.idatt/user.png b/src/main/resources/user.png
similarity index 100%
rename from src/main/resources/edu.ntnu.idi.idatt/user.png
rename to src/main/resources/user.png
diff --git a/src/test/java/edu/ntnu/idi/idatt/PlayerTest.java b/src/test/java/edu/ntnu/idi/idatt/PlayerTest.java
deleted file mode 100644
index cd6df5d..0000000
--- a/src/test/java/edu/ntnu/idi/idatt/PlayerTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package edu.ntnu.idi.idatt;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-
-import java.math.BigDecimal;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-class PlayerTest {
-
- private Player player;
-
- @BeforeEach
- public void PT_setup() {
-
- player = new Player("TestPlayer", new BigDecimal("500"));
-
- }
-
- @Test
- void PTConstructor() {
- assertEquals("TestPlayer", player.getName());
- assertEquals(new BigDecimal("500"), player.getMoney());
- assertNotNull(player.getPortfolio());
- assertNotNull(player.getTransactionArchive());
- }
-
- @Test
- void PTaddMoney() {
-
- player.addMoney(new BigDecimal("200"));
- assertEquals(new BigDecimal("700"), player.getMoney());
-
- }
-
- @Test
- void PTwithdrawMoney() {
- player.withdrawMoney(new BigDecimal("200"));
- assertEquals(new BigDecimal("300"), player.getMoney());
- }
-
-}
diff --git a/src/test/java/edu/ntnu/idi/idatt/marked/StockTest.java b/src/test/java/edu/ntnu/idi/idatt/marked/StockTest.java
deleted file mode 100644
index 7d81482..0000000
--- a/src/test/java/edu/ntnu/idi/idatt/marked/StockTest.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package edu.ntnu.idi.idatt.marked;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.math.BigDecimal;
-import java.util.List;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-public class StockTest {
-
- private Stock stock;
-
- @BeforeEach
- public void PT_setup() {
- List prices = List.of(new BigDecimal("46.2"),
- new BigDecimal("40.0"));
- stock = new Stock("AAPL", "Apple Inc.", prices);
-
- }
-
- @Test
- void constructorTest() {
- assertEquals("AAPL", stock.getSymbol());
- assertEquals("Apple Inc.", stock.getCompany());
-
- assertEquals(List.of(new BigDecimal("46.2"),
- new BigDecimal("40.0")), stock.getPrices());
-
- assertEquals(new BigDecimal("40.0"), stock.getSalesPrice());
- }
-
- @Test
- void addNewSalesPriceTest() {
- BigDecimal value = new BigDecimal("15.6");
- stock.addNewSalesPrice(value);
-
- assertEquals(value, stock.getSalesPrice());
- }
-
-}
diff --git a/src/test/java/edu/ntnu/idi/idatt/ExchangeTest.java b/src/test/java/edu/ntnu/idi/idatt/model/ExchangeTest.java
similarity index 61%
rename from src/test/java/edu/ntnu/idi/idatt/ExchangeTest.java
rename to src/test/java/edu/ntnu/idi/idatt/model/ExchangeTest.java
index 743f3be..af14b62 100644
--- a/src/test/java/edu/ntnu/idi/idatt/ExchangeTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt/model/ExchangeTest.java
@@ -1,38 +1,58 @@
-package edu.ntnu.idi.idatt;
+package edu.ntnu.idi.idatt.model;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.IOException;
+import java.io.InputStream;
import java.math.BigDecimal;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import edu.ntnu.idi.idatt.marked.Stock;
-import edu.ntnu.idi.idatt.transaction.Transaction;
+import edu.ntnu.idi.idatt.model.market.Stock;
+import edu.ntnu.idi.idatt.model.player.Player;
+import edu.ntnu.idi.idatt.model.transaction.Transaction;
class ExchangeTest {
+ /**
+ *
+ * Caution!
+ * stocks List is not sorted correctly.
+ * We parse it with hashMap.values().stream().toList
+ * which dont properly arrange it in memory.
+ * Take caution when programming or reading here.
+ *
+ * @see PTgetGainers_Losers don't take the get(index) literally
+ * but focus on the differences of last sale value made.
+ *
+ * This is a work around for implementing the ExchangeLoader to Exchange
+ * class.
+ *
+ */
private Exchange exchange;
private List stocks;
private Player player;
@BeforeEach
- public void PT_setup() {
+ public void PT_setup() throws IOException {
- // Initialize exchange with proper objects
- Stock AAPL = new Stock("AAPL", "Apple Inc.", List.of(new BigDecimal("32")));
+ Stock AAPL = new Stock("AAPL", "Apple Inc", List.of(new BigDecimal("30")));
Stock NVDA = new Stock("NVDA", "NVIDIA", List.of(new BigDecimal("182.81")));
Stock TSLA = new Stock("TSLA", "Tesla", List.of(new BigDecimal("417.44")));
Stock AMD = new Stock("AMD", "Advanced Micro Devices", List.of(new BigDecimal("207.32")));
- stocks = List.of(AAPL, NVDA, TSLA, AMD);
-
- exchange = new Exchange("TestExchange", stocks);
+ List stockss = List.of(AAPL, NVDA, TSLA, AMD);
+ exchange = new Exchange("TestExchange", stockss);
+ stocks = exchange.getStocks();
player = new Player("TestPlayer", new BigDecimal("500"));
}
@@ -55,12 +75,10 @@ void PTConstructor() {
void PTFindStock() {
assertTrue(exchange.hasStock("AAPL"));
- assertEquals(stocks.get(0) /* AAPL Stock */, exchange.getStock("AAPL"));
+ assertTrue(stocks.contains(exchange.getStock("AAPL")));
// FindStocks for letter "n" should be - AAPL, AMD (both symbols and names!)
- List expected = List.of(stocks.get(0), stocks.get(3));
- //
- assertEquals(expected, exchange.findStocks("n"));
+ assertEquals(2, exchange.findStocks("n").size());
}
@@ -95,7 +113,7 @@ void PTBuy() {
void PTSell() {
// Player has to have a share to sell it.
exchange.buy("AAPL", new BigDecimal("1"), player);
- stocks.get(0).addNewSalesPrice(new BigDecimal("40")); // Simulate increase of AAPL stock price
+ exchange.getStock("AAPL").addNewSalesPrice(new BigDecimal("40")); // Simulate increase of AAPL stock price
Transaction transaction = exchange.sell(player.getPortfolio().getShares().getLast(), player);
assertEquals(transaction, player.getTransactionArchive().getTransactions(1).getLast());
@@ -106,18 +124,25 @@ void PTSell() {
@Test
void PTAdvance() {
- List stockPricesBefore = new ArrayList<>();
- for (Stock stock : stocks) {
- stockPricesBefore.add(stocks.indexOf(stock), stock.getSalesPrice());
- }
-
- exchange.advance();
-
- for (Stock stock : stocks) {
- assertTrue(stockPricesBefore.get(stocks.indexOf(stock)).compareTo(stock.getSalesPrice()) != 0);
- // If compareTo returns 0 then its equal.
- }
+ // TODO: do
+ }
+ /**
+ * Test for obtaining the gainers and loosers.
+ */
+ @Test
+ void PTgetGainers_Losers() {
+ // Simulate price change
+ stocks.get(0).addNewSalesPrice(new BigDecimal("4333"));
+ stocks.get(1).addNewSalesPrice(new BigDecimal("10"));
+ stocks.get(2).addNewSalesPrice(new BigDecimal("3"));
+ stocks.get(3).addNewSalesPrice(new BigDecimal("800"));
+
+ assertEquals(2, exchange.getGainers(2).size());
+ assertEquals(2, exchange.getLosers(2).size());
+
+ assertEquals(List.of(stocks.get(0)), exchange.getGainers(1));
+ assertEquals(List.of(stocks.get(2)), exchange.getLosers(1));
}
/**
diff --git a/src/test/java/edu/ntnu/idi/idatt/model/market/StockTest.java b/src/test/java/edu/ntnu/idi/idatt/model/market/StockTest.java
new file mode 100644
index 0000000..5b27600
--- /dev/null
+++ b/src/test/java/edu/ntnu/idi/idatt/model/market/StockTest.java
@@ -0,0 +1,60 @@
+package edu.ntnu.idi.idatt.model.market;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class StockTest {
+
+ private Stock stock;
+ private List prices;
+
+ @BeforeEach
+ public void PT_setup() {
+ prices = List.of(new BigDecimal("46.2"),
+ new BigDecimal("40.0"),
+ new BigDecimal("39.5"),
+ new BigDecimal("51.2"),
+ new BigDecimal("43.4"));
+ stock = new Stock("AAPL", "Apple Inc.", prices);
+
+ }
+
+ @Test
+ void constructorTest() {
+ assertEquals("AAPL", stock.getSymbol());
+ assertEquals("Apple Inc.", stock.getCompany());
+ assertEquals(new BigDecimal("43.4"), stock.getSalesPrice());
+ }
+
+ @Test
+ void PTgetPrices() {
+ assertEquals(prices, stock.getHistoricalPrices());
+ assertEquals(new BigDecimal("39.5"), stock.getLowestPrice());
+ assertEquals(new BigDecimal("51.2"), stock.getHighestPrice());
+ assertEquals(new BigDecimal("43.4").subtract(new BigDecimal("51.2")), stock.getLatestPriceChange());
+ }
+
+ @Test
+ void NTgetLatestPriceChange() {
+ Stock noPrice = new Stock("NVDA", "Nvidia", List.of());
+ Stock onePrice = new Stock("TSLA", "Tesla Inc.", List.of(
+ BigDecimal.TEN));
+
+ assertEquals(BigDecimal.ZERO, noPrice.getLatestPriceChange());
+ assertEquals(BigDecimal.ZERO, onePrice.getLatestPriceChange());
+ }
+
+ @Test
+ void addNewSalesPriceTest() {
+ BigDecimal value = new BigDecimal("15.6");
+ stock.addNewSalesPrice(value);
+
+ assertEquals(value, stock.getSalesPrice());
+ }
+
+}
diff --git a/src/test/java/edu/ntnu/idi/idatt/model/player/PlayerTest.java b/src/test/java/edu/ntnu/idi/idatt/model/player/PlayerTest.java
new file mode 100644
index 0000000..16bbb95
--- /dev/null
+++ b/src/test/java/edu/ntnu/idi/idatt/model/player/PlayerTest.java
@@ -0,0 +1,94 @@
+package edu.ntnu.idi.idatt.model.player;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import edu.ntnu.idi.idatt.model.market.Stock;
+import edu.ntnu.idi.idatt.model.portfolio.Share;
+import edu.ntnu.idi.idatt.model.transaction.Purchase;
+
+class PlayerTest {
+
+ private Player player;
+
+ @BeforeEach
+ public void PT_setup() {
+
+ player = new Player("TestPlayer", new BigDecimal("500"));
+
+ }
+
+ @Test
+ void PTConstructor() {
+ assertEquals("TestPlayer", player.getName());
+ assertEquals(new BigDecimal("500"), player.getMoney());
+ assertNotNull(player.getPortfolio());
+ assertNotNull(player.getTransactionArchive());
+ }
+
+ @Test
+ void PTaddMoney() {
+
+ player.addMoney(new BigDecimal("200"));
+ assertEquals(new BigDecimal("700"), player.getMoney());
+
+ }
+
+ @Test
+ void PTwithdrawMoney() {
+ player.withdrawMoney(new BigDecimal("200"));
+ assertEquals(new BigDecimal("300"), player.getMoney());
+ }
+
+ @Test
+ void PTgetNetWorth() {
+ assertEquals(player.getNetWorth(), new BigDecimal("500"));
+
+ // Add to player portfolio
+ Stock stock = new Stock("A", "A", List.of(new BigDecimal("20")));
+ Share share = new Share(stock, new BigDecimal("3.5"), new BigDecimal("30"));
+ player.getPortfolio().addShare(share);
+
+ BigDecimal sum = player.getMoney().add(player.getPortfolio().getNetWorth());
+ assertEquals(sum, player.getNetWorth());
+
+ }
+
+ @Test
+ void PTgetStatus() {
+ assertEquals("Novice", player.getStatus());
+
+ // Simulate progress
+ Stock stock = new Stock("A", "A", List.of(new BigDecimal("20")));
+ Share share = new Share(stock, new BigDecimal("3.5"), new BigDecimal("30"));
+
+ for (int i = 0; i <= 20; i++) {
+ player.getTransactionArchive().add(new Purchase(share, i));
+ }
+
+ // Check different ifs
+ player.addMoney(new BigDecimal("90")); // 18%
+ assertEquals("Novice", player.getStatus());
+ player.addMoney(new BigDecimal("10")); // 20%
+ assertEquals("Investor", player.getStatus());
+ player.addMoney(new BigDecimal("400")); // 100%
+ assertEquals("Speculator", player.getStatus());
+
+ // new Player to check if Speculator dont happen week < 20
+ Player plr = new Player("plr", new BigDecimal("500"));
+
+ for (int i = 0; i <= 15; i++) {
+ plr.getTransactionArchive().add(new Purchase(share, i));
+ }
+ plr.addMoney(new BigDecimal("600")); // 120%
+ assertEquals("Investor", plr.getStatus());
+
+ }
+
+}
diff --git a/src/test/java/edu/ntnu/idi/idatt/marked/PortfolioTest.java b/src/test/java/edu/ntnu/idi/idatt/model/portfolio/PortfolioTest.java
similarity index 75%
rename from src/test/java/edu/ntnu/idi/idatt/marked/PortfolioTest.java
rename to src/test/java/edu/ntnu/idi/idatt/model/portfolio/PortfolioTest.java
index f0d0913..743963f 100644
--- a/src/test/java/edu/ntnu/idi/idatt/marked/PortfolioTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt/model/portfolio/PortfolioTest.java
@@ -1,4 +1,4 @@
-package edu.ntnu.idi.idatt.marked;
+package edu.ntnu.idi.idatt.model.portfolio;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -11,6 +11,9 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import edu.ntnu.idi.idatt.model.market.Stock;
+import edu.ntnu.idi.idatt.service.transaction.SaleCalculator;
+
public class PortfolioTest {
private Stock stock;
@@ -78,4 +81,17 @@ void NTremoveShare() {
assertThrows(IllegalArgumentException.class, () -> portfolio.removeShare(exception));
}
+ /**
+ * Positive test for finding net value of the players shares.
+ */
+ @Test
+ void PTgetNetWorth() {
+ Share share1 = new Share(stock, new BigDecimal("1"), new BigDecimal("135.8"));
+ Share share2 = new Share(stock, new BigDecimal("2"), new BigDecimal("254"));
+ portfolio.addShare(share1);
+ portfolio.addShare(share2);
+ assertEquals(new SaleCalculator(share1).calculateTotal().add(new SaleCalculator(share2).calculateTotal()),
+ portfolio.getNetWorth());
+ }
+
}
diff --git a/src/test/java/edu/ntnu/idi/idatt/marked/ShareTest.java b/src/test/java/edu/ntnu/idi/idatt/model/portfolio/ShareTest.java
similarity index 89%
rename from src/test/java/edu/ntnu/idi/idatt/marked/ShareTest.java
rename to src/test/java/edu/ntnu/idi/idatt/model/portfolio/ShareTest.java
index ac69822..cefea8b 100644
--- a/src/test/java/edu/ntnu/idi/idatt/marked/ShareTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt/model/portfolio/ShareTest.java
@@ -1,4 +1,4 @@
-package edu.ntnu.idi.idatt.marked;
+package edu.ntnu.idi.idatt.model.portfolio;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -8,6 +8,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import edu.ntnu.idi.idatt.model.market.Stock;
+
public class ShareTest {
private Share share;
diff --git a/src/test/java/edu/ntnu/idi/idatt/transaction/PurchaseTest.java b/src/test/java/edu/ntnu/idi/idatt/model/transaction/PurchaseTest.java
similarity index 90%
rename from src/test/java/edu/ntnu/idi/idatt/transaction/PurchaseTest.java
rename to src/test/java/edu/ntnu/idi/idatt/model/transaction/PurchaseTest.java
index 25f9c9e..caf44c1 100644
--- a/src/test/java/edu/ntnu/idi/idatt/transaction/PurchaseTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt/model/transaction/PurchaseTest.java
@@ -1,4 +1,4 @@
-package edu.ntnu.idi.idatt.transaction;
+package edu.ntnu.idi.idatt.model.transaction;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -10,9 +10,9 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import edu.ntnu.idi.idatt.Player;
-import edu.ntnu.idi.idatt.marked.Share;
-import edu.ntnu.idi.idatt.marked.Stock;
+import edu.ntnu.idi.idatt.model.market.Stock;
+import edu.ntnu.idi.idatt.model.player.Player;
+import edu.ntnu.idi.idatt.model.portfolio.Share;
/**
* Testing for Purchase class
diff --git a/src/test/java/edu/ntnu/idi/idatt/transaction/SaleTest.java b/src/test/java/edu/ntnu/idi/idatt/model/transaction/SaleTest.java
similarity index 91%
rename from src/test/java/edu/ntnu/idi/idatt/transaction/SaleTest.java
rename to src/test/java/edu/ntnu/idi/idatt/model/transaction/SaleTest.java
index 2f5523b..1bd944e 100644
--- a/src/test/java/edu/ntnu/idi/idatt/transaction/SaleTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt/model/transaction/SaleTest.java
@@ -1,4 +1,4 @@
-package edu.ntnu.idi.idatt.transaction;
+package edu.ntnu.idi.idatt.model.transaction;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -10,10 +10,9 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import edu.ntnu.idi.idatt.Exchange;
-import edu.ntnu.idi.idatt.Player;
-import edu.ntnu.idi.idatt.marked.Share;
-import edu.ntnu.idi.idatt.marked.Stock;
+import edu.ntnu.idi.idatt.model.market.Stock;
+import edu.ntnu.idi.idatt.model.player.Player;
+import edu.ntnu.idi.idatt.model.portfolio.Share;
/**
* Testing for Sale class
diff --git a/src/test/java/edu/ntnu/idi/idatt/transaction/TransactionArchiveTest.java b/src/test/java/edu/ntnu/idi/idatt/model/transaction/TransactionArchiveTest.java
similarity index 85%
rename from src/test/java/edu/ntnu/idi/idatt/transaction/TransactionArchiveTest.java
rename to src/test/java/edu/ntnu/idi/idatt/model/transaction/TransactionArchiveTest.java
index 482a45c..32739ae 100644
--- a/src/test/java/edu/ntnu/idi/idatt/transaction/TransactionArchiveTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt/model/transaction/TransactionArchiveTest.java
@@ -1,4 +1,4 @@
-package edu.ntnu.idi.idatt.transaction;
+package edu.ntnu.idi.idatt.model.transaction;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -10,8 +10,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import edu.ntnu.idi.idatt.marked.Share;
-import edu.ntnu.idi.idatt.marked.Stock;
+import edu.ntnu.idi.idatt.model.market.Stock;
+import edu.ntnu.idi.idatt.model.portfolio.Share;
class TransactionArchiveTest {
@@ -87,4 +87,18 @@ void PTgetTransactions() {
}
+ /**
+ * Tests for countDistinctWeeks().
+ */
+ @Test
+ void PTcountDistinctWeeks() {
+ assertEquals(2, transactionArchive.countDistinctWeeks());
+ }
+
+ @Test
+ void NTcountDistinctWeeks() {
+ TransactionArchive tArchive = new TransactionArchive();
+ assertEquals(0, tArchive.countDistinctWeeks());
+ }
+
}
diff --git a/src/test/java/edu/ntnu/idi/idatt/calculator/PurchaseCalculatorTest.java b/src/test/java/edu/ntnu/idi/idatt/service/transaction/PurchaseCalculatorTest.java
similarity index 89%
rename from src/test/java/edu/ntnu/idi/idatt/calculator/PurchaseCalculatorTest.java
rename to src/test/java/edu/ntnu/idi/idatt/service/transaction/PurchaseCalculatorTest.java
index 5d306c0..7870ea4 100644
--- a/src/test/java/edu/ntnu/idi/idatt/calculator/PurchaseCalculatorTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt/service/transaction/PurchaseCalculatorTest.java
@@ -1,4 +1,4 @@
-package edu.ntnu.idi.idatt.calculator;
+package edu.ntnu.idi.idatt.service.transaction;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -8,8 +8,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import edu.ntnu.idi.idatt.marked.Share;
-import edu.ntnu.idi.idatt.marked.Stock;
+import edu.ntnu.idi.idatt.model.market.Stock;
+import edu.ntnu.idi.idatt.model.portfolio.Share;
class PurchaseCalculatorTest {
diff --git a/src/test/java/edu/ntnu/idi/idatt/calculator/SaleCalculatorTest.java b/src/test/java/edu/ntnu/idi/idatt/service/transaction/SaleCalculatorTest.java
similarity index 93%
rename from src/test/java/edu/ntnu/idi/idatt/calculator/SaleCalculatorTest.java
rename to src/test/java/edu/ntnu/idi/idatt/service/transaction/SaleCalculatorTest.java
index 3672172..eacc8e2 100644
--- a/src/test/java/edu/ntnu/idi/idatt/calculator/SaleCalculatorTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt/service/transaction/SaleCalculatorTest.java
@@ -1,4 +1,4 @@
-package edu.ntnu.idi.idatt.calculator;
+package edu.ntnu.idi.idatt.service.transaction;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -8,8 +8,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import edu.ntnu.idi.idatt.marked.Share;
-import edu.ntnu.idi.idatt.marked.Stock;
+import edu.ntnu.idi.idatt.model.market.Stock;
+import edu.ntnu.idi.idatt.model.portfolio.Share;
class SaleCalculatorTest {
diff --git a/src/test/java/edu/ntnu/idi/idatt/storage/StockParserTest.java b/src/test/java/edu/ntnu/idi/idatt/storage/StockParserTest.java
new file mode 100644
index 0000000..102cf7b
--- /dev/null
+++ b/src/test/java/edu/ntnu/idi/idatt/storage/StockParserTest.java
@@ -0,0 +1,68 @@
+package edu.ntnu.idi.idatt.storage;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import edu.ntnu.idi.idatt.model.market.Stock;
+
+/**
+ * Test class for ExchangeLoader
+ *
+ *
+ * Tests the loading and saving of stock data.
+ *
+ */
+class StockParserTest {
+
+ String file;
+
+ @BeforeEach
+ public void PT_setup() throws IOException {
+
+ InputStream is = getClass()
+ .getClassLoader()
+ .getResourceAsStream("stocks.csv");
+
+ Path tempFile = Files.createTempFile("stocks", ".csv");
+ Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING);
+
+ file = tempFile.toFile().toPath().toString();
+
+ }
+
+ /**
+ * Positive test for loading/reading stocks
+ */
+ @Test
+ void PT_load() {
+ List stocks = null;
+ try {
+ stocks = StockParser.load(file);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ assertEquals(4, stocks.size());
+
+ }
+
+ /**
+ * Negative tests reading stocks.
+ */
+ @Test
+ void NT_IllegalArgumentException_Constructor() {
+ assertThrows(IOException.class,
+ () -> StockParser.load("resources/notexistantfile.csv"));
+ }
+
+}
diff --git a/src/test/resources/stocks.csv b/src/test/resources/stocks.csv
new file mode 100644
index 0000000..275ddb0
--- /dev/null
+++ b/src/test/resources/stocks.csv
@@ -0,0 +1,7 @@
+# Ticker,Name,Price
+AAPL,Apple Inc,32
+NVDA,NVIDIA,182.81
+TSLA,Tesla,417.44
+AMD,Advanced Micro Devices,207.32
+
+