Skip to content
Snippets Groups Projects
IncomeExpenseController.java 21.2 KiB
Newer Older
package no.ntnu.idatt1002.demo.controller;

import java.io.IOException;
import java.util.Optional;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.chart.PieChart.Data;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import no.ntnu.idatt1002.demo.data.Budget.FileHandlingBudget;
import no.ntnu.idatt1002.demo.data.Budget.FileHandlingBudgetArchive;
import no.ntnu.idatt1002.demo.data.Budget.FileHandlingSelectedBudget;
import no.ntnu.idatt1002.demo.data.Budget.GeneralBudget;
import no.ntnu.idatt1002.demo.data.Economics.Expense;
import no.ntnu.idatt1002.demo.data.Economics.ExpenseCategory;
import no.ntnu.idatt1002.demo.data.Economics.ExpenseRegister;
import no.ntnu.idatt1002.demo.data.Economics.FileHandling;
import no.ntnu.idatt1002.demo.data.Economics.Income;
import no.ntnu.idatt1002.demo.data.Economics.IncomeCategory;
import no.ntnu.idatt1002.demo.data.Economics.IncomeRegister;

/**
 * Class for representing an overview of the income and expenses of the users budget.
 * Displays information in tables and pie charts. It is possible to add, edit and delete
 * income and expenses. The difference of the expense and income sum contribute to
 * the monthly budget progress.

 * @author Harry Linrui Xu
 * @since 30.03.2023
 */
public class IncomeExpenseController extends FinanceController {
  private final static String sumText = "Sum: ";
  @FXML
  private TableColumn<Expense, Double> expAmountCol;

  @FXML
  private TableColumn<Expense, ExpenseCategory> expCategoryCol;

  @FXML
  private TableColumn<Expense, String> expDateCol;

  @FXML
  private TableColumn<Expense, String> expDescriptionCol;

  @FXML
  private TableColumn<Expense, Boolean> expRecurringCol;

  @FXML
  private TableColumn<Income, Double> inAmountCol;

  @FXML
  private TableColumn<Income, IncomeCategory>  inCategoryCol;

  @FXML
  private TableColumn<Income, String>  inDateCol;

  @FXML
  private TableColumn<Income, String>  inDescriptionCol;

  @FXML
  private TableColumn<Income, Boolean> inRecurringCol;

  @FXML
  private TableView<Expense> expenseTableView;

  @FXML
  private TableView<Income> incomeTableView;

  @FXML
  private MenuItem addExpense;

  @FXML
  private MenuItem addIncome;

  @FXML
  private DatePicker date;

  @FXML
  private Label daysLeftLbl;

  @FXML
  private ComboBox<?> filter;

  @FXML
  private Button returnToMainMenuBtn;

  @FXML
  private MenuItem editIncomeMenu;

  @FXML
  private MenuItem deleteIncomeMenu;

  @FXML
  private MenuItem editExpenseMenu;

  @FXML
  private MenuItem deleteExpenseMenu;

  private IncomeRegister incomeRegister;

  private ExpenseRegister expenseRegister;

  private ObservableList<Income> income;

  private ObservableList<Expense> expenses;

  @FXML
  private PieChart expensePieChart;

  @FXML
  private PieChart incomePieChart;
  /**
   * Initializes the window that is controlled by the controller.
   * Instantiates the income and expense registers and set them to
   * table views.
   */
  public void initialize() {
      incomeRegister = loadIncomeDataFromFile(
          "budgets/" + FileHandlingSelectedBudget
              .readSelectedBudget("budgets/SelectedBudget") + "/Income");
      expenseRegister = loadExpenseDataFromFile(
          "budgets/" + FileHandlingSelectedBudget
              .readSelectedBudget("budgets/SelectedBudget") + "/Expense");
    } catch(IOException ioe) {
      showErrorDialogBox("File reading error", "Could not read register", "");
    }

    income = FXCollections.observableArrayList(incomeRegister.getItems());
    incomeTableView.setItems(income);
    expenses = FXCollections.observableArrayList(expenseRegister.getItems());
    expenseTableView.setItems(expenses);
    incomePieChart.setLegendSide(Side.RIGHT);
    incomePieChart.setLabelLineLength(10);
    expensePieChart.setLegendSide(Side.RIGHT);
    expensePieChart.setLabelLineLength(10);
    inSum.setText(sumText + String.valueOf(incomeRegister.getTotalSum()));
    expSum.setText(sumText + String.valueOf(expenseRegister.getTotalSum()));

    //Add event filter to continue, such that budgets cannot have max amount at 1 or less
    continueBtn.addEventFilter(
        ActionEvent.ACTION, event -> {
          if (!isValidMaxAmount(incomeRegister.getTotalSum(), expenseRegister.getTotalSum())) {
            event.consume();
            showErrorDialogBox("Invalid disposable income",
                "Disposable income must be above 1",
                "Please increase the income or decrease the expenses");
          }
        }
    );
  /**
   * Method for initiating all tableview columns.
   */
    inDateCol.setCellValueFactory(new PropertyValueFactory<>("date"));
    inAmountCol.setCellValueFactory(new PropertyValueFactory<>("amount"));
    inCategoryCol.setCellValueFactory(new PropertyValueFactory<>("category"));
    inDescriptionCol.setCellValueFactory(new PropertyValueFactory<>("description"));
    inRecurringCol.setCellValueFactory(new PropertyValueFactory<>("recurring"));

    expDateCol.setCellValueFactory(new PropertyValueFactory<>("date"));
    expAmountCol.setCellValueFactory(new PropertyValueFactory<>("amount"));
    expCategoryCol.setCellValueFactory(new PropertyValueFactory<>("category"));
    expDescriptionCol.setCellValueFactory(new PropertyValueFactory<>("description"));
    expRecurringCol.setCellValueFactory(new PropertyValueFactory<>("recurring"));
  /**
   * Method for creating a list of data used for graphing expenses in a pie chart. The categories from the
   * expenses register become the pieces of data.

   * @return An observable list of the sum of expenditure on each category
   */
  private ObservableList<PieChart.Data> createExpensePieChart() {
    return FXCollections.observableArrayList(
        new Data("Food", expenseRegister.getExpenseByCategory(ExpenseCategory.FOOD).getTotalSum()),
        new Data("Books", expenseRegister.getExpenseByCategory(ExpenseCategory.BOOKS).getTotalSum()),
        new Data("Clothes", expenseRegister.getExpenseByCategory(ExpenseCategory.CLOTHES).getTotalSum()),
        new Data("Other", expenseRegister.getExpenseByCategory(ExpenseCategory.OTHER).getTotalSum())
    );
  /**
   * Method for creating a list of data used for graphing income in a pie chart. The categories from the
   * income register become the pieces of data.

   * @return An observable list of the sum of earnings on each category
   */
  private ObservableList<PieChart.Data> createIncomePieChart() {
        new Data("Gift", incomeRegister.getIncomeByCategory(IncomeCategory.GIFT).getTotalSum()),
        new Data("Salary", incomeRegister.getIncomeByCategory(IncomeCategory.SALARY).getTotalSum()),
        new Data("Loans", incomeRegister.getIncomeByCategory(IncomeCategory.STUDENT_LOAN).getTotalSum())
  /**
   * Method for disabling the date picker, yet having its opacity at max.
   */
    date.setValue(LocalDate.now());
    date.setDisable(true);
    date.setStyle("-fx-opacity: 1");
    date.getEditor().setStyle("-fx-opacity: 1");
  }
  /**
   * Method for handling the adding of new entries in the tableview.
  public void handleAddBtn(javafx.event.ActionEvent event) {
    int sizeBf = (expenseRegister.getItems().size() + incomeRegister.getItems().size());

    if (event.getSource() == addIncome) {
      handleAddIncome();
    } else if (event.getSource() == addExpense){
      handleAddExpense();
    }

    int sizeAf = (expenseRegister.getItems().size() + incomeRegister.getItems().size());

    if (sizeAf != sizeBf) {
      refreshTableView();
  }

  /**
   * Method for handling the editing of a chosen entry in the tableview.
   *
   * @param event A button click on the edit button.
   */
  @Override
  public void handleEditBtn(javafx.event.ActionEvent event) { 
    Income chosenIncome = incomeTableView.getSelectionModel().getSelectedItem();
    Expense chosenExpense = expenseTableView.getSelectionModel().getSelectedItem();

    //Determines which editing or deleting option has been chosen.
    boolean isEditIncome = event.getSource() == editIncomeMenu;

    boolean isDeleteIncome = event.getSource() == deleteIncomeMenu;

    boolean isEditExpense = event.getSource() == editExpenseMenu;

    boolean isDeleteExpense =  event.getSource() == deleteExpenseMenu;

    //Initiate the chosen editing/deleting option
    if (isEditIncome) {
      handleEditIncome(chosenIncome);
    } else if (isDeleteIncome) {
      handleDeleteIncome(chosenIncome);
    } else if (isEditExpense) {
      handleEditExpense(chosenExpense);
    } else if (isDeleteExpense) {
      handleDeleteExpense(chosenExpense);
    //Updates the tableview and pie chart using the register
    refreshTableView();
  }

  /**
   * Deletes an entry from the tableview, if an entry has been selected. The method brings up a
   * popup window, asking for confirmation for deleting the entry.
   *
   * @param event A button click on the delete button
   */
  @Override
  public void handleDeleteBtn(javafx.event.ActionEvent event) {
  }

  /**
   * Method for synching the register with the tableview. The observable list to which the tableview
   * is set, is being refilled with all the entries in the register, keeping it updated with new
   * changes.
   */
  @Override
  public void refreshTableView() {
    this.inSum.setText("Sum: " + String.valueOf(incomeRegister.getTotalSum()));
    this.expenses.setAll(expenseRegister.getItems());
    this.expSum.setText("Sum: " + String.valueOf(expenseRegister.getTotalSum()));
  /**
   * Method for synching the pie charts to the registers.
   */
    this.incomePieChart.setData(createIncomePieChart());
    this.expensePieChart.setData(createExpensePieChart());
  /**
   * Method for adding income to the income register
   */
  @FXML
  private void handleAddIncome() {
    //Instantiate FXML loader and loads the popup for adding income
    FXMLLoader loader = new FXMLLoader();
    loader.setLocation(getClass().getResource("/view/AddIncome.fxml"));

    Income newIncome;
    String dialogTitle = "Add income";
    // Load the FXML file for your dialog box
    Dialog<Income> dialog = new Dialog<>();
    dialog.initModality(Modality.APPLICATION_MODAL);

    try {
      // Set the Dialog's content to the loaded FXML file
      dialog.getDialogPane().setContent(loader.load());
    } catch (IOException e) {
      showErrorDialogBox("Loading", "Error in loading dialog box", "Could not load"
          + "the AddIncome window");
    }

    // Get the controller for the loaded FXML file
    AddIncomeController dialogController = loader.getController();
    //Sets the title of the dialog box
    dialog.setTitle(dialogTitle);
    // Show the Dialog and wait for the user to close it
    dialog.showAndWait();
    //Get the newly created income from the dialog pane
    newIncome = dialogController.getNewIncome();

    //Adds the new item to the register
    if (newIncome != null) {
      incomeRegister.addItem(newIncome);
      incomePieChart.setData(createIncomePieChart());
  /**
   * Method for adding expense to the expense register.
   */
  @FXML
  private void handleAddExpense() {
    //Instantiate FXML loader and loads the popup for adding expense
    FXMLLoader loader = new FXMLLoader();
    loader.setLocation(getClass().getResource("/view/AddExpense.fxml"));

    Expense newExpense;
    String dialogTitle = "Add expense";
    Dialog<Expense> dialog = new Dialog<>();
    dialog.initModality(Modality.APPLICATION_MODAL);

    try {
      // Set the Dialog's content to the loaded FXML file
      dialog.getDialogPane().setContent(loader.load());
    } catch (IOException e) {
      showErrorDialogBox("Loading", "Error in loading dialog box", "Could not load"
          + "the AddExpense window");
    }

    // Get the controller for the loaded FXML file
    AddExpenseController dialogController = loader.getController();

    dialog.setTitle(dialogTitle);
    // Show the Dialog and wait for the user to close it
    dialog.showAndWait();

    //Get the newly created expense from the dialog pane
    newExpense = dialogController.getNewExpense();

    //Adds the new item to the register
    if (newExpense != null) {
      expenseRegister.addItem(newExpense);
      expensePieChart.setData(createExpensePieChart());
  /**
   * Method for editing a chosen income in the income register.

   * @param chosenIncome The chosen income.
   */
  @FXML
  private void handleEditIncome(Income chosenIncome) {
    //Create copy of chosenIncome before changes

    //Instantiate FXML loader and loads the popup for adding income
    FXMLLoader loader = new FXMLLoader();
    loader.setLocation(getClass().getResource("/view/AddIncome.fxml"));

    String dialogTitle = "Edit income";
    // Load the FXML file for your dialog box
    Dialog<Income> dialog = new Dialog<>();
    dialog.initModality(Modality.APPLICATION_MODAL);

    try {
      // Set the Dialog's content to the loaded FXML file
      dialog.getDialogPane().setContent(loader.load());
    } catch (IOException e) {
      showErrorDialogBox("Loading", "Error in loading dialog box", "Could not load"
          + "the EditIncome window");
    }

    // Get the controller for the loaded FXML file
    AddIncomeController dialogController = loader.getController();
    //Binds the selected item to another item which is defined in the ItemController
    dialogController.setIncome(chosenIncome);

    dialog.setTitle(dialogTitle);
    // Show the Dialog and wait for the user to close it
    dialog.showAndWait();
    incomePieChart.setData(createIncomePieChart());
  /**
   * Method for editing a chosen expense in the expense register.

   * @param chosenExpense The chosen expense
   */
  @FXML
  private void handleEditExpense(Expense chosenExpense) {
    //Instantiate FXML loader and loads the popup for adding expense
    FXMLLoader loader = new FXMLLoader();
    loader.setLocation(getClass().getResource("/view/AddExpense.fxml"));

    String dialogTitle = "Edit expense";
    // Load the FXML file for your dialog box
    Dialog<Expense> dialog = new Dialog<>();
    dialog.initModality(Modality.APPLICATION_MODAL);

    try {
      // Set the Dialog's content to the loaded FXML file
      dialog.getDialogPane().setContent(loader.load());
    } catch (IOException e) {
      showErrorDialogBox("Loading", "Error in loading dialog box", "Could not"
          + "load the EditExpense window");
    }

    // Get the controller for the loaded FXML file
    AddExpenseController dialogController = loader.getController();
    //Binds the selected item to another item which is defined in the ItemController
    dialogController.setExpense(chosenExpense);
    dialog.setTitle(dialogTitle);
    // Show the Dialog and wait for the user to close it
    dialog.showAndWait();
    this.expensePieChart.setData(createExpensePieChart());

  /**
   * Method for deleting a chosen income from the income register.

   * @param chosenIncome The chosen income.
   */
  @FXML
  private void handleDeleteIncome(Income chosenIncome) {
    String title = "Confirm Delete" ;
    String header = "Delete Confirmation";
    String content = "Are you sure you would like to delete the selected income?";
    Optional<ButtonType> isConfirmed = showConfirmationDialog(title, header, content);
    if (isConfirmed.isPresent() && isConfirmed.get() == ButtonType.OK) {
      incomeRegister.removeItem(chosenIncome);
    }
    this.incomePieChart.setData(createIncomePieChart());
  /**
   * Method for deleting a chosen expense from the expenses register.

   * @param chosenExpense The chosen expense.
   */
  @FXML
  private void handleDeleteExpense(Expense chosenExpense) {
    String title = "Confirm Delete" ;
    String header = "Delete Confirmation";
    String content = "Are you sure you would like to delete the selected expense?";
    Optional<ButtonType> isConfirmed = showConfirmationDialog(title, header, content);
    if (isConfirmed.isPresent() && isConfirmed.get() == ButtonType.OK) {
      expenseRegister.removeItem(chosenExpense);
    }
    this.expensePieChart.setData(createExpensePieChart());
  /**
   * Saves the changes made to the tableview by writing the information to a file.
   * @throws IOException If an error occurs while writing to the file.
   */
  @Override
  public void saveDataToFile() throws IOException {
    FileHandling.writeItemRegisterToFile(incomeRegister,
        "budgets/" + FileHandlingSelectedBudget
            .readSelectedBudget("budgets/SelectedBudget") + "/Income");
    FileHandling.writeItemRegisterToFile(expenseRegister,
        "budgets/" + FileHandlingSelectedBudget
            .readSelectedBudget("budgets/SelectedBudget") + "/Expense");
  /**
   * Method for automatically updating the disposable income label.
   */
  private void refreshDisposableIncome() {
    maxAmount.setText(String.valueOf(incomeRegister.getTotalSum() - expenseRegister.getTotalSum()));
  }

  /**
   * Writes the disposable income amount to the budget file as the max amount field.
   * @throws IOException if there is an input or exception.
   */
  private void saveDisposableIncomeToFile() throws IOException {
    String disposableIncomeAsString = String.valueOf(incomeRegister.getTotalSum() - expenseRegister.getTotalSum());
    FileHandlingBudget.writeMaxAmountToFile(
        "budgets/" + FileHandlingSelectedBudget
            .readSelectedBudget("budgets/SelectedBudget") + "/Budget", disposableIncomeAsString);
  }


  /**
   * Method for validating if the max amount is above 1.

   * @param sumIncome Sum of incomes.
   * @param sumExpense Sum of expenses.
   * @return True, if the amount is above 1. Else, returns false
   */
  private boolean isValidMaxAmount(double sumIncome, double sumExpense) {
    return sumIncome - sumExpense > 1;
   * Switches scenes back to main menu, by loading a new FXML file and setting the scene to this location.
   * @param event A button click on the return to main menu button
  public void switchScene(javafx.event.ActionEvent event) {
      FXMLLoader loader = new FXMLLoader();
      if (event.getSource() == returnToMainMenuBtn) {
        saveDataToFile();
        loader.setLocation(getClass().getResource("/view/MainMenuNew.fxml"));
        Optional<ButtonType> isConfirmed = showConfirmationDialog("Continuation confirmation",
            "Are you satisfied with your changes?",
            "You can still make changes before continuing");
        if (isConfirmed.isPresent() && isConfirmed.get() == ButtonType.OK) {
          loader.setLocation(getClass().getResource("/view/newBudgetBudgert.fxml"));
          saveDisposableIncomeToFile();
        } else return;
      } else if (event.getSource() == returnBtn) {
        Optional<ButtonType> isConfirmed = showConfirmationDialog("Return confirmation",
            "Are you sure you want to go back?", "The budget you are creating will be deleted");
        if (isConfirmed.isPresent() && isConfirmed.get() == ButtonType.OK) {
          loader.setLocation(getClass().getResource("/view/FirstMenu.fxml"));
          FileHandlingSelectedBudget.deleteBudgetDirectory("budgets/" + FileHandlingSelectedBudget
              .readSelectedBudget("budgets/SelectedBudget"));
          FileHandlingSelectedBudget.clearSelectedBudget("budgets/SelectedBudget");
        } else return;
      Parent root = loader.load();
      Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow();
      Scene scene = new Scene(root);
      stage.setScene(scene);
      stage.show();
    } catch(IOException ioe) {
      ioe.printStackTrace();
        showErrorDialogBox("Loading error", "Error in loading", "Could not save"