package no.ntnu.idatt1002.demo.controller; import java.io.IOException; import java.time.LocalDate; import java.util.Optional; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; 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.Dialog; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; import javafx.stage.Modality; import javafx.stage.Stage; 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 Label inSum; @FXML private Label expSum; @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 Button returnBtn; @FXML private Button continueBtn; @FXML private Label maxAmount; @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. */ @FXML public void initialize() { //Initialize columns setColumns(); try { //Initialize registers 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", ""); } //Set data for tableviews income = FXCollections.observableArrayList(incomeRegister.getItems()); incomeTableView.setItems(income); expenses = FXCollections.observableArrayList(expenseRegister.getItems()); expenseTableView.setItems(expenses); //Format pie charts incomePieChart.setLegendSide(Side.RIGHT); incomePieChart.setLabelLineLength(10); expensePieChart.setLegendSide(Side.RIGHT); expensePieChart.setLabelLineLength(10); //Update pie charts and set date refreshPieChart(); formatDatePicker(); //Initialize sum field under the tableview 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. */ private void setColumns() { 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() { return FXCollections.observableArrayList( 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. */ @Override public void formatDatePicker() { 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. * @param event A button click on the add button. */ @Override 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(); refreshDisposableIncome(); } } /** * 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); } else return; //Updates the tableview and pie chart using the register refreshTableView(); refreshDisposableIncome(); } /** * 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) { handleEditBtn(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.income.setAll(incomeRegister.getItems()); 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. */ @Override public void refreshPieChart() { 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); //update just the income pie chart 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); //Update just the expense pie chart. 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(); //Refresh just the income pie chart 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(); //Update just the expense pie chart 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); } //Update pie chart 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); } //Update pie chart 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 */ @FXML public void switchScene(javafx.event.ActionEvent event) { try { //Determine which scene to switch to FXMLLoader loader = new FXMLLoader(); if (event.getSource() == returnToMainMenuBtn) { saveDataToFile(); loader.setLocation(getClass().getResource("/view/MainMenuNew.fxml")); } else if (event.getSource() == continueBtn) { 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; } //Load the scene 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" + "to file"); } } }