From 050f5921363d06e137d2e171d6661ac377633668 Mon Sep 17 00:00:00 2001 From: emilfa Date: Fri, 20 Mar 2026 12:49:35 +0100 Subject: [PATCH 01/98] refactor: replaced all the page controllers with one PageController --- src/main/java/edu/group5/app/App.java | 11 ++- .../app/control/BrowseCardController.java | 14 ---- .../app/control/BrowsePageController.java | 9 --- .../group5/app/control/HeaderController.java | 28 ------- .../app/control/HomePageController.java | 19 ----- .../app/control/LoginPageController.java | 13 ---- .../group5/app/control/MainController.java | 46 +++++------ .../control/OrganizationPageController.java | 13 ---- .../group5/app/control/PageController.java | 66 ++++++++++++++++ .../app/control/SignInPageController.java | 17 ---- .../donationpage/DonationPageController.java | 16 ---- .../PaymentCompleteController.java | 15 ---- src/main/java/edu/group5/app/view/Header.java | 6 +- .../java/edu/group5/app/view/MainView.java | 78 ++++++++++--------- .../app/view/browsepage/BrowseCard.java | 1 - .../app/view/browsepage/BrowsePageView.java | 17 +--- .../view/donationpage/DonationPageView.java | 1 - .../app/view/homepage/HomePageView.java | 12 +-- .../app/view/loginpage/LoginHeader.java | 1 - .../app/view/loginpage/LoginPageView.java | 3 - .../app/view/loginpage/SignInPageView.java | 2 - .../OrganizationPageView.java | 2 - .../app/view/userpage/UserPageView.java | 1 - 23 files changed, 147 insertions(+), 244 deletions(-) delete mode 100644 src/main/java/edu/group5/app/control/BrowseCardController.java delete mode 100644 src/main/java/edu/group5/app/control/BrowsePageController.java delete mode 100644 src/main/java/edu/group5/app/control/HeaderController.java delete mode 100644 src/main/java/edu/group5/app/control/HomePageController.java delete mode 100644 src/main/java/edu/group5/app/control/LoginPageController.java delete mode 100644 src/main/java/edu/group5/app/control/OrganizationPageController.java create mode 100644 src/main/java/edu/group5/app/control/PageController.java delete mode 100644 src/main/java/edu/group5/app/control/SignInPageController.java delete mode 100644 src/main/java/edu/group5/app/control/donationpage/DonationPageController.java delete mode 100644 src/main/java/edu/group5/app/control/donationpage/PaymentCompleteController.java diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index 6704bd2..b9f0db4 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -1,6 +1,9 @@ package edu.group5.app; import edu.group5.app.control.MainController; +import edu.group5.app.control.wrapper.DbWrapper; +import edu.group5.app.control.wrapper.OrgApiWrapper; +import edu.group5.app.model.organization.OrganizationRepository; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.image.Image; @@ -11,11 +14,15 @@ */ public class App extends Application { @Override - public void start(Stage stage) { + public void start(Stage stage) throws InterruptedException { + OrgApiWrapper orgApiWrapper = new OrgApiWrapper("https://app.innsamlingskontrollen.no/api/public/v1/all"); + orgApiWrapper.importData(); + OrganizationRepository organizationRepository = new OrganizationRepository(orgApiWrapper.getData()); + MainController controller = new MainController(); Scene scene = controller.getMainView().getScene(); - controller.showLoginPage(); + controller.showHomePage(); stage.getIcons().add(new Image(getClass().getResource("/header/images/hmh-logo.png").toExternalForm())); stage.setTitle("Help-Me-Help"); diff --git a/src/main/java/edu/group5/app/control/BrowseCardController.java b/src/main/java/edu/group5/app/control/BrowseCardController.java deleted file mode 100644 index 297ba9f..0000000 --- a/src/main/java/edu/group5/app/control/BrowseCardController.java +++ /dev/null @@ -1,14 +0,0 @@ -package edu.group5.app.control; - -public class BrowseCardController { - private final MainController controller; - - public BrowseCardController(MainController mainController) { - this.controller = mainController; - } - - public void handleCardClick() { - System.out.println("Browse Card Clicked"); - controller.showOrganizationPage(); - } -} diff --git a/src/main/java/edu/group5/app/control/BrowsePageController.java b/src/main/java/edu/group5/app/control/BrowsePageController.java deleted file mode 100644 index 0922e68..0000000 --- a/src/main/java/edu/group5/app/control/BrowsePageController.java +++ /dev/null @@ -1,9 +0,0 @@ -package edu.group5.app.control; - -public class BrowsePageController { - private final MainController controller; - - public BrowsePageController(MainController mainController) { - this.controller = mainController; - } -} diff --git a/src/main/java/edu/group5/app/control/HeaderController.java b/src/main/java/edu/group5/app/control/HeaderController.java deleted file mode 100644 index 0a1e424..0000000 --- a/src/main/java/edu/group5/app/control/HeaderController.java +++ /dev/null @@ -1,28 +0,0 @@ -package edu.group5.app.control; - -public class HeaderController { - private final MainController controller; - - public HeaderController(MainController controller) { - this.controller = controller; - } - - public void handleHomeBtn() { - System.out.println("Home button pressed"); - controller.showHomePage(); - } - - public void handleCausesBtn() { - System.out.println("Causes button pressed"); - controller.showBrowsePage(); - } - - public void handleAboutBtn() { - System.out.println("About button pressed"); - } - - public void handleProfileBtn() { - System.out.println("profileSection"); - controller.showUserPage(); - } -} diff --git a/src/main/java/edu/group5/app/control/HomePageController.java b/src/main/java/edu/group5/app/control/HomePageController.java deleted file mode 100644 index 10d3fbf..0000000 --- a/src/main/java/edu/group5/app/control/HomePageController.java +++ /dev/null @@ -1,19 +0,0 @@ -package edu.group5.app.control; - -public class HomePageController { - private final MainController controller; - - public HomePageController(MainController controller) { - this.controller = controller; - } - - public void handleDonateToACauseBtn() { - System.out.println("Donate to a cause button pressed"); - controller.showBrowsePage(); - } - - public void handleAboutUsBtn() { - System.out.println("About us button pressed"); - controller.showAboutUsPage(); - } -} diff --git a/src/main/java/edu/group5/app/control/LoginPageController.java b/src/main/java/edu/group5/app/control/LoginPageController.java deleted file mode 100644 index a06d135..0000000 --- a/src/main/java/edu/group5/app/control/LoginPageController.java +++ /dev/null @@ -1,13 +0,0 @@ -package edu.group5.app.control; - -public class LoginPageController { - private final MainController controller; - - public LoginPageController(MainController controller) { - this.controller = controller; - } - public void handleRegisterBtn() { - System.out.println("Sign in button pressed"); - controller.showSignInPage(); - } -} diff --git a/src/main/java/edu/group5/app/control/MainController.java b/src/main/java/edu/group5/app/control/MainController.java index 8106975..7f48f19 100644 --- a/src/main/java/edu/group5/app/control/MainController.java +++ b/src/main/java/edu/group5/app/control/MainController.java @@ -1,49 +1,41 @@ package edu.group5.app.control; -import edu.group5.app.control.donationpage.DonationPageController; import edu.group5.app.model.user.User; import edu.group5.app.view.MainView; -import edu.group5.app.view.donationpage.DonationPageView; public class MainController { private final MainView view; - private final HeaderController headerController; - private final HomePageController homePageController; - private final BrowsePageController browsePageController; - private final BrowseCardController browseCardController; - private final OrganizationPageController organizationPageController; - private final DonationPageController donationPageController; + private final PageController pageController; private User currentUser; public MainController() { this.view = new MainView(this); - this.headerController = new HeaderController(this); - this.homePageController = new HomePageController(this); - this.browsePageController = new BrowsePageController(this); - this.browseCardController = new BrowseCardController(this); - this.organizationPageController = new OrganizationPageController(this); - this.donationPageController = new DonationPageController(this); + this.pageController = new PageController(this); } - public void setCurrentUser(User user) { - this.currentUser = user; + public PageController getPageController() { + return this.pageController; } - public User getCurrentUser() { - return this.currentUser; - } - - public void logout() { - currentUser = null; - showLoginPage(); - } +// public void setCurrentUser(User user) { +// this.currentUser = user; +// } +// +// public User getCurrentUser() { +// return this.currentUser; +// } +// +// public void logout() { +// currentUser = null; +// showLoginPage(); +// } public MainView getMainView() { return view; } public void showHomePage() { - view.showHomePage(homePageController, headerController); + view.showHomePage(); } public void showLoginPage() { @@ -56,11 +48,11 @@ public void showPaymentCompletePage() { view.showPaymentCompletePage(); } public void showBrowsePage() { - view.showBrowsePage(browsePageController, browseCardController, headerController); + view.showBrowsePage(); } public void showOrganizationPage() { - view.showOrganizationPage(organizationPageController, headerController); + view.showOrganizationPage(); } public void showDonationPage() { diff --git a/src/main/java/edu/group5/app/control/OrganizationPageController.java b/src/main/java/edu/group5/app/control/OrganizationPageController.java deleted file mode 100644 index dd8f1bf..0000000 --- a/src/main/java/edu/group5/app/control/OrganizationPageController.java +++ /dev/null @@ -1,13 +0,0 @@ -package edu.group5.app.control; - -public class OrganizationPageController { - private final MainController controller; - - public OrganizationPageController(MainController controller) { - this.controller = controller; - } - - public void handleDonateClick() { - controller.showDonationPage(); - } -} diff --git a/src/main/java/edu/group5/app/control/PageController.java b/src/main/java/edu/group5/app/control/PageController.java new file mode 100644 index 0000000..0f52eb7 --- /dev/null +++ b/src/main/java/edu/group5/app/control/PageController.java @@ -0,0 +1,66 @@ +package edu.group5.app.control; + +public class PageController { + private final MainController controller; + + public PageController(MainController controller) { + this.controller = controller; + } + + public void handleHomeBtn() { + System.out.println("Home button pressed"); + controller.showHomePage(); + } + + public void handleCausesBtn() { + System.out.println("Causes button pressed"); + controller.showBrowsePage(); + } + + public void handleAboutBtn() { + System.out.println("About button pressed"); + } + + public void handleProfileBtn() { + System.out.println("profileSection"); + controller.showUserPage(); + } + + public void handleDonateToACauseBtn() { + System.out.println("Donate to a cause button pressed"); + controller.showBrowsePage(); + } + + public void handleRegisterBtn() { + System.out.println("Sign in button pressed"); + controller.showSignInPage(); + } + + public void handleDonateClick() { + controller.showDonationPage(); + } + + + public void handleAboutUsBtn() { + System.out.println("About us button pressed"); + controller.showAboutUsPage(); + } + + public void handleBrowseCardClick() { + controller.showDonationPage(); + } + + public void handleSignInBtn() { + System.out.println("Sign in button pressed"); + controller.showHomePage(); + } + public void handleLoginBtn() { + System.out.println("Back to login button pressed"); + controller.showLoginPage(); + } + + public void handleDonationBtn() { + System.out.println("Donating"); + controller.showPaymentCompletePage(); + } +} diff --git a/src/main/java/edu/group5/app/control/SignInPageController.java b/src/main/java/edu/group5/app/control/SignInPageController.java deleted file mode 100644 index a51e4d5..0000000 --- a/src/main/java/edu/group5/app/control/SignInPageController.java +++ /dev/null @@ -1,17 +0,0 @@ -package edu.group5.app.control; - -public class SignInPageController { - private final MainController controller; - - public SignInPageController(MainController controller) { - this.controller = controller; - } - public void handleSignInBtn() { - System.out.println("Sign in button pressed"); - controller.showHomePage(); - } - public void handleLoginBtn() { - System.out.println("Back to login button pressed"); - controller.showLoginPage(); - } -} diff --git a/src/main/java/edu/group5/app/control/donationpage/DonationPageController.java b/src/main/java/edu/group5/app/control/donationpage/DonationPageController.java deleted file mode 100644 index 248c4a1..0000000 --- a/src/main/java/edu/group5/app/control/donationpage/DonationPageController.java +++ /dev/null @@ -1,16 +0,0 @@ -package edu.group5.app.control.donationpage; - -import edu.group5.app.control.MainController; - -public class DonationPageController { - private final MainController controller; - - public DonationPageController(MainController controller) { - this.controller = controller; - } - public void handleDonationBtn() { - System.out.println("Donating"); - controller.showPaymentCompletePage(); - } - -} diff --git a/src/main/java/edu/group5/app/control/donationpage/PaymentCompleteController.java b/src/main/java/edu/group5/app/control/donationpage/PaymentCompleteController.java deleted file mode 100644 index d1bc3a5..0000000 --- a/src/main/java/edu/group5/app/control/donationpage/PaymentCompleteController.java +++ /dev/null @@ -1,15 +0,0 @@ -package edu.group5.app.control.donationpage; - -import edu.group5.app.control.MainController; - -public class PaymentCompleteController { - private final MainController controller; - - public PaymentCompleteController(MainController controller) { - this.controller = controller; - } - public void handleHomeBtn() { - System.out.println("Home button pressed"); - controller.showHomePage(); - } -} diff --git a/src/main/java/edu/group5/app/view/Header.java b/src/main/java/edu/group5/app/view/Header.java index 1edc5e4..4bc8284 100644 --- a/src/main/java/edu/group5/app/view/Header.java +++ b/src/main/java/edu/group5/app/view/Header.java @@ -1,6 +1,6 @@ package edu.group5.app.view; -import edu.group5.app.control.HeaderController; +import edu.group5.app.control.PageController; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.image.Image; @@ -8,9 +8,9 @@ import javafx.scene.layout.*; public class Header extends BorderPane { - private final HeaderController controller; + private final PageController controller; - public Header(HeaderController controller) { + public Header(PageController controller) { this.controller = controller; getStylesheets().add(getClass().getResource("/header/header.css").toExternalForm()); setId("header"); diff --git a/src/main/java/edu/group5/app/view/MainView.java b/src/main/java/edu/group5/app/view/MainView.java index 27eeac5..42999ff 100644 --- a/src/main/java/edu/group5/app/view/MainView.java +++ b/src/main/java/edu/group5/app/view/MainView.java @@ -1,6 +1,7 @@ package edu.group5.app.view; import edu.group5.app.control.*; +import edu.group5.app.model.organization.OrganizationRepository; import edu.group5.app.view.browsepage.BrowsePageView; import edu.group5.app.control.*; import edu.group5.app.control.donationpage.DonationPageController; @@ -16,60 +17,65 @@ import javafx.scene.Scene; import javafx.scene.layout.BorderPane; -public class MainView { - private final HeaderController headerController; - private final HomePageController homePageController; - private final LoginPageController loginPageController; - private final SignInPageController signInPageController; - private final DonationPageController donationPageController; - private final PaymentCompleteController paymentCompleteController; - private final Scene scene; - private final BorderPane root; +public class MainView extends BorderPane { + private final MainController controller; + private final HomePageView homePageView; + private final LoginPageView loginPageView; + private final SignInPageView signInPageView; + private final BrowsePageView browsePageView; + private final OrganizationPageView organizationPageView; + private final DonationPageView donationPageView; + private final PaymentCompletePageView paymentCompletePageView; + private final UserPageView userPageView; - public MainView(MainController mainController) { - this.headerController = new HeaderController(mainController); - this.homePageController = new HomePageController(mainController); - this.loginPageController = new LoginPageController(mainController); - this.signInPageController = new SignInPageController(mainController); - this.donationPageController = new DonationPageController(mainController); - this.paymentCompleteController = new PaymentCompleteController(mainController); - this.root = new BorderPane(); - this.scene = new Scene(root, 1280, 720); - } + public MainView(MainController controller) { + this.controller = controller; + Header header = new Header(controller.getPageController()); + this.homePageView = new HomePageView(); + this.loginPageView = new LoginPageView(); + this.signInPageView = new SignInPageView(); + this.browsePageView = new BrowsePageView(); + this.organizationPageView = new OrganizationPageView(); + this.donationPageView = new DonationPageView(); + this.paymentCompletePageView = new PaymentCompletePageView(); + this.userPageView = new UserPageView(); - public Scene getScene() { - return this.scene; - } + Scene root = new Scene(this, 1280, 720); + + setTop(header); - public Scene createView() { - root.setCenter(new LoginPageView(loginPageController)); - return new Scene(root, 1280, 720); +// this.headerController = new HeaderController(mainController); +// this.homePageController = new HomePageController(mainController); +// this.loginPageController = new LoginPageController(mainController); +// this.signInPageController = new SignInPageController(mainController); +// this.donationPageController = new DonationPageController(mainController); +// this.paymentCompleteController = new PaymentCompleteController(mainController); } - public void showHomePage(HomePageController homePageController, HeaderController headerController) { - root.setCenter(new HomePageView(homePageController, headerController)); + public void showHomePage() { + setCenter(this.homePageView); } public void showLoginPage() { - root.setCenter(new LoginPageView(loginPageController)); + setCenter(this.loginPageView); } - public void showBrowsePage(BrowsePageController browsePageController, BrowseCardController browseCardController, HeaderController headerController) { - root.setCenter(new BrowsePageView(getScene(), browsePageController, browseCardController, headerController)); + public void showBrowsePage() { + setCenter(this.browsePageView); } - public void showOrganizationPage(OrganizationPageController organizationController, HeaderController headerController) { - root.setCenter(new OrganizationPageView(organizationController, headerController)); + public void showOrganizationPage() { + setCenter(this.organizationPageView); } public void showSignInPage() { - root.setCenter(new SignInPageView(signInPageController)); + setCenter(this.signInPageView); } public void showDonationPage() { - root.setCenter(new DonationPageView(donationPageController, headerController)); + setCenter(this.donationPageView); } public void showPaymentCompletePage() { - root.setCenter(new PaymentCompletePageView(paymentCompleteController)); + setCenter(this.paymentCompletePageView); } public void showAboutUsPage() {} @@ -82,6 +88,6 @@ public void showUserPage() { "aurafarmer@gmail.com", "hashedpassword" ); - root.setCenter(new UserPageView(headerController, testCustomer)); + setCenter(this.userPageView); } } diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java index 02cc094..5184614 100644 --- a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java +++ b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java @@ -1,6 +1,5 @@ package edu.group5.app.view.browsepage; -import edu.group5.app.control.BrowseCardController; import javafx.geometry.Pos; import javafx.scene.image.Image; import javafx.scene.image.ImageView; diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java index 4cb54b2..5ecce7e 100644 --- a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java +++ b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java @@ -1,27 +1,18 @@ package edu.group5.app.view.browsepage; -import edu.group5.app.control.BrowseCardController; -import edu.group5.app.control.BrowsePageController; -import edu.group5.app.control.HeaderController; -import edu.group5.app.view.Header; import javafx.geometry.Pos; -import javafx.scene.Scene; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextField; import javafx.scene.layout.*; public class BrowsePageView extends BorderPane { - private final Scene scene; private final BrowsePageController controller; - private final BrowseCardController orgController; + private final BrowseCardController browseCardController; - public BrowsePageView(Scene mainScene, BrowsePageController browsePageController, BrowseCardController browseCardController, HeaderController headerController) { - this.scene = mainScene; + public BrowsePageView(BrowsePageController browsePageController, BrowseCardController browseCardController) { this.controller = browsePageController; - this.orgController = browseCardController; + this.browseCardController = browseCardController; getStylesheets().add(getClass().getResource("/browsepage/browsepage.css").toExternalForm()); - Header headerView = new Header(headerController); - setTop(headerView); setCenter(createBody()); } @@ -57,7 +48,7 @@ private GridPane createOrganizationSection() { int column = 0; int row = 0; for (int i = 0; i < 16; i++) { - BrowseCard card = new BrowseCard(orgController, "/browsepage/images/children_of_shambala.png", "Shambala Foundation"); + BrowseCard card = new BrowseCard(browseCardController, "/browsepage/images/children_of_shambala.png", "Shambala Foundation"); GridPane.setFillWidth(card, true); grid.setAlignment(Pos.CENTER); diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 5cf7171..49b0d7b 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -1,6 +1,5 @@ package edu.group5.app.view.donationpage; -import edu.group5.app.control.HeaderController; import edu.group5.app.control.donationpage.DonationPageController; import edu.group5.app.view.Header; import javafx.geometry.Insets; diff --git a/src/main/java/edu/group5/app/view/homepage/HomePageView.java b/src/main/java/edu/group5/app/view/homepage/HomePageView.java index 5510daa..e61c484 100644 --- a/src/main/java/edu/group5/app/view/homepage/HomePageView.java +++ b/src/main/java/edu/group5/app/view/homepage/HomePageView.java @@ -1,8 +1,6 @@ package edu.group5.app.view.homepage; -import edu.group5.app.control.HeaderController; -import edu.group5.app.control.HomePageController; -import edu.group5.app.view.Header; +import edu.group5.app.control.PageController; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.ScrollPane; @@ -12,13 +10,11 @@ import javafx.scene.text.Text; public class HomePageView extends BorderPane { - private final HomePageController controller; + private final PageController controller; - public HomePageView(HomePageController homePageController, HeaderController headerController) { - this.controller = homePageController; + public HomePageView(PageController controller) { + this.controller = controller; getStylesheets().add(getClass().getResource("/homepage/homepage.css").toExternalForm()); - Header headerView = new Header(headerController); - setTop(headerView); setCenter(createBody()); } diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java index decd5a1..793edb9 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java @@ -1,6 +1,5 @@ package edu.group5.app.view.loginpage; -import edu.group5.app.control.HeaderController; import javafx.geometry.Pos; import javafx.scene.image.Image; import javafx.scene.image.ImageView; diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java index d555301..b08423c 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java @@ -1,9 +1,6 @@ package edu.group5.app.view.loginpage; -import edu.group5.app.control.HeaderController; -import edu.group5.app.control.LoginPageController; -import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; diff --git a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java index 1c5ddae..462a1a5 100644 --- a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java @@ -1,7 +1,5 @@ package edu.group5.app.view.loginpage; -import edu.group5.app.control.HeaderController; -import edu.group5.app.control.SignInPageController; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 4155ccd..ca0d8e6 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -1,7 +1,5 @@ package edu.group5.app.view.organizationpage; -import edu.group5.app.control.HeaderController; -import edu.group5.app.control.OrganizationPageController; import edu.group5.app.view.Header; import javafx.geometry.Pos; import javafx.scene.control.Button; diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index 547b812..152c281 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -1,6 +1,5 @@ package edu.group5.app.view.userpage; -import edu.group5.app.control.HeaderController; import edu.group5.app.model.user.Customer; import edu.group5.app.view.Header; import javafx.geometry.Insets; From c3b2cacf9565290cc99bb074eb05c76caf6e0730 Mon Sep 17 00:00:00 2001 From: emilfa Date: Fri, 20 Mar 2026 12:52:31 +0100 Subject: [PATCH 02/98] refactor: made application run with new controller structure --- .../java/edu/group5/app/view/MainView.java | 19 ++++++++----------- .../app/view/browsepage/BrowseCard.java | 9 +++++---- .../app/view/browsepage/BrowsePageView.java | 11 +++++------ .../view/donationpage/DonationPageView.java | 12 ++++-------- .../donationpage/PaymentCompletePageView.java | 8 ++++---- .../app/view/loginpage/LoginPageView.java | 7 ++++--- .../app/view/loginpage/SignInPageView.java | 7 ++++--- .../OrganizationPageView.java | 7 +++---- .../app/view/userpage/UserPageView.java | 8 ++++---- 9 files changed, 41 insertions(+), 47 deletions(-) diff --git a/src/main/java/edu/group5/app/view/MainView.java b/src/main/java/edu/group5/app/view/MainView.java index 42999ff..3ebf457 100644 --- a/src/main/java/edu/group5/app/view/MainView.java +++ b/src/main/java/edu/group5/app/view/MainView.java @@ -1,11 +1,8 @@ package edu.group5.app.view; import edu.group5.app.control.*; -import edu.group5.app.model.organization.OrganizationRepository; import edu.group5.app.view.browsepage.BrowsePageView; import edu.group5.app.control.*; -import edu.group5.app.control.donationpage.DonationPageController; -import edu.group5.app.control.donationpage.PaymentCompleteController; import edu.group5.app.model.user.Customer; import edu.group5.app.view.donationpage.DonationPageView; import edu.group5.app.view.donationpage.PaymentCompletePageView; @@ -31,14 +28,14 @@ public class MainView extends BorderPane { public MainView(MainController controller) { this.controller = controller; Header header = new Header(controller.getPageController()); - this.homePageView = new HomePageView(); - this.loginPageView = new LoginPageView(); - this.signInPageView = new SignInPageView(); - this.browsePageView = new BrowsePageView(); - this.organizationPageView = new OrganizationPageView(); - this.donationPageView = new DonationPageView(); - this.paymentCompletePageView = new PaymentCompletePageView(); - this.userPageView = new UserPageView(); + this.homePageView = new HomePageView(controller.getPageController()); + this.loginPageView = new LoginPageView(controller.getPageController()); + this.signInPageView = new SignInPageView(controller.getPageController()); + this.browsePageView = new BrowsePageView(controller.getPageController()); + this.organizationPageView = new OrganizationPageView(controller.getPageController()); + this.donationPageView = new DonationPageView(controller.getPageController()); + this.paymentCompletePageView = new PaymentCompletePageView(controller.getPageController()); + this.userPageView = new UserPageView(controller.getPageController(), new Customer(1, "John", "Pedersen", "jh@gmail.com", "1234")); Scene root = new Scene(this, 1280, 720); diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java index 5184614..3a46244 100644 --- a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java +++ b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java @@ -1,5 +1,6 @@ package edu.group5.app.view.browsepage; +import edu.group5.app.control.PageController; import javafx.geometry.Pos; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -8,10 +9,10 @@ import javafx.scene.text.Text; public class BrowseCard extends VBox { - private final BrowseCardController controller; + private final PageController controller; - public BrowseCard(BrowseCardController browseCardController, String img, String name) { - this.controller = browseCardController; + public BrowseCard(PageController controller, String img, String name) { + this.controller = controller; setId("mainContainer"); getStylesheets().add(getClass().getResource("/browsepage/browse_org.css").toExternalForm()); @@ -22,7 +23,7 @@ public BrowseCard(BrowseCardController browseCardController, String img, String ); setOnMouseClicked(e -> { - controller.handleCardClick(); + controller.handleBrowseCardClick(); }); setSpacing(20); diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java index 5ecce7e..b529765 100644 --- a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java +++ b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java @@ -1,17 +1,16 @@ package edu.group5.app.view.browsepage; +import edu.group5.app.control.PageController; import javafx.geometry.Pos; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextField; import javafx.scene.layout.*; public class BrowsePageView extends BorderPane { - private final BrowsePageController controller; - private final BrowseCardController browseCardController; + private final PageController controller; - public BrowsePageView(BrowsePageController browsePageController, BrowseCardController browseCardController) { - this.controller = browsePageController; - this.browseCardController = browseCardController; + public BrowsePageView(PageController controller) { + this.controller = controller; getStylesheets().add(getClass().getResource("/browsepage/browsepage.css").toExternalForm()); setCenter(createBody()); } @@ -48,7 +47,7 @@ private GridPane createOrganizationSection() { int column = 0; int row = 0; for (int i = 0; i < 16; i++) { - BrowseCard card = new BrowseCard(browseCardController, "/browsepage/images/children_of_shambala.png", "Shambala Foundation"); + BrowseCard card = new BrowseCard(controller, "/browsepage/images/children_of_shambala.png", "Shambala Foundation"); GridPane.setFillWidth(card, true); grid.setAlignment(Pos.CENTER); diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 49b0d7b..f521657 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -1,7 +1,6 @@ package edu.group5.app.view.donationpage; -import edu.group5.app.control.donationpage.DonationPageController; -import edu.group5.app.view.Header; +import edu.group5.app.control.PageController; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -14,15 +13,12 @@ import javafx.scene.text.TextAlignment; public class DonationPageView extends BorderPane { - private final DonationPageController controller; + private final PageController controller; - public DonationPageView(DonationPageController donationPageController, HeaderController headerController) { - this.controller = donationPageController; + public DonationPageView(PageController controller) { + this.controller = controller; getStylesheets().add(getClass().getResource("/donationpage/donation.css").toExternalForm()); - Header headerView = new Header(headerController); - setTop(headerView); - VBox content = new VBox(); content.getChildren().addAll(createDonationGrid(), createDonateSection()); setCenter(content); diff --git a/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java b/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java index d563fef..2cd407b 100644 --- a/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java @@ -1,6 +1,6 @@ package edu.group5.app.view.donationpage; -import edu.group5.app.control.donationpage.PaymentCompleteController; +import edu.group5.app.control.PageController; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -13,10 +13,10 @@ import java.util.Objects; public class PaymentCompletePageView extends BorderPane { - private final PaymentCompleteController controller; + private final PageController controller; - public PaymentCompletePageView(PaymentCompleteController paymentCompleteController) { - this.controller = paymentCompleteController; + public PaymentCompletePageView(PageController controller) { + this.controller = controller; getStylesheets().add(getClass().getResource("/donationpage/paymentcomplete.css").toExternalForm()); VBox content = new VBox(20); diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java index b08423c..ba306ee 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java @@ -1,6 +1,7 @@ package edu.group5.app.view.loginpage; +import edu.group5.app.control.PageController; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -11,10 +12,10 @@ import java.util.Objects; public class LoginPageView extends BorderPane { - private final LoginPageController controller; + private final PageController controller; - public LoginPageView(LoginPageController loginPageController) { - this.controller = loginPageController; + public LoginPageView(PageController controller) { + this.controller = controller; LoginHeader loginHeaderView = new LoginHeader(); setTop(loginHeaderView); diff --git a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java index 462a1a5..01b0a44 100644 --- a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java @@ -1,5 +1,6 @@ package edu.group5.app.view.loginpage; +import edu.group5.app.control.PageController; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -11,10 +12,10 @@ public class SignInPageView extends BorderPane { - private final SignInPageController controller; + private final PageController controller; - public SignInPageView(SignInPageController signInPageController) { - this.controller = signInPageController; + public SignInPageView(PageController controller) { + this.controller = controller; setTop(new LoginHeader()); HBox content = new HBox(); diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index ca0d8e6..24ee8f7 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -1,5 +1,6 @@ package edu.group5.app.view.organizationpage; +import edu.group5.app.control.PageController; import edu.group5.app.view.Header; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -14,13 +15,11 @@ import javafx.scene.text.Text; public class OrganizationPageView extends BorderPane { - private final OrganizationPageController controller; + private final PageController controller; - public OrganizationPageView(OrganizationPageController controller, HeaderController headerController) { + public OrganizationPageView(PageController controller) { this.controller = controller; getStylesheets().add(getClass().getResource("/organizationpage/organizationpage.css").toExternalForm()); - Header headerView = new Header(headerController); - setTop(headerView); setCenter(createBody()); } diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index 152c281..2678ae8 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -1,5 +1,6 @@ package edu.group5.app.view.userpage; +import edu.group5.app.control.PageController; import edu.group5.app.model.user.Customer; import edu.group5.app.view.Header; import javafx.geometry.Insets; @@ -15,15 +16,14 @@ public class UserPageView extends BorderPane { + private final PageController controller; private final Customer customer; - public UserPageView(HeaderController headerController, Customer customer) { + public UserPageView(PageController controller, Customer customer) { + this.controller = controller; this.customer = customer; getStylesheets().add(getClass().getResource("/userpage/userpage.css").toExternalForm()); - Header headerView = new Header(headerController); - setTop(headerView); - VBox content = new VBox(30); content.setPadding(new Insets(40)); content.getChildren().addAll(createProfileSection(), createCausesSection(), createDonationsSection()); From 316d43c6872bf0db73fac086b5ace98ef58f8f83 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 24 Mar 2026 13:30:26 +0100 Subject: [PATCH 03/98] feat[utils]: Add ParameterValidator class increasing SoC and SRP --- .../group5/app/utils/ParameterValidator.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/main/java/edu/group5/app/utils/ParameterValidator.java diff --git a/src/main/java/edu/group5/app/utils/ParameterValidator.java b/src/main/java/edu/group5/app/utils/ParameterValidator.java new file mode 100644 index 0000000..1181e11 --- /dev/null +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -0,0 +1,72 @@ +package edu.group5.app.utils; + +import java.math.BigDecimal; +/** + * ParameterValidator is a utility class that provides static methods for validating various types of parameters. + * It includes methods for checking strings, integers, objects, and BigDecimal values to ensure they meet specific + * criteria such as not being null, not being blank, or being positive. + * + */ +public final class ParameterValidator { + + /** + * Validates that a string parameter is not null and not blank. + * @param stringArg the string parameter to validate + * @param variableName the name of the variable being validated, used in exception messages + * @throws IllegalArgumentException if the string is null or blank + */ + public static final void stringChecker(String stringArg, String variableName) throws IllegalArgumentException { + nullCheck(stringArg, variableName); + if (stringArg.isBlank()) { + throw new IllegalArgumentException(String.format("%s can't be blank", variableName)); + } + } + + /** + * Validates that an integer parameter is not null and is a positive integer. + * @param intArg the integer parameter to validate + * @param variableName the name of the variable being validated, used in exception messages + * @throws IllegalArgumentException if the integer is null or not a positive integer + */ + public static final void intChecker(int intArg, String variableName) throws IllegalArgumentException { + nullCheck(intArg, variableName); + if (intArg <= 0) { + throw new IllegalArgumentException(String.format("%s must be a positive integer", variableName)); + } + } + + /** + * Validates that an object parameter is not null. + * @param objectArg the object parameter to validate + * @param variableName the name of the variable being validated, used in exception messages + * @throws IllegalArgumentException if the object is null + */ + public static final void objectChecker(Object objectArg, String variableName) throws IllegalArgumentException { + nullCheck(objectArg, variableName); + } + + /** + * Validates that a BigDecimal parameter is not null and is greater than zero. + * @param bigDecimalArg the BigDecimal parameter to validate + * @param variableName the name of the variable being validated, used in exception messages + * @throws IllegalArgumentException if the BigDecimal is null or not greater than zero + */ + public static final void bigDecimalChecker(BigDecimal bigDecimalArg, String variableName) throws IllegalArgumentException { + nullCheck(bigDecimalArg, variableName); + if (bigDecimalArg.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException(String.format("%s must be larger than 0", variableName)); + } + } + + /** + * Helper method to check if a variable is null and throw an IllegalArgumentException with a formatted message if it is. + * @param variable the variable to check for null + * @param variableName the name of the variable being checked, used in the exception message + * @throws IllegalArgumentException if the variable is null + */ + private static final void nullCheck(Object variable, String variableName) throws IllegalArgumentException { + if (variable == null) { + throw new IllegalArgumentException(String.format("%s can't be null", variableName)); + } + } +} From 218a32f63920d0b73cc51c117ef8af169ada50b3 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 24 Mar 2026 14:51:39 +0100 Subject: [PATCH 04/98] update&test[utils]: Add positive and negative JUnit tests to ParameterValidator class, ensuring full test coverage --- .../group5/app/utils/ParameterValidator.java | 1 - .../app/utils/ParameterValidatorTest.java | 63 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/test/java/edu/group5/app/utils/ParameterValidatorTest.java diff --git a/src/main/java/edu/group5/app/utils/ParameterValidator.java b/src/main/java/edu/group5/app/utils/ParameterValidator.java index 1181e11..1a44e46 100644 --- a/src/main/java/edu/group5/app/utils/ParameterValidator.java +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -29,7 +29,6 @@ public static final void stringChecker(String stringArg, String variableName) th * @throws IllegalArgumentException if the integer is null or not a positive integer */ public static final void intChecker(int intArg, String variableName) throws IllegalArgumentException { - nullCheck(intArg, variableName); if (intArg <= 0) { throw new IllegalArgumentException(String.format("%s must be a positive integer", variableName)); } diff --git a/src/test/java/edu/group5/app/utils/ParameterValidatorTest.java b/src/test/java/edu/group5/app/utils/ParameterValidatorTest.java new file mode 100644 index 0000000..bf8ac0c --- /dev/null +++ b/src/test/java/edu/group5/app/utils/ParameterValidatorTest.java @@ -0,0 +1,63 @@ +package edu.group5.app.utils; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ParameterValidatorTest { + + @Test + void testValidatorDoesNotThrowWithValidParameters() { + assertDoesNotThrow(() -> ParameterValidator.stringChecker("valid", "validString")); + assertDoesNotThrow(() -> ParameterValidator.intChecker(1, "positiveInt")); + assertDoesNotThrow(() -> ParameterValidator.objectChecker(new Object(), "validObject")); + assertDoesNotThrow(() -> ParameterValidator.bigDecimalChecker(java.math.BigDecimal.valueOf(1), "positiveBigDecimal")); + } + + @Test + void testValidatorThrowsWithStringChecker() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.stringChecker(null, "nullString"); + }); + IllegalArgumentException exception2 = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.stringChecker("", "emptyString"); + }); + IllegalArgumentException exception3 = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.stringChecker(" ", "blankString"); + }); + assertEquals("nullString can't be null", exception.getMessage()); + assertEquals("emptyString can't be blank", exception2.getMessage()); + assertEquals("blankString can't be blank", exception3.getMessage()); + } + + @Test + void testValidatorThrowsWithIntChecker() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.intChecker(-1, "negativeInt"); + }); + assertEquals("negativeInt must be a positive integer", exception.getMessage()); + } + + @Test + void testValidatorThrowsWithObjectChecker() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.objectChecker(null, "nullObject"); + }); + assertEquals("nullObject can't be null", exception.getMessage()); + } + + @Test + void testValidatorThrowsWithBigDecimalChecker() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.bigDecimalChecker(null, "nullBigDecimal"); + }); + IllegalArgumentException exception2 = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.bigDecimalChecker(java.math.BigDecimal.valueOf(-1), "negativeBigDecimal"); + }); + IllegalArgumentException exception3 = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.bigDecimalChecker(java.math.BigDecimal.ZERO, "zeroBigDecimal"); + }); + assertEquals("nullBigDecimal can't be null", exception.getMessage()); + assertEquals("negativeBigDecimal must be larger than 0", exception2.getMessage()); + assertEquals("zeroBigDecimal must be larger than 0", exception3.getMessage()); + } +} From 7da35bf9de0af4c78a4d49c2ff4ec8f457461c06 Mon Sep 17 00:00:00 2001 From: emilfa Date: Tue, 24 Mar 2026 16:00:26 +0100 Subject: [PATCH 05/98] refactor: divided controllers into Navigation-, User-, Donation- and OrganizationController --- src/main/java/edu/group5/app/App.java | 12 ++- .../app/control/DonationController.java | 80 ++++++++++++++ .../app/control/NavigationController.java | 100 ++++++++---------- .../app/control/OrganizationController.java | 23 ++++ .../group5/app/control/UserController.java | 76 +++++++++++++ 5 files changed, 233 insertions(+), 58 deletions(-) diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index 782922c..c948b42 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -1,8 +1,9 @@ package edu.group5.app; -import edu.group5.app.control.MainController; +import edu.group5.app.control.NavigationController; import edu.group5.app.control.wrapper.DbWrapper; import edu.group5.app.control.wrapper.OrgApiWrapper; +import edu.group5.app.model.AppState; import edu.group5.app.model.donation.DonationRepository; import edu.group5.app.model.donation.DonationService; import edu.group5.app.model.organization.OrganizationRepository; @@ -12,6 +13,7 @@ import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.image.Image; +import javafx.scene.layout.BorderPane; import javafx.stage.Stage; import java.util.List; @@ -56,11 +58,13 @@ public void start(Stage stage) { DonationService donationService = new DonationService(donationRepository, organizationRepository); OrganizationService organizationService = new OrganizationService(organizationRepository); - MainController controller = new MainController(userService, donationService, organizationService); + BorderPane root = new BorderPane(); + AppState appState = new AppState(); + NavigationController nav = new NavigationController(root, appState, userService, donationService, organizationService); - Scene scene = controller.getMainView().getScene(); - controller.showLoginPage(); + nav.showLoginPage(); + Scene scene = new Scene(root, 1280, 720); stage.getIcons().add(new Image(getClass().getResource("/header/images/hmh-logo.png").toExternalForm())); stage.setTitle("Help-Me-Help"); stage.setScene(scene); diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 89ac400..97a1c9d 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -1,4 +1,84 @@ package edu.group5.app.control; +import edu.group5.app.model.AppState; +import edu.group5.app.model.donation.Donation; +import edu.group5.app.model.donation.DonationService; +import edu.group5.app.model.organization.Organization; +import edu.group5.app.model.user.Customer; +import edu.group5.app.model.user.User; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + public class DonationController { + private final AppState appState; + private final NavigationController nav; + private final DonationService service; + + public DonationController(AppState appState, NavigationController nav, DonationService service) { + this.appState = appState; + this.nav = nav; + this.service = service; + } + + public Map getUserDonations(int userId) { + return service.getDonationRepository().filterByUser(userId); + } + + public Set getUniqueOrgs() { + Map userDonations = getUserDonations(appState.getCurrentUser().getUserId()); + + Set uniqueOrgs = new HashSet<>(); + for (Donation donation : userDonations.values()) { + uniqueOrgs.add(donation.organizationId()); + } + + return uniqueOrgs; + } + + public void handleDonate() { + // Get session data from MainController + User currentUser = appState.getCurrentUser(); + Organization currentOrg = appState.getCurrentOrganization(); + BigDecimal amount = appState.getCurrentDonationAmount(); + + if (currentUser == null) { + System.err.println("Error: No user logged in"); + return; + } + if (!(currentUser instanceof Customer customer)) { + System.err.println("Error: Only customers can donate"); + return; + } + if (currentOrg == null) { + System.err.println("Error: No organization selected"); + return; + } + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + System.err.println("Error: Invalid donation amount"); + return; + } + + // Create donation via service + boolean success = service.donate( + customer, + currentOrg.orgNumber(), + amount, + "Online" + ); + + if (success) { + System.out.println("Donation created: " + amount + " kr to " + currentOrg.name()); + } else { + System.err.println("Failed to create donation"); + } + + // Clear donation session state + appState.setCurrentDonationAmount(null); + + // Navigate to payment complete + nav.showPaymentCompletePage(); + } } diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index 6a7b3c3..a54e4b6 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -1,99 +1,91 @@ package edu.group5.app.control; +import edu.group5.app.model.AppState; import edu.group5.app.model.donation.DonationService; -import edu.group5.app.model.organization.Organization; import edu.group5.app.model.organization.OrganizationService; -import edu.group5.app.model.user.User; import edu.group5.app.model.user.UserService; -import edu.group5.app.view.ViewManager; - -import java.math.BigDecimal; - -public class MainController { - private final ViewManager view; - +import edu.group5.app.view.Header; +import edu.group5.app.view.causespage.CausesPageView; +import edu.group5.app.view.donationpage.DonationPageView; +import edu.group5.app.view.donationpage.PaymentCompletePageView; +import edu.group5.app.view.homepage.HomePageView; +import edu.group5.app.view.loginpage.LoginHeader; +import edu.group5.app.view.loginpage.LoginPageView; +import edu.group5.app.view.loginpage.SignInPageView; +import edu.group5.app.view.organizationpage.OrganizationPageView; +import edu.group5.app.view.userpage.UserPageView; +import javafx.scene.layout.BorderPane; + +public class NavigationController { + private final BorderPane root; + private final Header header; + private final LoginHeader loginHeader; + + private final AppState appState; private final UserService userService; private final DonationService donationService; private final OrganizationService organizationService; private final UserController userController; + private final DonationController donationController; + private final OrganizationController organizationController; - private User currentUser; - private Organization currentOrganization; - private BigDecimal currentDonationAmount; + public NavigationController(BorderPane root, AppState appState, UserService userService, DonationService donationService, OrganizationService organizationService) { + this.root = root; + this.header = new Header(this); + this.loginHeader = new LoginHeader(); - public MainController(UserService userService, DonationService donationService, - OrganizationService organizationService) { + this.appState = appState; this.userService = userService; this.donationService = donationService; this.organizationService = organizationService; - this.userController = new UserController(this, userService, donationService, organizationService); - - this.view = new ViewManager(this); - } - - public ViewManager getMainView() { - return this.view; - } - - public User getCurrentUser() { - return this.currentUser; - } - - public void setCurrentUser(User user) { - this.currentUser = user; - } - - public Organization getCurrentOrganization() { - return this.currentOrganization; - } - - public void setCurrentOrganization(Organization organization) { - this.currentOrganization = organization; - } - - public BigDecimal getCurrentDonationAmount() { - return this.currentDonationAmount; - } - - public void setCurrentDonationAmount(BigDecimal amount) { - this.currentDonationAmount = amount; + this.userController = new UserController(appState, this, userService); + this.donationController = new DonationController(appState, this, donationService); + this.organizationController = new OrganizationController(appState, this, organizationService); } public void showHomePage() { - view.showHomePage(); + root.setTop(header); + root.setCenter(new HomePageView(appState, this)); } public void showLoginPage() { - view.showLoginPage(); + root.setTop(loginHeader); + root.setCenter(new LoginPageView(appState, this, userController)); } public void showSignInPage() { - view.showSignInPage(); + root.setTop(loginHeader); + root.setCenter(new SignInPageView(appState, this, userController)); } public void showPaymentCompletePage() { - view.showPaymentCompletePage(); + root.setTop(header); + root.setCenter(new PaymentCompletePageView(this)); } public void showCausesPage() { - view.showCausesPage(); + root.setTop(header); + root.setCenter(new CausesPageView(appState, this, organizationController)); } public void showOrganizationPage() { - view.showOrganizationPage(); + root.setTop(header); + root.setCenter(new OrganizationPageView(appState, this, donationController)); } public void showDonationPage() { - view.showDonationPage(); + root.setTop(header); + root.setCenter(new DonationPageView(appState, this, donationController)); } public void showAboutUsPage() { - view.showAboutUsPage(); + root.setTop(header); } public void showUserPage() { - view.showUserPage(); + root.setTop(header); + root.setCenter(new UserPageView(appState, this, userController, donationController, organizationController)); } } diff --git a/src/main/java/edu/group5/app/control/OrganizationController.java b/src/main/java/edu/group5/app/control/OrganizationController.java index 00d08b6..9cbfc7a 100644 --- a/src/main/java/edu/group5/app/control/OrganizationController.java +++ b/src/main/java/edu/group5/app/control/OrganizationController.java @@ -1,4 +1,27 @@ package edu.group5.app.control; +import edu.group5.app.model.AppState; +import edu.group5.app.model.organization.Organization; +import edu.group5.app.model.organization.OrganizationService; + +import java.util.Map; + public class OrganizationController { + private final AppState appState; + private final NavigationController nav; + private final OrganizationService service; + + public OrganizationController(AppState appState, NavigationController nav, OrganizationService service) { + this.appState = appState; + this.nav = nav; + this.service = service; + } + + public Organization getOrgById(int orgId) { + return service.findByOrgNumber(orgId); + } + + public Map getTrustedOrgs() { + return service.getTrustedOrganizations(); + } } diff --git a/src/main/java/edu/group5/app/control/UserController.java b/src/main/java/edu/group5/app/control/UserController.java index 2ee376c..c8d55dd 100644 --- a/src/main/java/edu/group5/app/control/UserController.java +++ b/src/main/java/edu/group5/app/control/UserController.java @@ -1,4 +1,80 @@ package edu.group5.app.control; +import edu.group5.app.model.AppState; +import edu.group5.app.model.donation.Donation; +import edu.group5.app.model.donation.DonationService; +import edu.group5.app.model.organization.Organization; +import edu.group5.app.model.organization.OrganizationService; +import edu.group5.app.model.user.Customer; +import edu.group5.app.model.user.User; +import edu.group5.app.model.user.UserService; +import edu.group5.app.view.loginpage.LoginPageView; +import edu.group5.app.view.loginpage.SignInPageView; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + public class UserController { + private final AppState appState; + private final NavigationController nav; + private final UserService userService; + + public UserController(AppState appState, NavigationController nav, UserService userService) { + this.appState = appState; + this.nav = nav; + this.userService = userService; + } + + public void handleSignIn(SignInPageView view, String firstName, String lastName, String email, char[] passwordChars) { + if (firstName == null || firstName.trim().isEmpty() || + lastName == null || lastName.trim().isEmpty() || + email == null || email.trim().isEmpty() || + passwordChars == null || passwordChars.length == 0) { + view.showError("All fields are required"); + return; + } + + String password = new String(passwordChars); + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + String hashedPassword = encoder.encode(password); + + boolean success = userService.registerUser( + "Customer", firstName, lastName, email, hashedPassword); + + if (success) { + User user = userService.getUserByEmail(email); + + appState.setCurrentUser(user); + nav.showHomePage(); + } else { + view.showError("Registration failed. Email may already be in use."); + } + } + + public void handleLogin(LoginPageView view, String email, char[] passwordChars) { + if (email == null || email.trim().isEmpty() || passwordChars == null || passwordChars.length == 0) { + view.showError("Email and password are required"); + return; + } + + User user = userService.login(email, passwordChars); + + if (user != null) { + appState.setCurrentUser(user); + nav.showHomePage(); + } else { + view.showError("Invalid email or password"); + } + } + + public void handleLogout() { + appState.setCurrentUser(null); + appState.setCurrentOrganization(null); + appState.setCurrentDonationAmount(null); + nav.showLoginPage(); + } } From 896ca24a35a0077f5eba137f17c7e82d024305fe Mon Sep 17 00:00:00 2001 From: emilfa Date: Tue, 24 Mar 2026 16:08:07 +0100 Subject: [PATCH 06/98] feat: created AppState to keep track of the state of the application --- .../java/edu/group5/app/model/AppState.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/java/edu/group5/app/model/AppState.java b/src/main/java/edu/group5/app/model/AppState.java index 96901b3..d8dd03a 100644 --- a/src/main/java/edu/group5/app/model/AppState.java +++ b/src/main/java/edu/group5/app/model/AppState.java @@ -1,4 +1,36 @@ package edu.group5.app.model; +import edu.group5.app.model.organization.Organization; +import edu.group5.app.model.user.User; + +import java.math.BigDecimal; + public class AppState { + private User currentUser; + private BigDecimal currentDonationAmount; + private Organization currentOrganization; + + public User getCurrentUser() { + return this.currentUser; + } + + public void setCurrentUser(User user) { + this.currentUser = user; + } + + public Organization getCurrentOrganization() { + return this.currentOrganization; + } + + public void setCurrentOrganization(Organization organization) { + this.currentOrganization = organization; + } + + public BigDecimal getCurrentDonationAmount() { + return this.currentDonationAmount; + } + + public void setCurrentDonationAmount(BigDecimal amount) { + this.currentDonationAmount = amount; + } } From 4cecbe4c76d9f2b2cac22de04608de8a54055b04 Mon Sep 17 00:00:00 2001 From: emilfa Date: Tue, 24 Mar 2026 16:11:49 +0100 Subject: [PATCH 07/98] refactor: changed view to work with new controller structure --- src/main/java/edu/group5/app/view/Header.java | 16 +++--- .../app/view/causespage/CausesPageView.java | 52 ++++++++++++------- .../app/view/causespage/OrganizationCard.java | 15 ++++-- .../view/donationpage/DonationPageView.java | 22 +++++--- .../donationpage/PaymentCompletePageView.java | 11 ++-- .../app/view/homepage/HomePageView.java | 16 +++--- .../app/view/loginpage/LoginPageView.java | 31 +++++++---- .../app/view/loginpage/SignInPageView.java | 33 +++++++++--- .../OrganizationPageView.java | 21 +++++--- .../app/view/userpage/UserPageView.java | 52 +++++++++++-------- 10 files changed, 169 insertions(+), 100 deletions(-) diff --git a/src/main/java/edu/group5/app/view/Header.java b/src/main/java/edu/group5/app/view/Header.java index 4bc8284..35472e1 100644 --- a/src/main/java/edu/group5/app/view/Header.java +++ b/src/main/java/edu/group5/app/view/Header.java @@ -1,6 +1,6 @@ package edu.group5.app.view; -import edu.group5.app.control.PageController; +import edu.group5.app.control.NavigationController; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.image.Image; @@ -8,9 +8,9 @@ import javafx.scene.layout.*; public class Header extends BorderPane { - private final PageController controller; + private final NavigationController controller; - public Header(PageController controller) { + public Header(NavigationController controller) { this.controller = controller; getStylesheets().add(getClass().getResource("/header/header.css").toExternalForm()); setId("header"); @@ -24,7 +24,7 @@ private StackPane getLogoSection() { StackPane logoSection = new StackPane(); logoSection.setId("logo-section"); logoSection.setAlignment(Pos.CENTER); - logoSection.setOnMouseClicked(e -> controller.handleHomeBtn()); + logoSection.setOnMouseClicked(e -> controller.showHomePage()); logoSection.setStyle("-fx-cursor: hand;"); ImageView logo = new ImageView( @@ -44,15 +44,15 @@ private HBox getNavBar() { navbar.setSpacing(10); Button home = new Button("Home"); - home.setOnAction(e -> controller.handleHomeBtn()); + home.setOnAction(e -> controller.showHomePage()); home.setStyle("-fx-cursor: hand;"); Button causes = new Button("Causes"); - causes.setOnAction(e -> controller.handleCausesBtn()); + causes.setOnAction(e -> controller.showCausesPage()); causes.setStyle("-fx-cursor: hand;"); Button about = new Button("About us"); - about.setOnAction(e -> controller.handleAboutBtn()); + about.setOnAction(e -> controller.showAboutUsPage()); about.setStyle("-fx-cursor: hand;"); navbar.getChildren().addAll(home, causes, about); @@ -63,7 +63,7 @@ private StackPane getProfileSection() { StackPane profileSection = new StackPane(); profileSection.setId("profile-section"); profileSection.setAlignment(Pos.CENTER); - profileSection.setOnMouseClicked(e -> controller.handleProfileBtn()); + profileSection.setOnMouseClicked(e -> controller.showUserPage()); profileSection.setStyle("-fx-cursor: hand;"); ImageView avatar = new ImageView( diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index d19b735..4784bcd 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -1,21 +1,30 @@ -package edu.group5.app.view.browsepage; +package edu.group5.app.view.causespage; -import edu.group5.app.control.PageController; +import edu.group5.app.control.NavigationController; +import edu.group5.app.control.OrganizationController; +import edu.group5.app.model.AppState; import edu.group5.app.model.organization.Organization; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextField; import javafx.scene.layout.*; +import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -public class BrowsePageView extends BorderPane { - private final PageController controller; +public class CausesPageView extends BorderPane { + private final AppState appState; + private final NavigationController nav; + private final OrganizationController orgController; + private GridPane organizationGrid; private Map allOrganizations; - public BrowsePageView(PageController controller) { - this.controller = controller; + public CausesPageView(AppState appState, NavigationController nav, OrganizationController orgController) { + this.appState = appState; + this.nav = nav; + this.orgController = orgController; + getStylesheets().add(getClass().getResource("/browsepage/browsepage.css").toExternalForm()); setCenter(createBody()); } @@ -62,30 +71,33 @@ private GridPane createOrganizationSection(String searchTerm) { grid.setMaxWidth(Double.MAX_VALUE); if (allOrganizations == null) { - System.out.println("Controller: " + controller.getMainController().getOrganizationService()); - allOrganizations = controller.getMainController().getOrganizationService().getTrustedOrganizations(); + allOrganizations = orgController.getTrustedOrgs(); } - // Filter organizations by search term - Map organizations = filterOrganizations(searchTerm); + Map organizations = new HashMap<>(); + if (searchTerm != null) { + // Filter organizations by search term + organizations = filterOrganizations(searchTerm); + } else { + organizations = allOrganizations; + } int column = 0; int row = 0; - for (int i = 0; i < 16; i++) { - for (Organization org : organizations.values()) { - String defaultImg = "/browsepage/images/children_of_shambala.png"; - BrowseCard card = new BrowseCard(controller, org, defaultImg); + for (Organization org : organizations.values()) { + String defaultImg = "/browsepage/images/children_of_shambala.png"; + OrganizationCard card = new OrganizationCard(appState, nav, org, defaultImg); - grid.add(card, column, row); + grid.add(card, column, row); - column++; + column++; - if (column == 4) { - column = 0; - row++; - } + if (column == 4) { + column = 0; + row++; } } + for (int i = 0; i < 4; i++) { ColumnConstraints col = new ColumnConstraints(); col.setPercentWidth(25); diff --git a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java index ccdb76a..389b934 100644 --- a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java +++ b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java @@ -1,5 +1,7 @@ package edu.group5.app.view.causespage; +import edu.group5.app.control.NavigationController; +import edu.group5.app.model.AppState; import edu.group5.app.model.organization.Organization; import javafx.geometry.Pos; import javafx.scene.image.Image; @@ -8,12 +10,14 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; -public class OrgCard extends VBox { +public class OrganizationCard extends VBox { + private final AppState appState; private final Organization organization; - private final PageController controller; + private final NavigationController nav; - public OrgCard(PageController controller, Organization org, String img) { - this.controller = controller; + public OrganizationCard(AppState appstate, NavigationController nav, Organization org, String img) { + this.appState = appstate; + this.nav = nav; this.organization = org; setId("mainContainer"); getStylesheets().add(getClass().getResource("/browsepage/browse_org.css").toExternalForm()); @@ -25,7 +29,8 @@ public OrgCard(PageController controller, Organization org, String img) { ); setOnMouseClicked(e -> { - controller.handleBrowseCardClick(organization); + appstate.setCurrentOrganization(organization); + nav.showOrganizationPage(); }); setSpacing(10); diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 31919f4..5087267 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -1,6 +1,8 @@ package edu.group5.app.view.donationpage; -import edu.group5.app.control.PageController; +import edu.group5.app.control.DonationController; +import edu.group5.app.control.NavigationController; +import edu.group5.app.model.AppState; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -20,12 +22,18 @@ import java.util.Map; public class DonationPageView extends BorderPane { - private final PageController controller; + private final AppState appState; + private final NavigationController nav; + private final DonationController donationController; + private final List allDonationElements = new ArrayList<>(); private final Map elementAmounts = new HashMap<>(); - public DonationPageView(PageController controller) { - this.controller = controller; + public DonationPageView(AppState appState, NavigationController nav, DonationController donationController) { + this.appState = appState; + this.nav = nav; + this.donationController = donationController; + getStylesheets().add(getClass().getResource("/donationpage/donation.css").toExternalForm()); VBox content = new VBox(); @@ -94,7 +102,6 @@ private VBox createCustomButton() { box.setAlignment(Pos.CENTER); box.getStyleClass().add("donation-button"); - box.setOnMouseClicked(e -> { try { BigDecimal amount = new BigDecimal(amountField.getText().trim()); @@ -103,7 +110,6 @@ private VBox createCustomButton() { } catch (NumberFormatException exception) { System.err.println("Invalid custom donation amount: " + amountField.getText()); } - }); allDonationElements.add(box); @@ -112,7 +118,7 @@ private VBox createCustomButton() { private HBox createDonateSection() { Button donateBtn = new Button("Donate"); donateBtn.getStyleClass().add("donate-button"); - donateBtn.setOnAction(e -> controller.handleDonationBtn()); + donateBtn.setOnAction(e -> donationController.handleDonate()); HBox section = new HBox(donateBtn); section.setAlignment(Pos.CENTER); @@ -135,7 +141,7 @@ private void selectDonationElement(Node element) { private void extractAndStoreAmount(Node element) { BigDecimal amount = elementAmounts.get(element); if (amount != null) { - controller.setCurrentDonationAmount(amount); + appState.setCurrentDonationAmount(amount); } else { System.err.println("Error: No amount found for selected element"); } diff --git a/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java b/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java index 2cd407b..fcefe97 100644 --- a/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java @@ -1,6 +1,6 @@ package edu.group5.app.view.donationpage; -import edu.group5.app.control.PageController; +import edu.group5.app.control.NavigationController; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -9,14 +9,13 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; -import java.awt.*; import java.util.Objects; public class PaymentCompletePageView extends BorderPane { - private final PageController controller; + private final NavigationController nav; - public PaymentCompletePageView(PageController controller) { - this.controller = controller; + public PaymentCompletePageView(NavigationController nav) { + this.nav = nav; getStylesheets().add(getClass().getResource("/donationpage/paymentcomplete.css").toExternalForm()); VBox content = new VBox(20); @@ -40,7 +39,7 @@ public VBox getImageSection() { public Button getHomeBtn() { Button home = new Button("Home"); - home.setOnAction(e -> controller.handleHomeBtn()); + home.setOnAction(e -> nav.showHomePage()); home.setId("home-button"); return home; } diff --git a/src/main/java/edu/group5/app/view/homepage/HomePageView.java b/src/main/java/edu/group5/app/view/homepage/HomePageView.java index e61c484..6d398fb 100644 --- a/src/main/java/edu/group5/app/view/homepage/HomePageView.java +++ b/src/main/java/edu/group5/app/view/homepage/HomePageView.java @@ -1,6 +1,7 @@ package edu.group5.app.view.homepage; -import edu.group5.app.control.PageController; +import edu.group5.app.control.NavigationController; +import edu.group5.app.model.AppState; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.ScrollPane; @@ -10,10 +11,13 @@ import javafx.scene.text.Text; public class HomePageView extends BorderPane { - private final PageController controller; + private final AppState appState; + private final NavigationController nav; + + public HomePageView(AppState appState, NavigationController nav) { + this.appState = appState; + this.nav = nav; - public HomePageView(PageController controller) { - this.controller = controller; getStylesheets().add(getClass().getResource("/homepage/homepage.css").toExternalForm()); setCenter(createBody()); } @@ -42,10 +46,10 @@ private VBox createIntroductionSection() { h2.setId("h2"); Button donateToACauseBtn = new Button("Donate to a cause"); - donateToACauseBtn.setOnAction(e -> controller.handleDonateToACauseBtn()); + donateToACauseBtn.setOnAction(e -> nav.showCausesPage()); Button aboutUsBtn = new Button("About us"); - aboutUsBtn.setOnAction(e -> controller.handleAboutUsBtn()); + aboutUsBtn.setOnAction(e -> nav.showAboutUsPage()); introductionSection.getChildren().addAll(h1, h2, donateToACauseBtn, aboutUsBtn); return introductionSection; diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java index 0e5f0a6..bedb796 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java @@ -1,7 +1,8 @@ package edu.group5.app.view.loginpage; - -import edu.group5.app.control.PageController; +import edu.group5.app.control.NavigationController; +import edu.group5.app.control.UserController; +import edu.group5.app.model.AppState; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -12,15 +13,18 @@ import java.util.Objects; public class LoginPageView extends BorderPane { - private final PageController controller; + private final AppState appState; + private final NavigationController nav; + private final UserController userController; + private TextField emailField; private PasswordField passwordField; private Label errorLabel; - public LoginPageView(PageController controller) { - this.controller = controller; - LoginHeader loginHeaderView = new LoginHeader(); - setTop(loginHeaderView); + public LoginPageView(AppState appState, NavigationController nav, UserController userController) { + this.appState = appState; + this.nav = nav; + this.userController = userController; HBox content = new HBox(); content.setFillHeight(true); @@ -55,6 +59,7 @@ private VBox getOuterSection() { outerSection.getChildren().addAll(getLoginBox(), getRegisterBtn()); return outerSection; } + private VBox getLoginBox() { VBox loginSection = new VBox(12); loginSection.setAlignment(Pos.CENTER); @@ -78,6 +83,7 @@ private VBox getEmailBox() { emailBox.getChildren().addAll(new Label("Email"), emailField); return emailBox; } + private VBox getPasswordBox() { VBox passwordBox = new VBox(); passwordBox.setMaxWidth(300); @@ -86,20 +92,27 @@ private VBox getPasswordBox() { passwordBox.getChildren().addAll(new Label("Password"), passwordField); return passwordBox; } + private Button getLoginBtn() { Button loginBtn = new Button("Log In"); loginBtn.setMaxWidth(300); loginBtn.setId("login-btn"); - loginBtn.setOnMouseClicked(e -> controller.handleLoginBtn()); + loginBtn.setOnMouseClicked(e -> userController.handleLogin( + this, + getEmail(), + getPassword() + )); return loginBtn; } + public Button getRegisterBtn() { Button registerBtn = new Button("Don't have an account? Sign In"); registerBtn.setMaxWidth(300); - registerBtn.setOnMouseClicked(e -> controller.handleRegisterBtn()); + registerBtn.setOnMouseClicked(e -> nav.showSignInPage()); registerBtn.setId("register-btn"); return registerBtn; } + private StackPane getImageSection() { StackPane imageSection = new StackPane(); imageSection.setId("image-section"); diff --git a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java index 7f147dd..0103870 100644 --- a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java @@ -1,6 +1,8 @@ package edu.group5.app.view.loginpage; -import edu.group5.app.control.PageController; +import edu.group5.app.control.NavigationController; +import edu.group5.app.control.UserController; +import edu.group5.app.model.AppState; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -12,16 +14,20 @@ public class SignInPageView extends BorderPane { - private final PageController controller; + private final AppState appState; + private final NavigationController nav; + private final UserController userController; + private TextField nameField; private TextField surnameField; private TextField emailField; private PasswordField passwordField; private Label errorLabel; - public SignInPageView(PageController controller) { - this.controller = controller; - setTop(new LoginHeader()); + public SignInPageView(AppState appState, NavigationController nav, UserController userController) { + this.appState = appState; + this.nav = nav; + this.userController = userController; HBox content = new HBox(); content.setFillHeight(true); @@ -37,7 +43,6 @@ public SignInPageView(PageController controller) { } - public String getFirstName() { return nameField.getText(); } @@ -66,6 +71,7 @@ private VBox getOuterSection() { outerSection.getChildren().addAll(getSignInBox(), getBackToLoginBtn()); return outerSection; } + private VBox getSignInBox() { VBox signInSection = new VBox(12); signInSection.setAlignment(Pos.CENTER); @@ -100,6 +106,7 @@ private HBox getNameRow() { return nameRow; } + private VBox getEmailBox() { VBox emailBox = new VBox(); emailBox.setMaxWidth(300); @@ -109,6 +116,7 @@ private VBox getEmailBox() { emailBox.getChildren().addAll(new Label("Email"), emailField); return emailBox; } + private VBox getPasswordBox() { VBox passwordBox = new VBox(); passwordBox.setMaxWidth(300); @@ -117,20 +125,29 @@ private VBox getPasswordBox() { passwordBox.getChildren().addAll(new Label("Password"), passwordField); return passwordBox; } + private Button getSignInBtn() { Button signInBtn = new Button("Sign In"); signInBtn.setMaxWidth(300); signInBtn.setId("login-btn"); - signInBtn.setOnMouseClicked(e -> controller.handleSignInBtn()); + signInBtn.setOnMouseClicked(e -> userController.handleSignIn( + this, + getFirstName(), + getLastName(), + getEmail(), + getPassword() + )); return signInBtn; } + public Button getBackToLoginBtn() { Button backBtn = new Button("Already have an account? Log in"); backBtn.setMaxWidth(300); - backBtn.setOnMouseClicked(e -> controller.handleLoginBtn()); + backBtn.setOnMouseClicked(e -> nav.showLoginPage()); backBtn.setId("register-btn"); return backBtn; } + private StackPane getImageSection() { StackPane imageSection = new StackPane(); imageSection.setId("image-section"); diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 2d2aee6..93cace6 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -1,6 +1,8 @@ package edu.group5.app.view.organizationpage; -import edu.group5.app.control.PageController; +import edu.group5.app.control.DonationController; +import edu.group5.app.control.NavigationController; +import edu.group5.app.model.AppState; import edu.group5.app.model.organization.Organization; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -15,10 +17,15 @@ import javafx.scene.text.Text; public class OrganizationPageView extends BorderPane { - private final PageController controller; + private final AppState appState; + private final NavigationController nav; + private final DonationController donationController; + + public OrganizationPageView(AppState appState, NavigationController nav, DonationController donationController) { + this.appState = appState; + this.nav = nav; + this.donationController = donationController; - public OrganizationPageView(PageController controller) { - this.controller = controller; getStylesheets().add(getClass().getResource("/organizationpage/organizationpage.css").toExternalForm()); setCenter(createBody()); } @@ -55,7 +62,7 @@ private StackPane createImageContainer() { imageContainer.setPrefWidth(120); imageContainer.setMaxWidth(Double.MAX_VALUE); - Organization org = controller.getCurrentOrganization(); + Organization org = appState.getCurrentOrganization(); String imagePath = "/browsepage/images/children_of_shambala.png"; ImageView logo = new ImageView( @@ -71,7 +78,7 @@ private StackPane createImageContainer() { } private VBox createOrgInfoSection() { - Organization org = controller.getCurrentOrganization(); + Organization org = appState.getCurrentOrganization(); VBox orgInfoSection = new VBox(); orgInfoSection.setSpacing(50); @@ -88,7 +95,7 @@ private VBox createOrgInfoSection() { Button donateBtn = new Button("Donate"); donateBtn.setId("donate-button"); - donateBtn.setOnAction(e -> controller.handleDonateClick()); + donateBtn.setOnAction(e -> nav.showDonationPage()); orgInfoSection.getChildren().addAll(orgNameAndDescription, donateBtn); return orgInfoSection; diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index a538f4d..6fce42c 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -1,7 +1,10 @@ package edu.group5.app.view.userpage; -import edu.group5.app.control.PageController; -import edu.group5.app.model.user.Customer; +import edu.group5.app.control.DonationController; +import edu.group5.app.control.NavigationController; +import edu.group5.app.control.OrganizationController; +import edu.group5.app.control.UserController; +import edu.group5.app.model.AppState; import edu.group5.app.model.donation.Donation; import edu.group5.app.model.organization.Organization; import edu.group5.app.model.user.User; @@ -16,18 +19,23 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; +import java.util.*; public class UserPageView extends BorderPane { - private final PageController controller; - private final User currentUser; + private final AppState appState; + private final NavigationController nav; + private final UserController userController; + private final DonationController donationController; + private final OrganizationController organizationController; + + public UserPageView(AppState appState, NavigationController nav, UserController userController, DonationController donationController, OrganizationController organizationController) { + this.appState = appState; + this.nav = nav; + this.userController = userController; + this.donationController = donationController; + this.organizationController = organizationController; - public UserPageView(PageController controller, Customer customer) { - this.controller = controller; - this.currentUser = controller.getCurrentUser(); getStylesheets().add(getClass().getResource("/userpage/userpage.css").toExternalForm()); VBox content = new VBox(30); @@ -37,12 +45,14 @@ public UserPageView(PageController controller, Customer customer) { } private HBox createProfileSection() { - ImageView avatar = new ImageView(new Image(getClass().getResourceAsStream("/userpage/account_circle.png"))); + ImageView avatar = new ImageView(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/userpage/account_circle.png")))); avatar.setFitWidth(150); avatar.setFitHeight(150); avatar.setPreserveRatio(true); avatar.setId("avatar"); + User currentUser = appState.getCurrentUser(); + Text name = new Text(currentUser.getFirstName() + " " + currentUser.getLastName()); name.setId("profile-name"); @@ -54,7 +64,7 @@ private HBox createProfileSection() { Button logoutBtn = new Button("Logout"); logoutBtn.getStyleClass().add("logout-button"); - logoutBtn.setOnAction(e -> controller.logout()); + logoutBtn.setOnAction(e -> userController.handleLogout()); VBox info = new VBox(10, name, email, location, logoutBtn); info.setAlignment(Pos.CENTER_LEFT); @@ -72,13 +82,9 @@ private VBox createCausesSection() { causesBox.getStyleClass().add("section-box"); causesBox.setPadding(new Insets(10)); - HashMap userDonations = controller.getDonationService() - .getDonationRepository().filterByUser(currentUser.getUserId()); + User currentUser = appState.getCurrentUser(); - Set uniqueOrgs = new HashSet<>(); - for (Donation donation : userDonations.values()) { - uniqueOrgs.add(donation.organizationId()); - } + Set uniqueOrgs = donationController.getUniqueOrgs(); if (uniqueOrgs.isEmpty()) { Label noCauses = new Label("No causes supported yet"); @@ -86,7 +92,7 @@ private VBox createCausesSection() { causesBox.getChildren().add(noCauses); } else { for (int orgId : uniqueOrgs) { - Organization org = controller.getOrganizationService().findByOrgNumber(orgId); + Organization org = organizationController.getOrgById(orgId); if (org != null) { Label causeLabel = new Label("β€’ " + org.name()); causesBox.getChildren().add(causeLabel); @@ -105,8 +111,9 @@ private VBox createDonationsSection() { donationsBox.getStyleClass().add("section-box"); donationsBox.setPadding(new Insets(10)); - HashMap userDonations = controller.getDonationService() - .getDonationRepository().filterByUser(currentUser.getUserId()); + User currentUser = appState.getCurrentUser(); + + Map userDonations = donationController.getUserDonations(currentUser.getUserId()); if (userDonations.isEmpty()) { Label noDonations = new Label("No donations yet"); @@ -114,8 +121,7 @@ private VBox createDonationsSection() { donationsBox.getChildren().add(noDonations); } else { for (Donation donation : userDonations.values()) { - Organization org = controller.getOrganizationService() - .findByOrgNumber(donation.organizationId()); + Organization org = organizationController.getOrgById(donation.organizationId()); String orgName = (org != null) ? org.name() : "Unknown Organization"; Label donationLabel = new Label( From e0ef5344cff750644b9eeb947dc49302dc876664 Mon Sep 17 00:00:00 2001 From: emilfa Date: Tue, 24 Mar 2026 16:21:30 +0100 Subject: [PATCH 08/98] fix: removed test css borders --- .../edu/group5/app/view/homepage/HomePageView.java | 2 ++ src/main/resources/header/header.css | 6 ------ src/main/resources/homepage/homepage.css | 11 ++++++++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/edu/group5/app/view/homepage/HomePageView.java b/src/main/java/edu/group5/app/view/homepage/HomePageView.java index 6d398fb..b40299a 100644 --- a/src/main/java/edu/group5/app/view/homepage/HomePageView.java +++ b/src/main/java/edu/group5/app/view/homepage/HomePageView.java @@ -46,9 +46,11 @@ private VBox createIntroductionSection() { h2.setId("h2"); Button donateToACauseBtn = new Button("Donate to a cause"); + donateToACauseBtn.setId("donate-to-cause-btn"); donateToACauseBtn.setOnAction(e -> nav.showCausesPage()); Button aboutUsBtn = new Button("About us"); + aboutUsBtn.setId("about-us-btn"); aboutUsBtn.setOnAction(e -> nav.showAboutUsPage()); introductionSection.getChildren().addAll(h1, h2, donateToACauseBtn, aboutUsBtn); diff --git a/src/main/resources/header/header.css b/src/main/resources/header/header.css index 28fcc59..23115eb 100644 --- a/src/main/resources/header/header.css +++ b/src/main/resources/header/header.css @@ -4,16 +4,10 @@ } #logo-section { - -fx-border-color: black; - -fx-border-width: 2px; } #navbar { - -fx-border-color: black; - -fx-border-width: 2px; } #profile-section { - -fx-border-color: black; - -fx-border-width: 2px; } \ No newline at end of file diff --git a/src/main/resources/homepage/homepage.css b/src/main/resources/homepage/homepage.css index 90b090c..2579557 100644 --- a/src/main/resources/homepage/homepage.css +++ b/src/main/resources/homepage/homepage.css @@ -1,6 +1,4 @@ #introduction-section { - -fx-border-color: black; - -fx-border-width: 2px; -fx-padding: 20px 0; } @@ -16,7 +14,6 @@ } #charity-image-section { - -fx-border-color: black; } #charity-image { @@ -24,4 +21,12 @@ -fx-background-position: center 55%; -fx-background-size: 100% auto; -fx-background-repeat: no-repeat; +} + +#donate-to-cause-btn { + -fx-cursor: hand; +} + +#about-us-btn { + -fx-cursor: hand; } \ No newline at end of file From d6ce27270ed15bdc68881b9c152ef238d84089b1 Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Tue, 24 Mar 2026 19:18:32 +0100 Subject: [PATCH 09/98] added jsoup dependency --- pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pom.xml b/pom.xml index f9a8b88..75290fc 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,11 @@ 2.2.224 runtime + + org.jsoup + jsoup + 1.17.2 + From 7680c99e08b6880f72239f4ff6d3f96f37678ccc Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Tue, 24 Mar 2026 19:19:50 +0100 Subject: [PATCH 10/98] feat: added logoUrl atribute --- .../group5/app/model/organization/Organization.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/group5/app/model/organization/Organization.java b/src/main/java/edu/group5/app/model/organization/Organization.java index 42844e1..7016567 100644 --- a/src/main/java/edu/group5/app/model/organization/Organization.java +++ b/src/main/java/edu/group5/app/model/organization/Organization.java @@ -7,7 +7,8 @@ * *

* An organization is identified by an organization number, a name, - * trust status, website Url, pre-approval status, and a textual description. + * trust status, website Url, pre-approval status, and a textual description, + * and a logo URL. * *

* Instances are validated on creation: @@ -15,6 +16,7 @@ *

  • orgNumber must be non-negative
  • *
  • name and websiteUrl must not be null or blank
  • *
  • description must not be null
  • + *
  • logoUrl may be null if no logo is available
  • * */ public record Organization( @@ -23,7 +25,8 @@ public record Organization( boolean trusted, String websiteUrl, boolean isPreApproved, - String description) { + String description, + String logoUrl) { /** * Creates a new organization. * @@ -35,12 +38,13 @@ public record Organization( * @param isPreApproved whether the organization is pre-approved * @param description a textual description of the organization; must not be * null + * @param logoUrl the URL to the organization's logo image; may be null * @throws NullPointerException if name, websiteUrl or description is null * @throws IllegalArgumentException if orgNumber is negative, or if name or * websiteUrl is blank */ public Organization(int orgNumber, String name, boolean trusted, String websiteUrl, boolean isPreApproved, - String description) { + String description, String logoUrl) { if (orgNumber < 0) { throw new IllegalArgumentException("orgNumber cannot be negative"); } @@ -50,6 +54,7 @@ public Organization(int orgNumber, String name, boolean trusted, String websiteU this.websiteUrl = Objects.requireNonNull(websiteUrl, "websiteUrl cannot be null"); this.isPreApproved = isPreApproved; this.description = Objects.requireNonNull(description, "description cannot be null"); + this.logoUrl = logoUrl; if (name.isBlank()) { throw new IllegalArgumentException("name cannot be blank"); From f1488757d04e17e622f3d1e298dbb6b4d3be1983 Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Tue, 24 Mar 2026 19:23:53 +0100 Subject: [PATCH 11/98] feat: set logoUrl as null --- .../group5/app/model/organization/OrganizationRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java index a47b3d5..cc0a6b1 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java @@ -43,7 +43,7 @@ public OrganizationRepository(Object[] input) { String websiteURL = (String) contentMap.get("url"); boolean isPreApproved = Boolean.TRUE.equals(contentMap.get("is_pre_approved")); String description = "Information about " + name; - Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description); + Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description, null); grandMap.put(org.orgNumber(), org); } From 632bdcb871be1777ca59ab1b12bafefe5c6ac562 Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Tue, 24 Mar 2026 19:25:06 +0100 Subject: [PATCH 12/98] feat: added methods to get organization logos from API --- .../organization/OrganizationService.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java index c5979f5..9785040 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -1,15 +1,28 @@ package edu.group5.app.model.organization; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; /** * Service class for managing organization-related operations. * It interacts with the OrganizationRepository to retrieve organization information * and contains business logic associated with organization management. + * + *

    It provides fetching logo URLs by web scraping each organization's page on + * Innsamlingskontrollen.

    + * + * Fetched logo URLs are cached to avoid redundant network requests. */ public class OrganizationService { private OrganizationRepository organizationRepository; + private final Map logoCache = new HashMap<>(); + /** * Constructs an OrganizationService with the given OrganizationRepository. * @param organizationRepository the OrganizationRepository to use for managing organization data; must not be null @@ -55,4 +68,82 @@ public Organization findByOrgNumber(int orgNumber) { public Organization findByOrgName(String name) { return organizationRepository.findByOrgName(name); } + + /** + * Fetches the logo URL for the given page by scraping the {@code div.logo img} + * element. Results are cached so each URL is only fetched once. + * + *

    + * Using Jsoup to web scrape through the URLs in the API. + *

    + * @param pageUrl the URL for the organization's page; may be null or blank + * @return the absolute logo URL, or null if not found or pageUrl is invalid + */ + + public String fetchLogoUrl(String pageUrl) { + if (pageUrl == null || pageUrl.isBlank()) { + return null; + } + + if (logoCache.containsKey(pageUrl)) { + return logoCache.get(pageUrl); + } + + try { + Document doc = Jsoup.connect(pageUrl).get(); + Element img = doc.selectFirst("div.logo img"); + + if (img != null) { + String logoUrl = img.absUrl("src"); + logoCache.put(pageUrl, logoUrl); + return logoUrl; + } + } catch (Exception e) { + System.out.println("Could not get logo for: " + pageUrl); + } + return null; + } + + /** + * Fetches all trusted organizations with their logo URLs. + * + *

    + * For each trusted organization, attempts to get its logo using + * {@link #fetchLogoUrl(String)}. Creates a new Organization + * object including the logo URL. + *

    + * @return a map of trusted organizations keyed by organization number, with logos included + */ + public Map getTrustedOrganizationsWithLogos() { + Map original = getTrustedOrganizations(); + Map trustedOrgsWithLogos = new HashMap<>(); + + for (Organization org : original.values()) { + String logoUrl = fetchLogoUrl(org.websiteUrl()); + + Organization newOrg = new Organization( + org.orgNumber(), + org.name(), + org.trusted(), + org.websiteUrl(), + org.isPreApproved(), + org.description(), + logoUrl + ); + trustedOrgsWithLogos.put(newOrg.orgNumber(), newOrg); + } + return trustedOrgsWithLogos; + } + + /** + * Asynchronously fetches trusted organizations with logos. + * + *

    Runs in the background so the UI thread is no blocked. + * Returns a CompletableFuture that completes when all logos are loaded.

    + * + * @return a CompletableFuture containing a map of organizations with logos + */ + public CompletableFuture> getTrustedOrganizationsWithLogosAsync() { + return CompletableFuture.supplyAsync(this::getTrustedOrganizationsWithLogos); + } } From 04c92c48946d270736565fb4534cc3ab623e596f Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Tue, 24 Mar 2026 19:26:22 +0100 Subject: [PATCH 13/98] feat: added images to BrowseCards --- .../app/view/browsepage/BrowseCard.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java index ad4be8a..b529e89 100644 --- a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java +++ b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java @@ -37,20 +37,32 @@ public BrowseCard(BrowseCardController browseCardController, Organization org, S private StackPane imageContainer(String img) { StackPane imageContainer = new StackPane(); imageContainer.setId("imageContainer"); + imageContainer.setPrefHeight(80); imageContainer.setPrefWidth(80); imageContainer.setMaxWidth(Double.MAX_VALUE); - ImageView logo = new ImageView( - new Image(getClass().getResource(img).toExternalForm()) - ); - logo.setId("logo"); - logo.setSmooth(true); - logo.setPreserveRatio(true); - logo.setFitHeight(80); + if (img != null && !img.isBlank()) { + ImageView logo = new ImageView(new Image(img, true)); + logo.setId("logo"); + logo.setSmooth(true); + logo.setPreserveRatio(true); + logo.setFitHeight(80); + logo.setFitWidth(80); + + imageContainer.getChildren().add(logo); + } else { + StackPane placeholder = new StackPane(); + placeholder.setPrefSize(80, 80); + + Text text = new Text("No image"); + text.setStyle("-fx-font-size: 10;"); + + placeholder.getChildren().add(text); + imageContainer.getChildren().add(placeholder); + } - imageContainer.getChildren().add(logo); return imageContainer; } From 27aff0fc1728738e38df5f2d5cd8625f7193c63e Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Tue, 24 Mar 2026 19:28:18 +0100 Subject: [PATCH 14/98] feat: added images to BrowsePageView --- .../app/view/browsepage/BrowsePageView.java | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java index a2d92c8..b27b011 100644 --- a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java +++ b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java @@ -6,6 +6,7 @@ import edu.group5.app.control.MainController; import edu.group5.app.model.organization.Organization; import edu.group5.app.view.Header; +import javafx.application.Platform; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.ScrollPane; @@ -75,8 +76,26 @@ private GridPane createOrganizationSection(String searchTerm) { grid.setStyle("-fx-padding: 0;"); grid.setMaxWidth(Double.MAX_VALUE); + // Store reference for later updates + if (organizationGrid == null) { + organizationGrid = grid; + } + if (allOrganizations == null) { - allOrganizations = mainController.getOrganizationService().getTrustedOrganizations(); + + //Show loading text while organizations and logos are fetched + grid.add(new javafx.scene.control.Label("Loading..."), 0, 0); + + //Fetch trusted organizations with logos asynchronously (runs in background) + mainController.getOrganizationService() + .getTrustedOrganizationsWithLogosAsync() + .thenAccept(orgs -> { + this.allOrganizations = orgs; + + // Update UI when data is ready + Platform.runLater(() -> updateOrganizationGrid("")); + }); + return grid; } // Filter organizations by search term @@ -86,13 +105,16 @@ private GridPane createOrganizationSection(String searchTerm) { int row = 0; for (Organization org : organizations.values()) { - String defaultImg = "/browsepage/images/children_of_shambala.png"; - BrowseCard card = new BrowseCard(orgController, org, defaultImg); + //Adds default text if organization does not have any + String img = (org.logoUrl() != null && !org.logoUrl().isBlank()) + ? org.logoUrl() + : null; + + BrowseCard card = new BrowseCard(orgController, org, img); grid.add(card, column, row); column++; - if (column == 4) { column = 0; row++; @@ -105,11 +127,6 @@ private GridPane createOrganizationSection(String searchTerm) { grid.getColumnConstraints().add(col); } - // Store reference for later updates - if (organizationGrid == null) { - organizationGrid = grid; - } - return grid; } From 33484e3609cf2ed2215fea9b13d4f94d58380acd Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Tue, 24 Mar 2026 19:28:49 +0100 Subject: [PATCH 15/98] feat: added tests to Organization classes --- .../OrganizationRepositoryTest.java | 6 ++-- .../organization/OrganizationServiceTest.java | 15 +++++++++ .../model/organization/OrganizationTest.java | 32 +++++++++++++++---- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java index 7a5ece5..f821e35 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java @@ -73,7 +73,7 @@ void getTrustedOrganizations_OnlyReturnsTrustedOrganizations() { @Test void testFindByOrgNumberReturnsOrganization() { assertEquals(new Organization(1, "Trusted Org1", true, - "org.com", true, "Information about Trusted Org1"), + "org.com", true, "Information about Trusted Org1", null), repository.findByOrgNumber(1)); } @@ -93,7 +93,7 @@ void testFindByOrgNumberIfOrgNumberNotFound() { @Test void testFindByOrgNameReturnsOrganization() { assertEquals(new Organization(1, "Trusted Org1", true, - "org.com", true, "Information about Trusted Org1"), + "org.com", true, "Information about Trusted Org1", null), repository.findByOrgName("Trusted Org1")); } @@ -116,7 +116,7 @@ void testFindByOrgNameIfNameNotFound() { @Test void testFindByOrgNameIsCaseInsensitive() { assertEquals(new Organization(1, "Trusted Org1", true, - "org.com", true, "Information about Trusted Org1"), + "org.com", true, "Information about Trusted Org1", null), repository.findByOrgName("trusted org1")); } diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java index e34aba7..0920e67 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java @@ -67,4 +67,19 @@ void testFindByOrgName() { assertEquals(1, org.orgNumber()); assertEquals("Misjonsalliansen", org.name()); } + + @Test + void fetchLogoUrlReturnsNullWhenUrlIsNull() { + assertNull(service.fetchLogoUrl(null)); + } + @Test + void fetchLogoUrlReturnsNullWhenUrlIsBlank() { + assertNull(service.fetchLogoUrl("")); + } + @Test + void fetchLogoUrlCachesResultOnSecondCall() { + String result1 = service.fetchLogoUrl("https://"); + String result2 = service.fetchLogoUrl("https://"); + assertEquals(result1, result2); + } } diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationTest.java index f921b60..0b97840 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationTest.java @@ -14,7 +14,8 @@ void constructor_CreatesAnOrganizationWhenInputIsValid() { true, "org.com", true, - "Org description" + "Org description", + null ); assertAll( @@ -35,7 +36,8 @@ void constructor_ThrowsWhenOrgNumberIsNegative() { true, "org.com", true, - "Org description" + "Org description", + null )); } @@ -47,7 +49,8 @@ void constructor_ThrowsWhenNameIsNull() { true, "org.com", true, - "Org description" + "Org description", + null )); } @@ -59,7 +62,8 @@ void constructor_ThrowsWhenNameIsBlank() { true, "org.com", true, - "Org description" + "Org description", + null )); } @@ -71,7 +75,8 @@ void constructor_ThrowsWhenWebsiteURLIsNull() { true, null, true, - "Org description" + "Org description", + null )); } @@ -83,7 +88,8 @@ void constructor_ThrowsWhenWebsiteURLIsBlank() { true, "", true, - "Org description" + "Org description", + null )); } @@ -95,6 +101,20 @@ void constructor_ThrowsWhenDescriptionIsNull() { true, "org.com", true, + null, + null + )); + } + + @Test + void constructor_AcceptsNullLogoUrl() { + assertDoesNotThrow(() -> new Organization( + 1, + "Org", + true, + "org.com", + true, + "description", null )); } From 5db4ec6270743fce4e4754cb8fe89e5daa6e2eb1 Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Tue, 24 Mar 2026 21:02:22 +0100 Subject: [PATCH 16/98] feat: added images to Organization pages --- .../OrganizationPageView.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 4ec7c91..b0afa27 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -63,17 +63,21 @@ private StackPane createImageContainer() { imageContainer.setMaxWidth(Double.MAX_VALUE); Organization org = mainController.getCurrentOrganization(); - String imagePath = org != null ? "/browsepage/images/children_of_shambala.png" : "/browsepage/images/children_of_shambala.png"; - - ImageView logo = new ImageView( - new Image(getClass().getResource(imagePath).toExternalForm()) - ); - - logo.setId("logo"); - logo.setSmooth(true); - logo.setPreserveRatio(true); - - imageContainer.getChildren().add(logo); + if (org != null && org.logoUrl() != null && !org.logoUrl().isBlank()) { + ImageView logo = new ImageView(new Image(org.logoUrl(), true)); + logo.setId("logo"); + logo.setSmooth(true); + logo.setPreserveRatio(true); + imageContainer.getChildren().add(logo); + } else { + StackPane placeholder = new StackPane(); + + Text text = new Text("No image"); + text.setStyle("-fx-font-size: 10;"); + + placeholder.getChildren().add(text); + imageContainer.getChildren().add(placeholder); + } return imageContainer; } From c8341afbaab7c488be435a02bffc00a68b683f01 Mon Sep 17 00:00:00 2001 From: emilfa Date: Thu, 26 Mar 2026 09:18:17 +0100 Subject: [PATCH 17/98] refactor: removed unnessecary class variables --- .../java/edu/group5/app/control/NavigationController.java | 6 ------ .../java/edu/group5/app/view/loginpage/LoginHeader.java | 1 - 2 files changed, 7 deletions(-) diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index a54e4b6..2dd872a 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -22,9 +22,6 @@ public class NavigationController { private final LoginHeader loginHeader; private final AppState appState; - private final UserService userService; - private final DonationService donationService; - private final OrganizationService organizationService; private final UserController userController; private final DonationController donationController; @@ -36,9 +33,6 @@ public NavigationController(BorderPane root, AppState appState, UserService user this.loginHeader = new LoginHeader(); this.appState = appState; - this.userService = userService; - this.donationService = donationService; - this.organizationService = organizationService; this.userController = new UserController(appState, this, userService); this.donationController = new DonationController(appState, this, donationService); diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java index 793edb9..ad6a412 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java @@ -18,7 +18,6 @@ private StackPane getLogoSection() { StackPane logoSection = new StackPane(); logoSection.setId("logo-section"); logoSection.setAlignment(Pos.CENTER); - logoSection.setStyle("-fx-cursor: hand;"); ImageView logo = new ImageView( new Image(getClass().getResource("/header/images/hmh-logo.png").toExternalForm()) From 1fc75a755d8c846d7e68ac47672401c91e986949 Mon Sep 17 00:00:00 2001 From: emilfa Date: Thu, 26 Mar 2026 09:31:57 +0100 Subject: [PATCH 18/98] refactor: renamed UserController to LoginController --- .../{UserController.java => LoginController.java} | 15 ++------------- .../group5/app/control/NavigationController.java | 10 +++++----- .../group5/app/view/loginpage/LoginPageView.java | 10 +++++----- .../group5/app/view/loginpage/SignInPageView.java | 10 +++++----- .../group5/app/view/userpage/UserPageView.java | 10 +++++----- 5 files changed, 22 insertions(+), 33 deletions(-) rename src/main/java/edu/group5/app/control/{UserController.java => LoginController.java} (80%) diff --git a/src/main/java/edu/group5/app/control/UserController.java b/src/main/java/edu/group5/app/control/LoginController.java similarity index 80% rename from src/main/java/edu/group5/app/control/UserController.java rename to src/main/java/edu/group5/app/control/LoginController.java index c8d55dd..cdd5b5f 100644 --- a/src/main/java/edu/group5/app/control/UserController.java +++ b/src/main/java/edu/group5/app/control/LoginController.java @@ -1,29 +1,18 @@ package edu.group5.app.control; import edu.group5.app.model.AppState; -import edu.group5.app.model.donation.Donation; -import edu.group5.app.model.donation.DonationService; -import edu.group5.app.model.organization.Organization; -import edu.group5.app.model.organization.OrganizationService; -import edu.group5.app.model.user.Customer; import edu.group5.app.model.user.User; import edu.group5.app.model.user.UserService; import edu.group5.app.view.loginpage.LoginPageView; import edu.group5.app.view.loginpage.SignInPageView; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import java.math.BigDecimal; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -public class UserController { +public class LoginController { private final AppState appState; private final NavigationController nav; private final UserService userService; - public UserController(AppState appState, NavigationController nav, UserService userService) { + public LoginController(AppState appState, NavigationController nav, UserService userService) { this.appState = appState; this.nav = nav; this.userService = userService; diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index 2dd872a..ddab7e2 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -23,7 +23,7 @@ public class NavigationController { private final AppState appState; - private final UserController userController; + private final LoginController loginController; private final DonationController donationController; private final OrganizationController organizationController; @@ -34,7 +34,7 @@ public NavigationController(BorderPane root, AppState appState, UserService user this.appState = appState; - this.userController = new UserController(appState, this, userService); + this.loginController = new LoginController(appState, this, userService); this.donationController = new DonationController(appState, this, donationService); this.organizationController = new OrganizationController(appState, this, organizationService); } @@ -46,12 +46,12 @@ public void showHomePage() { public void showLoginPage() { root.setTop(loginHeader); - root.setCenter(new LoginPageView(appState, this, userController)); + root.setCenter(new LoginPageView(appState, this, loginController)); } public void showSignInPage() { root.setTop(loginHeader); - root.setCenter(new SignInPageView(appState, this, userController)); + root.setCenter(new SignInPageView(appState, this, loginController)); } public void showPaymentCompletePage() { @@ -80,6 +80,6 @@ public void showAboutUsPage() { public void showUserPage() { root.setTop(header); - root.setCenter(new UserPageView(appState, this, userController, donationController, organizationController)); + root.setCenter(new UserPageView(appState, this, loginController, donationController, organizationController)); } } diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java index bedb796..af972e0 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java @@ -1,7 +1,7 @@ package edu.group5.app.view.loginpage; import edu.group5.app.control.NavigationController; -import edu.group5.app.control.UserController; +import edu.group5.app.control.LoginController; import edu.group5.app.model.AppState; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -15,16 +15,16 @@ public class LoginPageView extends BorderPane { private final AppState appState; private final NavigationController nav; - private final UserController userController; + private final LoginController loginController; private TextField emailField; private PasswordField passwordField; private Label errorLabel; - public LoginPageView(AppState appState, NavigationController nav, UserController userController) { + public LoginPageView(AppState appState, NavigationController nav, LoginController loginController) { this.appState = appState; this.nav = nav; - this.userController = userController; + this.loginController = loginController; HBox content = new HBox(); content.setFillHeight(true); @@ -97,7 +97,7 @@ private Button getLoginBtn() { Button loginBtn = new Button("Log In"); loginBtn.setMaxWidth(300); loginBtn.setId("login-btn"); - loginBtn.setOnMouseClicked(e -> userController.handleLogin( + loginBtn.setOnMouseClicked(e -> loginController.handleLogin( this, getEmail(), getPassword() diff --git a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java index 0103870..946dae4 100644 --- a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java @@ -1,7 +1,7 @@ package edu.group5.app.view.loginpage; import edu.group5.app.control.NavigationController; -import edu.group5.app.control.UserController; +import edu.group5.app.control.LoginController; import edu.group5.app.model.AppState; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -16,7 +16,7 @@ public class SignInPageView extends BorderPane { private final AppState appState; private final NavigationController nav; - private final UserController userController; + private final LoginController loginController; private TextField nameField; private TextField surnameField; @@ -24,10 +24,10 @@ public class SignInPageView extends BorderPane { private PasswordField passwordField; private Label errorLabel; - public SignInPageView(AppState appState, NavigationController nav, UserController userController) { + public SignInPageView(AppState appState, NavigationController nav, LoginController loginController) { this.appState = appState; this.nav = nav; - this.userController = userController; + this.loginController = loginController; HBox content = new HBox(); content.setFillHeight(true); @@ -130,7 +130,7 @@ private Button getSignInBtn() { Button signInBtn = new Button("Sign In"); signInBtn.setMaxWidth(300); signInBtn.setId("login-btn"); - signInBtn.setOnMouseClicked(e -> userController.handleSignIn( + signInBtn.setOnMouseClicked(e -> loginController.handleSignIn( this, getFirstName(), getLastName(), diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index 6fce42c..c5d886d 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -3,7 +3,7 @@ import edu.group5.app.control.DonationController; import edu.group5.app.control.NavigationController; import edu.group5.app.control.OrganizationController; -import edu.group5.app.control.UserController; +import edu.group5.app.control.LoginController; import edu.group5.app.model.AppState; import edu.group5.app.model.donation.Donation; import edu.group5.app.model.organization.Organization; @@ -25,14 +25,14 @@ public class UserPageView extends BorderPane { private final AppState appState; private final NavigationController nav; - private final UserController userController; + private final LoginController loginController; private final DonationController donationController; private final OrganizationController organizationController; - public UserPageView(AppState appState, NavigationController nav, UserController userController, DonationController donationController, OrganizationController organizationController) { + public UserPageView(AppState appState, NavigationController nav, LoginController loginController, DonationController donationController, OrganizationController organizationController) { this.appState = appState; this.nav = nav; - this.userController = userController; + this.loginController = loginController; this.donationController = donationController; this.organizationController = organizationController; @@ -64,7 +64,7 @@ private HBox createProfileSection() { Button logoutBtn = new Button("Logout"); logoutBtn.getStyleClass().add("logout-button"); - logoutBtn.setOnAction(e -> userController.handleLogout()); + logoutBtn.setOnAction(e -> loginController.handleLogout()); VBox info = new VBox(10, name, email, location, logoutBtn); info.setAlignment(Pos.CENTER_LEFT); From 18f65208cf8de7325c4821444a2318b203cf2012 Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Mon, 30 Mar 2026 12:49:59 +0200 Subject: [PATCH 19/98] added getter and setter for PaymentMethod --- src/main/java/edu/group5/app/model/AppState.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/edu/group5/app/model/AppState.java b/src/main/java/edu/group5/app/model/AppState.java index d8dd03a..73d5cfc 100644 --- a/src/main/java/edu/group5/app/model/AppState.java +++ b/src/main/java/edu/group5/app/model/AppState.java @@ -9,6 +9,7 @@ public class AppState { private User currentUser; private BigDecimal currentDonationAmount; private Organization currentOrganization; + private String currentDonation; public User getCurrentUser() { return this.currentUser; @@ -33,4 +34,12 @@ public BigDecimal getCurrentDonationAmount() { public void setCurrentDonationAmount(BigDecimal amount) { this.currentDonationAmount = amount; } + + public String getCurrentPaymentMethod() { + return this.currentDonation; + } + + public void setCurrentPaymentMethod(String paymentMethod){ + this.currentDonation = paymentMethod; + } } From 9a4e4dc7e5f0f4cfcbe7144c189e92a58c1f18ba Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Mon, 30 Mar 2026 12:51:38 +0200 Subject: [PATCH 20/98] feat: added paymentMethod to controller --- .../java/edu/group5/app/control/DonationController.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 97a1c9d..9118c7e 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -43,6 +43,7 @@ public void handleDonate() { User currentUser = appState.getCurrentUser(); Organization currentOrg = appState.getCurrentOrganization(); BigDecimal amount = appState.getCurrentDonationAmount(); + String paymentMethod = appState.getCurrentPaymentMethod(); if (currentUser == null) { System.err.println("Error: No user logged in"); @@ -60,17 +61,21 @@ public void handleDonate() { System.err.println("Error: Invalid donation amount"); return; } + if (paymentMethod == null) { + System.out.println("Error: Invalid payment method"); + return; + } // Create donation via service boolean success = service.donate( customer, currentOrg.orgNumber(), amount, - "Online" + paymentMethod ); if (success) { - System.out.println("Donation created: " + amount + " kr to " + currentOrg.name()); + System.out.println("Donation created: " + amount + " kr to " + currentOrg.name() + ", with payment method: " + paymentMethod); } else { System.err.println("Failed to create donation"); } From 9625956db3949425b79d2a96911533cb752f4efa Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Mon, 30 Mar 2026 12:54:32 +0200 Subject: [PATCH 21/98] feat: added paymentMethodSection --- .../view/donationpage/DonationPageView.java | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 5087267..97f3b15 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -27,7 +27,9 @@ public class DonationPageView extends BorderPane { private final DonationController donationController; private final List allDonationElements = new ArrayList<>(); + private final List allPaymentElements = new ArrayList<>(); private final Map elementAmounts = new HashMap<>(); + private final Map elementPaymentMethods = new HashMap<>(); public DonationPageView(AppState appState, NavigationController nav, DonationController donationController) { this.appState = appState; @@ -37,7 +39,7 @@ public DonationPageView(AppState appState, NavigationController nav, DonationCon getStylesheets().add(getClass().getResource("/donationpage/donation.css").toExternalForm()); VBox content = new VBox(); - content.getChildren().addAll(createDonationGrid(), createDonateSection()); + content.getChildren().addAll(createDonationGrid(), createPaymentMethodSection(), createDonateSection()); setCenter(content); } @@ -115,6 +117,29 @@ private VBox createCustomButton() { allDonationElements.add(box); return box; } + + public HBox createPaymentMethodSection() { + Button appleBtn = new Button("Apple Pay"); + Button vippsBtn = new Button("Vipps"); + Button visaBtn = new Button("Visa"); + + for (Button btn : new Button[]{appleBtn, vippsBtn, visaBtn}) { + btn.getStyleClass().add("payment-method-button"); + + btn.setOnAction(e -> {selectPaymentMethod(btn);}); + allPaymentElements.add(btn); + } + + elementPaymentMethods.put(appleBtn, "Apple Pay"); + elementPaymentMethods.put(vippsBtn, "Vipps"); + elementPaymentMethods.put(visaBtn, "Visa"); + + HBox sectionPm = new HBox(appleBtn, vippsBtn, visaBtn); + sectionPm.setAlignment(Pos.CENTER); + sectionPm.setPadding(new Insets(20, 20, 20, 20)); + return sectionPm; + } + private HBox createDonateSection() { Button donateBtn = new Button("Donate"); donateBtn.getStyleClass().add("donate-button"); @@ -137,6 +162,14 @@ private void selectDonationElement(Node element) { // Extract and store the amount extractAndStoreAmount(element); } + private void selectPaymentMethod(Node element) { + for (Node node : allPaymentElements) { + node.getStyleClass().remove("payment-method-selected"); + } + element.getStyleClass().add("payment-method-selected"); + + extractAndStorePaymentMethod(element); + } private void extractAndStoreAmount(Node element) { BigDecimal amount = elementAmounts.get(element); @@ -147,6 +180,15 @@ private void extractAndStoreAmount(Node element) { } } + private void extractAndStorePaymentMethod(Node element) { + String paymentMethod = elementPaymentMethods.get(element); + if (paymentMethod != null) { + appState.setCurrentPaymentMethod(paymentMethod); + } else { + System.err.println("Error: No amount found for selected element"); + } + } + private BigDecimal parseAmount(String amountStr) { try { return new BigDecimal(amountStr.replace("kr", "").trim()); From d1c9f2be7b2b9840071dd58afbc6f7382a495e41 Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Mon, 30 Mar 2026 12:55:30 +0200 Subject: [PATCH 22/98] fead: added scrollpanes and formatted amount --- .../group5/app/view/userpage/UserPageView.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index c5d886d..b410761 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -12,6 +12,7 @@ import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; @@ -19,6 +20,7 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; +import java.math.RoundingMode; import java.util.*; @@ -73,7 +75,6 @@ private HBox createProfileSection() { profile.setAlignment(Pos.CENTER_LEFT); return profile; } - private VBox createCausesSection() { Text title = new Text("YOUR SUPPORTED CAUSES"); title.getStyleClass().add("section-title"); @@ -99,8 +100,11 @@ private VBox createCausesSection() { } } } + ScrollPane scrollPane = new ScrollPane(causesBox); + scrollPane.setFitToWidth(true); + scrollPane.setPrefHeight(150); - return new VBox(10, title, causesBox); + return new VBox(10, title, scrollPane); } private VBox createDonationsSection() { @@ -125,13 +129,17 @@ private VBox createDonationsSection() { String orgName = (org != null) ? org.name() : "Unknown Organization"; Label donationLabel = new Label( - orgName + " β€’ " + donation.amount() + " kr" + " β€’ " + donation.date() + orgName + " β€’ " + donation.amount().setScale(2, RoundingMode.HALF_UP) + " kr" + " β€’ " + + donation.date() + " β€’ " + donation.paymentMethod() ); donationsBox.getChildren().add(donationLabel); } } + ScrollPane scrollPane = new ScrollPane(donationsBox); + scrollPane.setFitToWidth(true); + scrollPane.setPrefHeight(200); - return new VBox(10, title, donationsBox); + return new VBox(10, title, scrollPane); } } From b841bb5c6353831c83632b340a042e30ff2954ce Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Mon, 30 Mar 2026 18:13:32 +0200 Subject: [PATCH 23/98] fix: fixed the buttons for payment methods --- .../view/donationpage/DonationPageView.java | 29 ++++++++++---- src/main/resources/donationpage/donation.css | 39 +++++++++++++++++-- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 97f3b15..72f0a54 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -31,6 +31,10 @@ public class DonationPageView extends BorderPane { private final Map elementAmounts = new HashMap<>(); private final Map elementPaymentMethods = new HashMap<>(); + private BigDecimal selectedAmount = null; + private String selectedPaymentMethod = null; + private Button donateBtn; + public DonationPageView(AppState appState, NavigationController nav, DonationController donationController) { this.appState = appState; this.nav = nav; @@ -106,7 +110,12 @@ private VBox createCustomButton() { box.setOnMouseClicked(e -> { try { - BigDecimal amount = new BigDecimal(amountField.getText().trim()); + String text = amountField.getText().trim(); + if (text.isEmpty()) { + return; + } + BigDecimal amount = new BigDecimal(text); + elementAmounts.put(box, amount); selectDonationElement(box); } catch (NumberFormatException exception) { @@ -136,18 +145,21 @@ public HBox createPaymentMethodSection() { HBox sectionPm = new HBox(appleBtn, vippsBtn, visaBtn); sectionPm.setAlignment(Pos.CENTER); + sectionPm.setSpacing(20); sectionPm.setPadding(new Insets(20, 20, 20, 20)); return sectionPm; } private HBox createDonateSection() { - Button donateBtn = new Button("Donate"); + donateBtn = new Button("Donate"); donateBtn.getStyleClass().add("donate-button"); + + donateBtn.setDisable(true); + donateBtn.setOnAction(e -> donationController.handleDonate()); HBox section = new HBox(donateBtn); section.setAlignment(Pos.CENTER); - section.setPadding(new Insets(20, 0, 30, 0)); return section; } @@ -174,18 +186,18 @@ private void selectPaymentMethod(Node element) { private void extractAndStoreAmount(Node element) { BigDecimal amount = elementAmounts.get(element); if (amount != null) { + selectedAmount = amount; appState.setCurrentDonationAmount(amount); - } else { - System.err.println("Error: No amount found for selected element"); + updateDonationButtonState(); } } private void extractAndStorePaymentMethod(Node element) { String paymentMethod = elementPaymentMethods.get(element); if (paymentMethod != null) { + selectedPaymentMethod = paymentMethod; appState.setCurrentPaymentMethod(paymentMethod); - } else { - System.err.println("Error: No amount found for selected element"); + updateDonationButtonState(); } } @@ -196,5 +208,8 @@ private BigDecimal parseAmount(String amountStr) { return BigDecimal.ZERO; } } + private void updateDonationButtonState() { + donateBtn.setDisable(selectedAmount == null || selectedPaymentMethod == null); + } } \ No newline at end of file diff --git a/src/main/resources/donationpage/donation.css b/src/main/resources/donationpage/donation.css index 32433df..acb2655 100644 --- a/src/main/resources/donationpage/donation.css +++ b/src/main/resources/donationpage/donation.css @@ -18,15 +18,24 @@ -fx-text-fill: white; -fx-border-color: #111; } +.donation-button-selected:hover { + -fx-background-color: #222; + -fx-border-color: #222; +} + +.donation-button-selected Text, +.donation-button-selected .donation-input { + -fx-fill: white; + -fx-border-color: transparent transparent white transparent; + -fx-text-fill: white; + -fx-prompt-text-fill: #ccc; +} + .donation-title { -fx-font-size: 18px; -fx-font-weight: bold; -fx-fill: #111; } -.donation-amount { - -fx-font-size: 18px; - -fx-fill: #111; -} .donation-input { -fx-font-size:16px; -fx-pref-width: 140px; @@ -51,3 +60,25 @@ -fx-background-color: #c02020; } +.payment-method-button { + -fx-background-color: #111; + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-pref-width: 180px; + -fx-pref-height: 45px; + -fx-background-radius: 6; + -fx-border-radius: 6; + -fx-cursor: hand; +} +.payment-method-button:hover { + -fx-background-color: #222; +} +.payment-method-selected, +.payment-method-selected:hover { + -fx-background-color: #e03030; +} +.donation-button-selected .donation-title, +.donation-button-selected .donation-amount { + -fx-fill: white; +} \ No newline at end of file From 453552437d0d2c58303b6fe4d8998f8f8dc4ce9d Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Mon, 30 Mar 2026 19:47:28 +0200 Subject: [PATCH 24/98] feat: added backbuttons with css --- .../view/donationpage/DonationPageView.java | 18 ++++++++----- .../OrganizationPageView.java | 27 ++++++++++++++----- src/main/resources/donationpage/donation.css | 16 +++++++++++ .../organizationpage/organizationpage.css | 16 +++++++++++ 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 72f0a54..63d0493 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -16,10 +16,7 @@ import javafx.scene.Node; import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public class DonationPageView extends BorderPane { private final AppState appState; @@ -40,13 +37,22 @@ public DonationPageView(AppState appState, NavigationController nav, DonationCon this.nav = nav; this.donationController = donationController; - getStylesheets().add(getClass().getResource("/donationpage/donation.css").toExternalForm()); + getStylesheets().add(Objects.requireNonNull(getClass().getResource("/donationpage/donation.css")).toExternalForm()); VBox content = new VBox(); - content.getChildren().addAll(createDonationGrid(), createPaymentMethodSection(), createDonateSection()); + content.getChildren().addAll(createBackButton(), createDonationGrid(), createPaymentMethodSection(), createDonateSection()); setCenter(content); } + private HBox createBackButton() { + Button backBtn = new Button("←"); + backBtn.getStyleClass().add("back-button"); + backBtn.setOnAction(e -> nav.showOrganizationPage()); + + HBox container = new HBox(backBtn); + container.setPadding(new Insets(10, 0, 0, 10)); + return container; + } private TilePane createDonationGrid(){ TilePane body = new TilePane(); body.setAlignment(Pos.CENTER); diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index da8d1df..684a1a6 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -4,6 +4,7 @@ import edu.group5.app.control.NavigationController; import edu.group5.app.model.AppState; import edu.group5.app.model.organization.Organization; +import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -16,6 +17,8 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; +import java.util.Objects; + public class OrganizationPageView extends BorderPane { private final AppState appState; private final NavigationController nav; @@ -26,24 +29,34 @@ public OrganizationPageView(AppState appState, NavigationController nav, Donatio this.nav = nav; this.donationController = donationController; - getStylesheets().add(getClass().getResource("/organizationpage/organizationpage.css").toExternalForm()); - setCenter(createBody()); + getStylesheets().add(Objects.requireNonNull(getClass().getResource("/organizationpage/organizationpage.css")).toExternalForm()); + + VBox content = new VBox(); + content.getChildren().addAll(createBackButton(), createBody()); + setCenter(content); } private ScrollPane createBody() { ScrollPane body = new ScrollPane(); body.setFitToWidth(true); - body.setFitToHeight(true) - ; + body.setFitToHeight(true); + VBox vBox = new VBox(); vBox.setId("main-container"); - vBox.getChildren().addAll( - createOrgSection() - ); + vBox.getChildren().addAll(createOrgSection()); body.setContent(vBox); return body; } + private HBox createBackButton() { + Button backBtn = new Button("←"); + backBtn.getStyleClass().add("back-button"); + backBtn.setOnAction(e -> nav.showCausesPage()); + + HBox container = new HBox(backBtn); + container.setPadding(new Insets(10, 0, 0, 10)); + return container; + } private HBox createOrgSection() { HBox orgSection = new HBox(); diff --git a/src/main/resources/donationpage/donation.css b/src/main/resources/donationpage/donation.css index acb2655..74851a4 100644 --- a/src/main/resources/donationpage/donation.css +++ b/src/main/resources/donationpage/donation.css @@ -81,4 +81,20 @@ .donation-button-selected .donation-title, .donation-button-selected .donation-amount { -fx-fill: white; +} +.back-button { + -fx-background-color: white; + -fx-text-fill: black; + -fx-font-weight: bold; + -fx-font-size: 20px; + -fx-background-radius: 50; + -fx-padding: 4px 10px; + -fx-cursor: hand; + -fx-border-radius: 50; + -fx-border-color: black; + -fx-border-width: 2px; +} +.back-button:hover { + -fx-background-color: #333; + -fx-border-color:#333; } \ No newline at end of file diff --git a/src/main/resources/organizationpage/organizationpage.css b/src/main/resources/organizationpage/organizationpage.css index a7276b5..8459526 100644 --- a/src/main/resources/organizationpage/organizationpage.css +++ b/src/main/resources/organizationpage/organizationpage.css @@ -28,4 +28,20 @@ #donate-button:hover { -fx-background-color: #c02020; +} +.back-button { + -fx-background-color: white; + -fx-text-fill: black; + -fx-font-weight: bold; + -fx-font-size: 20px; + -fx-background-radius: 50; + -fx-padding: 4px 10px; + -fx-cursor: hand; + -fx-border-radius: 50; + -fx-border-color: black; + -fx-border-width: 2px; +} +.back-button:hover { + -fx-background-color: #333; + -fx-border-color:#333; } \ No newline at end of file From 2f11b6852366b89c4756bfc34217b28fb092fb21 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Sun, 5 Apr 2026 14:38:52 +0200 Subject: [PATCH 25/98] update: update speed of rendering OrgCards and Org image with ParallelStream --- .../organization/OrganizationService.java | 33 ++++++------- .../app/view/causespage/CausesPageView.java | 49 ++++++++++++------- .../app/view/causespage/OrganizationCard.java | 43 ++++++++++++++-- .../OrganizationPageView.java | 2 + 4 files changed, 88 insertions(+), 39 deletions(-) diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java index 9785040..8ebd625 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -3,6 +3,7 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; +import java.util.stream.Collectors; import java.util.HashMap; import java.util.Map; @@ -90,7 +91,9 @@ public String fetchLogoUrl(String pageUrl) { } try { - Document doc = Jsoup.connect(pageUrl).get(); + Document doc = Jsoup.connect(pageUrl) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .timeout(5000).get(); Element img = doc.selectFirst("div.logo img"); if (img != null) { @@ -116,23 +119,17 @@ public String fetchLogoUrl(String pageUrl) { */ public Map getTrustedOrganizationsWithLogos() { Map original = getTrustedOrganizations(); - Map trustedOrgsWithLogos = new HashMap<>(); - - for (Organization org : original.values()) { - String logoUrl = fetchLogoUrl(org.websiteUrl()); - - Organization newOrg = new Organization( - org.orgNumber(), - org.name(), - org.trusted(), - org.websiteUrl(), - org.isPreApproved(), - org.description(), - logoUrl - ); - trustedOrgsWithLogos.put(newOrg.orgNumber(), newOrg); - } - return trustedOrgsWithLogos; + return original.values().parallelStream() + .map(org -> new Organization( + org.orgNumber(), + org.name(), + org.trusted(), + org.websiteUrl(), + org.isPreApproved(), + org.description(), + fetchLogoUrl(org.websiteUrl()) + )) + .collect(Collectors.toMap(Organization::orgNumber, org -> org)); } /** diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 1cd6e83..5cfc197 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -41,11 +41,41 @@ private ScrollPane createBody() { vBox.setStyle("-fx-padding: 10;"); vBox.setSpacing(10); vBox.setMaxWidth(Double.MAX_VALUE); + + // Load organizations INSTANTLY from cache + allOrganizations = orgController.getTrustedOrgs(); + vBox.getChildren().addAll( createSearchSection(), createOrganizationSection(null) ); body.setContent(vBox); + + // Build a map of org ID -> card for quick lookup + Map cardMap = new HashMap<>(); + for (var node : organizationGrid.getChildren()) { + if (node instanceof OrganizationCard card) { + cardMap.put(card.getOrganization().orgNumber(), card); + } + } + + // Fetch logos and update existing cards (don't rebuild grid) + orgController.getOrganizationsWithLogosAsync() + .thenAccept(orgs -> {this.allOrganizations = orgs; + Platform.runLater(() -> { + for (var entry : orgs.entrySet()) { + OrganizationCard card = cardMap.get(entry.getKey()); + if (card != null && entry.getValue().logoUrl() != null) { + card.updateLogo(entry.getValue().logoUrl()); + } + } + Organization currentOrg = appState.getCurrentOrganization(); + if (currentOrg != null && orgs.containsKey(currentOrg.orgNumber())) { + appState.setCurrentOrganization(orgs.get(currentOrg.orgNumber())); + } + }); + }); + return body; } @@ -76,25 +106,8 @@ private GridPane createOrganizationSection(String searchTerm) { organizationGrid = grid; } - if (allOrganizations == null) { - allOrganizations = orgController.getTrustedOrgs(); - - //Show loading text while organizations and logos are fetched - grid.add(new javafx.scene.control.Label("Loading..."), 0, 0); - - //Fetch trusted organizations with logos asynchronously (runs in background) - orgController.getOrganizationsWithLogosAsync() - .thenAccept(orgs -> { - this.allOrganizations = orgs; - - // Update UI when data is ready - Platform.runLater(() -> updateOrganizationGrid("")); - }); - return grid; - } - Map organizations = new HashMap<>(); - if (searchTerm != null) { + if (searchTerm != null && !searchTerm.isEmpty()) { // Filter organizations by search term organizations = filterOrganizations(searchTerm); } else { diff --git a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java index 31b25ce..2114d17 100644 --- a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java +++ b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java @@ -14,6 +14,8 @@ public class OrganizationCard extends VBox { private final AppState appState; private final Organization organization; private final NavigationController nav; + private StackPane imageContainer; + private String currentLogoUrl; public OrganizationCard(AppState appstate, NavigationController nav, Organization org, String img) { this.appState = appstate; @@ -22,14 +24,15 @@ public OrganizationCard(AppState appstate, NavigationController nav, Organizatio setId("mainContainer"); getStylesheets().add(getClass().getResource("/browsepage/browse_org.css").toExternalForm()); + imageContainer = createImageContainer(img); getChildren().addAll( - imageContainer(img), + imageContainer, orgName(org.name()), checkMarkContainer() ); setOnMouseClicked(e -> { - appstate.setCurrentOrganization(organization); + appstate.setCurrentOrganization(getOrganizationWithCurrentLogo()); nav.showOrganizationPage(); }); @@ -38,7 +41,41 @@ public OrganizationCard(AppState appstate, NavigationController nav, Organizatio setAlignment(Pos.CENTER); } - private StackPane imageContainer(String img) { + public Organization getOrganization() { + return organization; + } + + public void updateLogo(String logoUrl) { + this.currentLogoUrl = logoUrl; + if (imageContainer == null) return; + imageContainer.getChildren().clear(); + if (logoUrl != null && !logoUrl.isBlank()) { + ImageView logo = new ImageView(new Image(logoUrl, true)); + logo.setId("logo"); + logo.setSmooth(true); + logo.setPreserveRatio(true); + logo.setFitHeight(80); + logo.setFitWidth(80); + imageContainer.getChildren().add(logo); + } + } + + private Organization getOrganizationWithCurrentLogo() { + if (currentLogoUrl == null) { + return organization; + } + return new Organization( + organization.orgNumber(), + organization.name(), + organization.trusted(), + organization.websiteUrl(), + organization.isPreApproved(), + organization.description(), + currentLogoUrl + ); + } + + private StackPane createImageContainer(String img) { StackPane imageContainer = new StackPane(); imageContainer.setId("imageContainer"); diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index da8d1df..1ac2fa6 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -68,6 +68,8 @@ private StackPane createImageContainer() { logo.setId("logo"); logo.setSmooth(true); logo.setPreserveRatio(true); + logo.setFitHeight(120); + logo.setFitWidth(120); imageContainer.getChildren().add(logo); } else { StackPane placeholder = new StackPane(); From ed93b95e8d25392a1267d6a7097e55bcf8c60884 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Sun, 5 Apr 2026 14:45:16 +0200 Subject: [PATCH 26/98] update&perf[App]: Update and infcreased performance of org.logos rendering by adding it into init in App --- src/main/java/edu/group5/app/App.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index 22b3baa..c83fe44 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -71,6 +71,9 @@ public void init() { UserService userService = new UserService(this.userRepository); DonationService donationService = new DonationService(this.donationRepository, organizationRepository); OrganizationService organizationService = new OrganizationService(organizationRepository); + + // Pre-load logos in background so they're ready when user views causes page + organizationService.getTrustedOrganizationsWithLogosAsync(); this.root = new BorderPane(); this.appState = new AppState(); From 8fb60ceee79cbc4955f66385b0b836695415a7a9 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:36:26 +0200 Subject: [PATCH 27/98] refactor[wrapper]: move wrapper module from control to model Move wrapper module from control subdirectory to model subdirectory, and rename all mentions of module to reflect this --- src/main/java/edu/group5/app/App.java | 4 ++-- .../edu/group5/app/{control => model}/wrapper/DbWrapper.java | 2 +- .../group5/app/{control => model}/wrapper/OrgApiWrapper.java | 2 +- .../edu/group5/app/{control => model}/wrapper/Wrapper.java | 2 +- .../{control => model}/wrapper/DbWrapperDonationsTest.java | 4 +++- .../app/{control => model}/wrapper/DbWrapperUserTest.java | 3 ++- .../app/{control => model}/wrapper/OrgApiWrapperTest.java | 4 +++- 7 files changed, 13 insertions(+), 8 deletions(-) rename src/main/java/edu/group5/app/{control => model}/wrapper/DbWrapper.java (99%) rename src/main/java/edu/group5/app/{control => model}/wrapper/OrgApiWrapper.java (98%) rename src/main/java/edu/group5/app/{control => model}/wrapper/Wrapper.java (95%) rename src/test/java/edu/group5/app/{control => model}/wrapper/DbWrapperDonationsTest.java (99%) rename src/test/java/edu/group5/app/{control => model}/wrapper/DbWrapperUserTest.java (98%) rename src/test/java/edu/group5/app/{control => model}/wrapper/OrgApiWrapperTest.java (97%) diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index 22b3baa..be397d8 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -1,8 +1,6 @@ package edu.group5.app; import edu.group5.app.control.NavigationController; -import edu.group5.app.control.wrapper.DbWrapper; -import edu.group5.app.control.wrapper.OrgApiWrapper; import edu.group5.app.model.AppState; import edu.group5.app.model.donation.DonationRepository; import edu.group5.app.model.donation.DonationService; @@ -10,6 +8,8 @@ import edu.group5.app.model.organization.OrganizationService; import edu.group5.app.model.user.UserRepository; import edu.group5.app.model.user.UserService; +import edu.group5.app.model.wrapper.DbWrapper; +import edu.group5.app.model.wrapper.OrgApiWrapper; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.image.Image; diff --git a/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java similarity index 99% rename from src/main/java/edu/group5/app/control/wrapper/DbWrapper.java rename to src/main/java/edu/group5/app/model/wrapper/DbWrapper.java index 7e3adc0..073fd81 100644 --- a/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -1,4 +1,4 @@ -package edu.group5.app.control.wrapper; +package edu.group5.app.model.wrapper; import java.math.BigDecimal; import java.sql.Connection; diff --git a/src/main/java/edu/group5/app/control/wrapper/OrgApiWrapper.java b/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java similarity index 98% rename from src/main/java/edu/group5/app/control/wrapper/OrgApiWrapper.java rename to src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java index 39ac283..910b110 100644 --- a/src/main/java/edu/group5/app/control/wrapper/OrgApiWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java @@ -1,4 +1,4 @@ -package edu.group5.app.control.wrapper; +package edu.group5.app.model.wrapper; import java.io.IOException; import java.net.URI; diff --git a/src/main/java/edu/group5/app/control/wrapper/Wrapper.java b/src/main/java/edu/group5/app/model/wrapper/Wrapper.java similarity index 95% rename from src/main/java/edu/group5/app/control/wrapper/Wrapper.java rename to src/main/java/edu/group5/app/model/wrapper/Wrapper.java index 992b7a9..85d8dc8 100644 --- a/src/main/java/edu/group5/app/control/wrapper/Wrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/Wrapper.java @@ -1,4 +1,4 @@ -package edu.group5.app.control.wrapper; +package edu.group5.app.model.wrapper; /** * An abstract class for all Wrappers of datasets. diff --git a/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java b/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java similarity index 99% rename from src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java rename to src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java index 8c401c4..cf76092 100644 --- a/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java @@ -1,4 +1,4 @@ -package edu.group5.app.control.wrapper; +package edu.group5.app.model.wrapper; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -16,6 +16,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import edu.group5.app.model.wrapper.DbWrapper; + public class DbWrapperDonationsTest { private Object[] johnDonation; private List users; diff --git a/src/test/java/edu/group5/app/control/wrapper/DbWrapperUserTest.java b/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java similarity index 98% rename from src/test/java/edu/group5/app/control/wrapper/DbWrapperUserTest.java rename to src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java index 10c514f..7aca3c0 100644 --- a/src/test/java/edu/group5/app/control/wrapper/DbWrapperUserTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java @@ -1,4 +1,4 @@ -package edu.group5.app.control.wrapper; +package edu.group5.app.model.wrapper; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import edu.group5.app.model.wrapper.DbWrapper; import javafx.util.converter.BigDecimalStringConverter; public class DbWrapperUserTest { diff --git a/src/test/java/edu/group5/app/control/wrapper/OrgApiWrapperTest.java b/src/test/java/edu/group5/app/model/wrapper/OrgApiWrapperTest.java similarity index 97% rename from src/test/java/edu/group5/app/control/wrapper/OrgApiWrapperTest.java rename to src/test/java/edu/group5/app/model/wrapper/OrgApiWrapperTest.java index 23b30ec..0a643f2 100644 --- a/src/test/java/edu/group5/app/control/wrapper/OrgApiWrapperTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/OrgApiWrapperTest.java @@ -1,4 +1,4 @@ -package edu.group5.app.control.wrapper; +package edu.group5.app.model.wrapper; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -9,6 +9,8 @@ import java.lang.IllegalArgumentException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + +import edu.group5.app.model.wrapper.OrgApiWrapper; import tools.jackson.core.exc.StreamReadException; /** From c5c3ce9af9764659eabeec75b0eda13b5a7f5f46 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Wed, 8 Apr 2026 11:54:07 +0200 Subject: [PATCH 28/98] fix[App]: remove preloading redundancy --- src/main/java/edu/group5/app/App.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index c83fe44..fa48178 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -71,10 +71,6 @@ public void init() { UserService userService = new UserService(this.userRepository); DonationService donationService = new DonationService(this.donationRepository, organizationRepository); OrganizationService organizationService = new OrganizationService(organizationRepository); - - // Pre-load logos in background so they're ready when user views causes page - organizationService.getTrustedOrganizationsWithLogosAsync(); - this.root = new BorderPane(); this.appState = new AppState(); this.nav = new NavigationController(root, appState, userService, donationService, organizationService); From a2a087b70b55435d5293f890b17bf1c00b1b84f0 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Wed, 8 Apr 2026 15:17:55 +0200 Subject: [PATCH 29/98] feat&update[OrganizationPage]: Update description to be description fetched from the API --- src/main/java/edu/group5/app/App.java | 8 +- .../organization/OrganizationRepository.java | 26 +++-- .../organization/OrganizationScraper.java | 107 ++++++++++++++++++ .../organization/OrganizationService.java | 56 +++------ .../OrganizationPageView.java | 24 +++- .../organizationpage/organizationpage.css | 44 ++++--- 6 files changed, 192 insertions(+), 73 deletions(-) create mode 100644 src/main/java/edu/group5/app/model/organization/OrganizationScraper.java diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index fa48178..b59c6ef 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -7,6 +7,7 @@ import edu.group5.app.model.donation.DonationRepository; import edu.group5.app.model.donation.DonationService; import edu.group5.app.model.organization.OrganizationRepository; +import edu.group5.app.model.organization.OrganizationScraper; import edu.group5.app.model.organization.OrganizationService; import edu.group5.app.model.user.UserRepository; import edu.group5.app.model.user.UserService; @@ -62,15 +63,16 @@ public void init() { System.err.println("Failed to load organization data: " + e.getMessage()); } - // Create repositories with fetched data + // Create scraper and repositories with fetched data + OrganizationScraper orgScraper = new OrganizationScraper(); this.userRepository = new UserRepository(userData); this.donationRepository = new DonationRepository(donationData); - OrganizationRepository organizationRepository = new OrganizationRepository(organizationData); + OrganizationRepository organizationRepository = new OrganizationRepository(organizationData, orgScraper); // Create services (backend wiring) UserService userService = new UserService(this.userRepository); DonationService donationService = new DonationService(this.donationRepository, organizationRepository); - OrganizationService organizationService = new OrganizationService(organizationRepository); + OrganizationService organizationService = new OrganizationService(organizationRepository, orgScraper); this.root = new BorderPane(); this.appState = new AppState(); this.nav = new NavigationController(root, appState, userService, donationService, organizationService); diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java index cc0a6b1..f31efdc 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java @@ -11,25 +11,33 @@ * Repository class for managing Organization entities. It provides methods to retrieve trusted organizations, * find organizations by their organization number or name, and initializes the repository with input data. * The repository uses a HashMap to store Organization objects for efficient retrieval based on their organization number. - * Handles the business logic associated with organizations + * Delegates web scraping to OrganizationScraper for separation of concerns. */ public class OrganizationRepository extends Repository { private final HashMap grandMap; + private final OrganizationScraper scraper; /** - * Initializes the repository with the given input data, c - * onverting it into Organization objects and storing them in a map for efficient retrieval. - * The input is expected to be an array of objects, where each object contains + * Initializes the repository with the given input data and scraper. + * Converts input into Organization objects and stores them in a map for efficient retrieval. + * The input is expected to be an array of objects, where each object contains * the necessary information to create an Organization. + * * @param input the input data used to populate the repository, must not be null - * @throws IllegalArgumentException if the input is null + * @param scraper the OrganizationScraper to use for fetching web data, must not be null + * @throws IllegalArgumentException if input or scraper is null */ - public OrganizationRepository(Object[] input) { - super(new HashMap<>()); - grandMap = new HashMap<>(); + public OrganizationRepository(Object[] input, OrganizationScraper scraper) { + super(new HashMap<>()); + this.grandMap = new HashMap<>(); if (input == null) { throw new IllegalArgumentException("The input cannot be null"); } + if (scraper == null) { + throw new IllegalArgumentException("The scraper cannot be null"); + } + this.scraper = scraper; + ObjectMapper mapper = new ObjectMapper(); for (Object obj : input) { @@ -42,7 +50,7 @@ public OrganizationRepository(Object[] input) { boolean trusted = "approved".equalsIgnoreCase((String) contentMap.get("status")); String websiteURL = (String) contentMap.get("url"); boolean isPreApproved = Boolean.TRUE.equals(contentMap.get("is_pre_approved")); - String description = "Information about " + name; + String description = scraper.fetchDescription(websiteURL) != null ? scraper.fetchDescription(websiteURL) : "Information about " + name; Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description, null); grandMap.put(org.orgNumber(), org); diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java new file mode 100644 index 0000000..edb41fd --- /dev/null +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -0,0 +1,107 @@ +package edu.group5.app.model.organization; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import java.util.stream.Collectors; + +import java.util.HashMap; +import java.util.Map; + +/** + * Handles web scraping of organization information from Innsamlingskontrollen. + * Responsible for fetching logos and descriptions from organization pages. + * All results are cached to avoid redundant network requests. + */ +public class OrganizationScraper { + private final Map logoCache = new HashMap<>(); + private final Map descriptionCache = new HashMap<>(); + + /** + * Fetches the description for the given URL by scraping all text content + * inside {@code
    }. Results are cached. + * + *

    Strategy:

    + *
      + *
    1. Tries to get all <p> tags (skipping the first one) and concatenates them
    2. + *
    3. If no paragraphs found, gets all text content from the section
    4. + *
    5. Returns null if section not found or is empty
    6. + *
    + * + * @param pageUrl the URL for the organization's page; may be null or blank + * @return the description text, or null if not found or pageUrl is invalid + */ + public String fetchDescription(String pageUrl) { + if (pageUrl == null || pageUrl.isBlank()) { + return null; + } + + if (descriptionCache.containsKey(pageUrl)) { + return descriptionCache.get(pageUrl); + } + + try { + Document doc = Jsoup.connect(pageUrl) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .timeout(5000).get(); + + Element section = doc.selectFirst("section.information"); + if (section != null) { + // Try to get all

    tags (skip first one if multiple exist) + String description = section.select("p").stream() + .skip(1) // Skip first paragraph (usually a heading) + .map(Element::text) + .filter(text -> !text.isBlank()) + .map(String::trim) + .collect(Collectors.joining("\n\n")); + + // Fallback: if no paragraphs after first, get all text from section + if (description.isBlank()) { + description = section.text().trim(); + } + + // Only cache and return if we found something meaningful + if (!description.isBlank()) { + descriptionCache.put(pageUrl, description); + return description; + } + } + } catch (Exception e) { + System.out.println("Could not get description for: " + pageUrl); + } + return null; + } + + /** + * Fetches the logo URL for the given page by scraping the {@code div.logo img} + * element. Results are cached so each URL is only fetched once. + * + * @param pageUrl the URL for the organization's page; may be null or blank + * @return the absolute logo URL, or null if not found or pageUrl is invalid + */ + public String fetchLogoUrl(String pageUrl) { + if (pageUrl == null || pageUrl.isBlank()) { + return null; + } + + if (logoCache.containsKey(pageUrl)) { + return logoCache.get(pageUrl); + } + + try { + Document doc = Jsoup.connect(pageUrl) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .timeout(5000).get(); + Element img = doc.selectFirst("div.logo img"); + + if (img != null) { + String logoUrl = img.absUrl("src"); + logoCache.put(pageUrl, logoUrl); + return logoUrl; + } + } catch (Exception e) { + System.out.println("Could not get logo for: " + pageUrl); + } + return null; + } +} diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java index 8ebd625..170b9a5 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -1,8 +1,5 @@ package edu.group5.app.model.organization; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; import java.util.stream.Collectors; import java.util.HashMap; @@ -14,26 +11,29 @@ * It interacts with the OrganizationRepository to retrieve organization information * and contains business logic associated with organization management. * - *

    It provides fetching logo URLs by web scraping each organization's page on - * Innsamlingskontrollen.

    + *

    It provides fetching logo URLs by delegating to OrganizationScraper.

    * * Fetched logo URLs are cached to avoid redundant network requests. */ public class OrganizationService { private OrganizationRepository organizationRepository; - - private final Map logoCache = new HashMap<>(); + private OrganizationScraper scraper; /** - * Constructs an OrganizationService with the given OrganizationRepository. + * Constructs an OrganizationService with the given OrganizationRepository and scraper. * @param organizationRepository the OrganizationRepository to use for managing organization data; must not be null - * @throws IllegalArgumentException if organizationRepository is null + * @param scraper the OrganizationScraper to use for fetching web data; must not be null + * @throws IllegalArgumentException if organizationRepository or scraper is null */ - public OrganizationService(OrganizationRepository organizationRepository) { + public OrganizationService(OrganizationRepository organizationRepository, OrganizationScraper scraper) { if (organizationRepository == null) { throw new IllegalArgumentException("OrganizationRepository cannot be null"); } + if (scraper == null) { + throw new IllegalArgumentException("OrganizationScraper cannot be null"); + } this.organizationRepository = organizationRepository; + this.scraper = scraper; } /** @@ -81,39 +81,13 @@ public Organization findByOrgName(String name) { * @return the absolute logo URL, or null if not found or pageUrl is invalid */ - public String fetchLogoUrl(String pageUrl) { - if (pageUrl == null || pageUrl.isBlank()) { - return null; - } - - if (logoCache.containsKey(pageUrl)) { - return logoCache.get(pageUrl); - } - - try { - Document doc = Jsoup.connect(pageUrl) - .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") - .timeout(5000).get(); - Element img = doc.selectFirst("div.logo img"); - - if (img != null) { - String logoUrl = img.absUrl("src"); - logoCache.put(pageUrl, logoUrl); - return logoUrl; - } - } catch (Exception e) { - System.out.println("Could not get logo for: " + pageUrl); - } - return null; - } - /** - * Fetches all trusted organizations with their logo URLs. + * Fetches all trusted organizations with their logo URLs and descriptions. * *

    - * For each trusted organization, attempts to get its logo using - * {@link #fetchLogoUrl(String)}. Creates a new Organization - * object including the logo URL. + * For each trusted organization, attempts to get its logo using the scraper. + * Creates a new Organization object including the logo URL (description is + * already fetched during repository initialization). *

    * @return a map of trusted organizations keyed by organization number, with logos included */ @@ -127,7 +101,7 @@ public Map getTrustedOrganizationsWithLogos() { org.websiteUrl(), org.isPreApproved(), org.description(), - fetchLogoUrl(org.websiteUrl()) + scraper.fetchLogoUrl(org.websiteUrl()) )) .collect(Collectors.toMap(Organization::orgNumber, org -> org)); } diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 1ac2fa6..2d10328 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -8,6 +8,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextArea; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; @@ -68,8 +69,8 @@ private StackPane createImageContainer() { logo.setId("logo"); logo.setSmooth(true); logo.setPreserveRatio(true); - logo.setFitHeight(120); - logo.setFitWidth(120); + logo.setFitHeight(350); + logo.setFitWidth(350); imageContainer.getChildren().add(logo); } else { StackPane placeholder = new StackPane(); @@ -90,14 +91,27 @@ private VBox createOrgInfoSection() { orgInfoSection.setSpacing(50); VBox orgNameAndDescription = new VBox(); + orgNameAndDescription.setSpacing(5); Label orgName = new Label(org != null ? org.name() : "Unknown Organization"); orgName.setId("orgName"); - Text description = new Text(org != null ? org.description() : "No description available"); - description.setId("description"); + VBox descriptionBox = new VBox(); + descriptionBox.setSpacing(15); + descriptionBox.setId("description-container"); + + if (org != null && org.description() != null) { + String[] paragraphs = org.description().split("\n\n"); + for (String para : paragraphs) { + if (!para.isBlank()) { + Label paragraph = new Label(para.trim()); + paragraph.setWrapText(true); + descriptionBox.getChildren().add(paragraph); + } + } + } - orgNameAndDescription.getChildren().addAll(orgName, description); + orgNameAndDescription.getChildren().addAll(orgName, descriptionBox); Button donateBtn = new Button("Donate"); donateBtn.setId("donate-button"); diff --git a/src/main/resources/organizationpage/organizationpage.css b/src/main/resources/organizationpage/organizationpage.css index a7276b5..e5263f2 100644 --- a/src/main/resources/organizationpage/organizationpage.css +++ b/src/main/resources/organizationpage/organizationpage.css @@ -1,31 +1,45 @@ #main-container { - -fx-padding: 50px + -fx-padding: 50px } #logo { - -fx-min-height: 50%; + -fx-min-height: 80%; } #orgName { - -fx-font-weight: bold; - -fx-font-size: 20pt; + -fx-font-weight: bold; + -fx-font-size: 28pt; + -fx-padding: 0 0 30 0; } -#description { - -fx-font-size: 10pt; +#description-container { + -fx-padding: 30 0 30 0; + -fx-spacing: 25; +} + +#description-paragraph { + -fx-font-size: 16; + -fx-text-fill: #222; + -fx-font-family: "Segoe UI", Arial, sans-serif; + -fx-padding: 15 50 15 50; + -fx-line-spacing: 6; + -fx-text-alignment: Left; + -fx-wrap-text: true; + } #donate-button { - -fx-pref-height: 55px; - -fx-background-color: #e03030; - -fx-text-fill: white; - -fx-font-size: 22px; - -fx-font-weight: bold; - -fx-background-radius: 8; - -fx-cursor: hand; - -fx-padding: 0 40 0 40; + -fx-pref-height: 55px; + -fx-background-color: #e03030; + -fx-text-fill: white; + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-padding: 0 40 0 40; + -fx-margin-top: 30; } #donate-button:hover { - -fx-background-color: #c02020; + -fx-background-color: #c02020; } \ No newline at end of file From 456a1d63d22253ab1d051ca091222be0c74b4960 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Wed, 8 Apr 2026 15:32:34 +0200 Subject: [PATCH 30/98] update&test[Organization]: Update JUnit tests with new features regarding OrganizationPage --- .../model/donation/DonationServiceTest.java | 11 ++-- .../OrganizationRepositoryTest.java | 26 +++++++- .../organization/OrganizationScraperTest.java | 66 +++++++++++++++++++ .../organization/OrganizationServiceTest.java | 57 ++++++++++------ 4 files changed, 134 insertions(+), 26 deletions(-) create mode 100644 src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java diff --git a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java index 80d37bf..3ed1aec 100644 --- a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java +++ b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java @@ -1,6 +1,7 @@ package edu.group5.app.model.donation; import edu.group5.app.model.organization.OrganizationRepository; +import edu.group5.app.model.organization.OrganizationScraper; import edu.group5.app.model.user.Customer; import org.junit.jupiter.api.BeforeEach; @@ -21,18 +22,20 @@ class DonationServiceTest { private OrganizationRepository organizationRepository; private DonationService donationService; private Customer customer; + private OrganizationScraper scraper; @BeforeEach void setUp() { + scraper = new OrganizationScraper(); HashMap orgMap = new HashMap<>(); - orgMap.put("org_number", "101"); + orgMap.put("org_number", "101"); orgMap.put("name", "CharityOrg"); - orgMap.put("status", "approved"); + orgMap.put("status", "approved"); orgMap.put("url", "https://charity.org"); - orgMap.put("is_pre_approved", true); + orgMap.put("is_pre_approved", true); Object[] orgInput = new Object[]{ orgMap }; - organizationRepository = new OrganizationRepository(orgInput); + organizationRepository = new OrganizationRepository(orgInput, scraper); donationRepository = new DonationRepository(new ArrayList<>()); diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java index f821e35..8906395 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java @@ -10,9 +10,11 @@ class OrganizationRepositoryTest { private OrganizationRepository repository; + private OrganizationScraper scraper; @BeforeEach void setUp() { + scraper = new OrganizationScraper(); Object[] content = new Object[] { Map.of( "org_number", "1", @@ -43,13 +45,13 @@ void setUp() { "is_pre_approved", true ) }; - repository = new OrganizationRepository(content); + repository = new OrganizationRepository(content, scraper); } private void constructorTest(Object[] input, String expectedMessage) { IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, - () -> new OrganizationRepository(input) + () -> new OrganizationRepository(input, scraper) ); assertEquals(expectedMessage, exception.getMessage()); } @@ -58,6 +60,24 @@ void constructor_ThrowsWhenContentIsNull() { constructorTest(null, "The input cannot be null"); } + @Test + void constructor_ThrowsWhenScraperIsNull() { + Object[] content = new Object[] { + Map.of( + "org_number", "1", + "name", "Org", + "status", "approved", + "url", "org.com", + "is_pre_approved", true + ) + }; + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new OrganizationRepository(content, null) + ); + assertEquals("The scraper cannot be null", exception.getMessage()); + } + @Test void getTrustedOrganizations_OnlyReturnsTrustedOrganizations() { Map trusted = repository.getTrustedOrganizations(); @@ -128,7 +148,7 @@ void testExportAllOrganizations() { @Test void testExportAllOrganizationsThrowsWhenRepositoryIsEmpty() { - OrganizationRepository emptyRepo = new OrganizationRepository(new Object[0]); + OrganizationRepository emptyRepo = new OrganizationRepository(new Object[0], scraper); IllegalStateException exception = assertThrows( IllegalStateException.class, () -> emptyRepo.export() ); diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java new file mode 100644 index 0000000..c6c318b --- /dev/null +++ b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java @@ -0,0 +1,66 @@ +package edu.group5.app.model.organization; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class OrganizationScraperTest { + + private OrganizationScraper scraper; + + @BeforeEach + void setUp() { + scraper = new OrganizationScraper(); + } + + @Test + void fetchDescription_ReturnsNullWhenUrlIsNull() { + assertNull(scraper.fetchDescription(null)); + } + + @Test + void fetchDescription_ReturnsNullWhenUrlIsBlank() { + assertNull(scraper.fetchDescription("")); + } + + @Test + void fetchDescription_ReturnsNullWhenUrlIsInvalid() { + String result = scraper.fetchDescription("https://invalid-url-that-does-not-exist-xyz123.com"); + assertNull(result); + } + + @Test + void fetchDescription_CachesResultOnSecondCall() { + // Mock URLs won't work, but cache still works with null returns + scraper.fetchDescription("https://example.com"); + scraper.fetchDescription("https://example.com"); + // If no exception thrown, cache works + assertTrue(true); + } + + @Test + void fetchLogoUrl_ReturnsNullWhenUrlIsNull() { + assertNull(scraper.fetchLogoUrl(null)); + } + + @Test + void fetchLogoUrl_ReturnsNullWhenUrlIsBlank() { + assertNull(scraper.fetchLogoUrl("")); + } + + @Test + void fetchLogoUrl_ReturnsNullWhenUrlIsInvalid() { + String result = scraper.fetchLogoUrl("https://invalid-url-that-does-not-exist-xyz123.com"); + assertNull(result); + } + + @Test + void fetchLogoUrl_CachesResultOnSecondCall() { + // Mock URLs won't work, but cache still works with null returns + scraper.fetchLogoUrl("https://example.com"); + scraper.fetchLogoUrl("https://example.com"); + // If no exception thrown, cache works + assertTrue(true); + } +} diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java index 0920e67..2d76391 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java @@ -6,14 +6,17 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; public class OrganizationServiceTest { private OrganizationRepository repo; private OrganizationService service; + private OrganizationScraper scraper; private Object[] content; @BeforeEach public void setUp() { + scraper = new OrganizationScraper(); Map orgMap = new HashMap<>(); orgMap.put("org_number", "1"); orgMap.put("name", "Misjonsalliansen"); @@ -22,22 +25,52 @@ public void setUp() { orgMap.put("is_pre_approved", false); content = new Object[]{orgMap}; - repo = new OrganizationRepository(content); - service = new OrganizationService(repo); + repo = new OrganizationRepository(content, scraper); + service = new OrganizationService(repo, scraper); } @Test - void constructor_throwsIfNull() { + void constructor_throwsIfRepositoryIsNull() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, - () -> new OrganizationService(null)); + () -> new OrganizationService(null, scraper)); assertEquals("OrganizationRepository cannot be null", ex.getMessage()); } + @Test + void constructor_throwsIfScraperIsNull() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new OrganizationService(repo, null)); + assertEquals("OrganizationScraper cannot be null", ex.getMessage()); + } + @Test void testGetOrganizationRepository() { assertEquals(repo, service.getOrganizationRepository()); } + @Test + void testGetTrustedOrganizationsWithLogos() { + Map orgsWithLogos = service.getTrustedOrganizationsWithLogos(); + assertNotNull(orgsWithLogos); + assertTrue(orgsWithLogos.containsKey(1)); + Organization org = orgsWithLogos.get(1); + assertEquals(1, org.orgNumber()); + assertEquals("Misjonsalliansen", org.name()); + assertNotNull(org); + } + + @Test + void testGetTrustedOrganizationsWithLogosAsync() throws Exception { + CompletableFuture> futureOrgs = + service.getTrustedOrganizationsWithLogosAsync(); + + assertNotNull(futureOrgs); + Map orgsWithLogos = futureOrgs.get(); + assertNotNull(orgsWithLogos); + assertTrue(orgsWithLogos.containsKey(1)); + assertEquals("Misjonsalliansen", orgsWithLogos.get(1).name()); + } + @Test void testGetTrustedOrganizations() { Map trustedOrgs = service.getTrustedOrganizations(); @@ -67,19 +100,5 @@ void testFindByOrgName() { assertEquals(1, org.orgNumber()); assertEquals("Misjonsalliansen", org.name()); } - - @Test - void fetchLogoUrlReturnsNullWhenUrlIsNull() { - assertNull(service.fetchLogoUrl(null)); - } - @Test - void fetchLogoUrlReturnsNullWhenUrlIsBlank() { - assertNull(service.fetchLogoUrl("")); - } - @Test - void fetchLogoUrlCachesResultOnSecondCall() { - String result1 = service.fetchLogoUrl("https://"); - String result2 = service.fetchLogoUrl("https://"); - assertEquals(result1, result2); - } } + From fbc4ed1c0a1fc5b09d65b5d16a9b6f236f440b5c Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 9 Apr 2026 09:29:06 +0200 Subject: [PATCH 31/98] update[Organization]: Update Hashmap of Organizations to be displayed in alphabetical order for more manuverable causesPage --- .../app/model/organization/OrganizationService.java | 11 ++++++++--- .../group5/app/view/causespage/CausesPageView.java | 7 ++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java index 170b9a5..e19c273 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -2,7 +2,8 @@ import java.util.stream.Collectors; -import java.util.HashMap; +import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -49,7 +50,10 @@ public OrganizationRepository getOrganizationRepository() { * @return a map of trusted organizations by organization number */ public Map getTrustedOrganizations() { - return organizationRepository.getTrustedOrganizations(); + return organizationRepository.getTrustedOrganizations().values().stream() + .sorted(Comparator.comparing(Organization::name)) + .collect(Collectors.toMap(Organization::orgNumber, + org -> org, (e1, e2) -> e1, LinkedHashMap::new)); } /** @@ -103,7 +107,8 @@ public Map getTrustedOrganizationsWithLogos() { org.description(), scraper.fetchLogoUrl(org.websiteUrl()) )) - .collect(Collectors.toMap(Organization::orgNumber, org -> org)); + .sorted(Comparator.comparing(Organization::name)) + .collect(Collectors.toMap(Organization::orgNumber, org -> org, (e1, e2) -> e1, LinkedHashMap::new)); } /** diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 5cfc197..36c056f 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -10,8 +10,10 @@ import javafx.scene.layout.*; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; +import java.util.Comparator; public class CausesPageView extends BorderPane { private final AppState appState; @@ -158,9 +160,12 @@ private Map filterOrganizations(String searchTerm) { String lowerSearchTerm = searchTerm.toLowerCase(); return allOrganizations.values().stream() .filter(org -> org.name().toLowerCase().contains(lowerSearchTerm)) + .sorted(Comparator.comparing(Organization::name)) .collect(Collectors.toMap( Organization::orgNumber, - org -> org + org -> org, + (e1, e2) -> e1, + LinkedHashMap::new )); } From 4dcccb28b63b166ee7f8c6f6b8c96f92d3c2525b Mon Sep 17 00:00:00 2001 From: emilfa Date: Thu, 9 Apr 2026 10:38:24 +0200 Subject: [PATCH 32/98] docs: added javadoc to controller classes --- .../app/control/DonationController.java | 16 +++++++++---- .../group5/app/control/LoginController.java | 23 +++++++++++++++++++ .../app/control/NavigationController.java | 3 +++ .../app/control/OrganizationController.java | 4 ++++ .../app/view/userpage/UserPageView.java | 2 +- 5 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 9118c7e..5c700b0 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -12,6 +12,10 @@ import java.util.Map; import java.util.Set; +/** + * A controller that handles the logic pertaining to + * making donations and getting data via {@link DonationService}. + */ public class DonationController { private final AppState appState; private final NavigationController nav; @@ -27,17 +31,21 @@ public Map getUserDonations(int userId) { return service.getDonationRepository().filterByUser(userId); } - public Set getUniqueOrgs() { + public Set getUniqueOrganizations() { Map userDonations = getUserDonations(appState.getCurrentUser().getUserId()); - Set uniqueOrgs = new HashSet<>(); + Set uniqueOrganizations = new HashSet<>(); for (Donation donation : userDonations.values()) { - uniqueOrgs.add(donation.organizationId()); + uniqueOrganizations.add(donation.organizationId()); } - return uniqueOrgs; + return uniqueOrganizations; } + /** + * Handles the creation of a new donation via {@link DonationService} + * and {@link AppState}. + */ public void handleDonate() { // Get session data from MainController User currentUser = appState.getCurrentUser(); diff --git a/src/main/java/edu/group5/app/control/LoginController.java b/src/main/java/edu/group5/app/control/LoginController.java index cdd5b5f..d9cb35e 100644 --- a/src/main/java/edu/group5/app/control/LoginController.java +++ b/src/main/java/edu/group5/app/control/LoginController.java @@ -7,6 +7,10 @@ import edu.group5.app.view.loginpage.SignInPageView; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +/** + * A controller that handles the logic pertaining to {@link User} + * sign in, login and logout via {@link UserService}. + */ public class LoginController { private final AppState appState; private final NavigationController nav; @@ -18,6 +22,15 @@ public LoginController(AppState appState, NavigationController nav, UserService this.userService = userService; } + /** + * Handles the sign in of a new {@link User}. + * + * @param view view connected to the sign in. + * @param firstName first name of the {@link User}. + * @param lastName last name of the {@link User}. + * @param email email of the {@link User}. + * @param passwordChars password of the {@link User}. + */ public void handleSignIn(SignInPageView view, String firstName, String lastName, String email, char[] passwordChars) { if (firstName == null || firstName.trim().isEmpty() || lastName == null || lastName.trim().isEmpty() || @@ -44,6 +57,13 @@ public void handleSignIn(SignInPageView view, String firstName, String lastName, } } + /** + * Handles the login of a {@link User}. + * + * @param view view connected to the login. + * @param email email of the {@link User}. + * @param passwordChars password of the {@link User}. + */ public void handleLogin(LoginPageView view, String email, char[] passwordChars) { if (email == null || email.trim().isEmpty() || passwordChars == null || passwordChars.length == 0) { view.showError("Email and password are required"); @@ -60,6 +80,9 @@ public void handleLogin(LoginPageView view, String email, char[] passwordChars) } } + /** + * Handles the logout of a {@link User}. + */ public void handleLogout() { appState.setCurrentUser(null); appState.setCurrentOrganization(null); diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index ddab7e2..db16c95 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -16,6 +16,9 @@ import edu.group5.app.view.userpage.UserPageView; import javafx.scene.layout.BorderPane; +/** + * A controller that handles the switching of views in the root node. + */ public class NavigationController { private final BorderPane root; private final Header header; diff --git a/src/main/java/edu/group5/app/control/OrganizationController.java b/src/main/java/edu/group5/app/control/OrganizationController.java index 499b7c9..62a787c 100644 --- a/src/main/java/edu/group5/app/control/OrganizationController.java +++ b/src/main/java/edu/group5/app/control/OrganizationController.java @@ -7,6 +7,10 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +/** + * A controller that handles the data being passed to + * the view via the {@link OrganizationService}. + */ public class OrganizationController { private final AppState appState; private final NavigationController nav; diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index b410761..d416916 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -85,7 +85,7 @@ private VBox createCausesSection() { User currentUser = appState.getCurrentUser(); - Set uniqueOrgs = donationController.getUniqueOrgs(); + Set uniqueOrgs = donationController.getUniqueOrganizations(); if (uniqueOrgs.isEmpty()) { Label noCauses = new Label("No causes supported yet"); From fe434610cb84f684fe52e85faf4e3fb5509b3168 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 9 Apr 2026 10:39:10 +0200 Subject: [PATCH 33/98] update[CausesPage]: Update CausesPage to have search bare fixed at the top ensuring user friendly UX --- .../app/view/causespage/CausesPageView.java | 13 +++--- src/main/resources/loginpage/login.css | 42 ++++++++++++------- src/main/resources/loginpage/signin.css | 42 ++++++++++++------- 3 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 36c056f..a2f3374 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -32,7 +32,10 @@ public CausesPageView(AppState appState, NavigationController nav, OrganizationC setCenter(createBody()); } - private ScrollPane createBody() { + private BorderPane createBody() { + BorderPane bodyRoot = new BorderPane(); + bodyRoot.setTop(createSearchSection()); + ScrollPane body = new ScrollPane(); body.setId("body"); body.setFitToWidth(true); @@ -47,11 +50,9 @@ private ScrollPane createBody() { // Load organizations INSTANTLY from cache allOrganizations = orgController.getTrustedOrgs(); - vBox.getChildren().addAll( - createSearchSection(), - createOrganizationSection(null) - ); + vBox.getChildren().add(createOrganizationSection(null)); body.setContent(vBox); + bodyRoot.setCenter(body); // Build a map of org ID -> card for quick lookup Map cardMap = new HashMap<>(); @@ -78,7 +79,7 @@ private ScrollPane createBody() { }); }); - return body; + return bodyRoot; } private HBox createSearchSection() { diff --git a/src/main/resources/loginpage/login.css b/src/main/resources/loginpage/login.css index dc758a1..f0ffc41 100644 --- a/src/main/resources/loginpage/login.css +++ b/src/main/resources/loginpage/login.css @@ -1,25 +1,35 @@ #image-section { - -fx-background-image: url("/loginpage/login-image.jpg"); - -fx-background-size: 200%; - -fx-background-position: left center; - -fx-background-repeat: no-repeat; - -fx-pref-width: 50%; + -fx-background-image: url("/loginpage/login-image.jpg"); + -fx-background-size: 200%; + -fx-background-position: left center; + -fx-background-repeat: no-repeat; + -fx-pref-width: 50%; } + #login-btn { - -fx-background-color: #000000; - -fx-text-fill: white; - -fx-pref-height: 35px; + -fx-background-color: #000000; + -fx-text-fill: white; + -fx-pref-height: 35px; } #register-btn { - -fx-background-color: #000000; - -fx-text-fill: white; - -fx-pref-height: 35px; + -fx-background-color: #000000; + -fx-text-fill: white; + -fx-pref-height: 35px; } + #login-box { - -fx-border-color: #ccc; - -fx-border-radius: 8px; - -fx-border-width: 1px; - -fx-padding: 24px; - -fx-max-width: 340px; + -fx-border-color: #ccc; + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-padding: 24px; + -fx-max-width: 340px; +} + +#login-btn:hover { + -fx-cursor: hand; +} + +#register-btn:hover { + -fx-cursor: hand; } \ No newline at end of file diff --git a/src/main/resources/loginpage/signin.css b/src/main/resources/loginpage/signin.css index 4ab0276..d14af06 100644 --- a/src/main/resources/loginpage/signin.css +++ b/src/main/resources/loginpage/signin.css @@ -1,25 +1,35 @@ #image-section { - -fx-background-image: url("/loginpage/signin-image.png"); - -fx-background-size: auto; - -fx-background-position: right center; - -fx-background-repeat: no-repeat; - -fx-pref-width: 50%; + -fx-background-image: url("/loginpage/signin-image.png"); + -fx-background-size: auto; + -fx-background-position: right center; + -fx-background-repeat: no-repeat; + -fx-pref-width: 50%; } + #login-btn { - -fx-background-color: #000000; - -fx-text-fill: white; - -fx-pref-height: 35px; + -fx-background-color: #000000; + -fx-text-fill: white; + -fx-pref-height: 35px; } #register-btn { - -fx-background-color: #000000; - -fx-text-fill: white; - -fx-pref-height: 35px; + -fx-background-color: #000000; + -fx-text-fill: white; + -fx-pref-height: 35px; } + #login-box { - -fx-border-color: #ccc; - -fx-border-radius: 8px; - -fx-border-width: 1px; - -fx-padding: 24px; - -fx-max-width: 340px; + -fx-border-color: #ccc; + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-padding: 24px; + -fx-max-width: 340px; +} + +#login-btn:hover { + -fx-cursor: hand; +} + +#register-btn:hover { + -fx-cursor: hand; } \ No newline at end of file From 1960fced52e4bde825bc224758ae3ad86afddebd Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:44:59 +0200 Subject: [PATCH 34/98] style[DbWrapper]: add javadocs and split lines --- .../group5/app/model/wrapper/DbWrapper.java | 87 +++++++++++++++++-- 1 file changed, 82 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java index 073fd81..2357e87 100644 --- a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -15,6 +15,9 @@ import java.util.logging.Level; import java.util.logging.Logger; +/** + * A class for wrapping the database. + */ public class DbWrapper { protected Connection connection; private static final String CONNECTION_TYPE = "jdbc:h2:"; @@ -24,6 +27,11 @@ public class DbWrapper { private List donations; private Logger logger = Logger.getLogger(DbWrapper.class.getName()); + /** + * The constructor, which constructs a String for connecting to the database. + * + * @param test Whether to construct the connection String for testing (in-memory) or not. + */ public DbWrapper(boolean test) { if (test) { this.connectionString = CONNECTION_TYPE + "mem:test;" + DB_SCRIPT + "test_init.sql'"; @@ -33,6 +41,11 @@ public DbWrapper(boolean test) { this.logger.info("connectionString constructed"); } + /** + * Connects to the database, and returns the result, logging failures. + * + * @return True if successful, false if not. + */ public boolean connect() { try { this.connection = DriverManager.getConnection(this.connectionString); @@ -49,7 +62,13 @@ public boolean connect() { } } + /** + * Disconnects the database connection, logging failures. + * + * @return True if successful, false if not. + */ public boolean disconnect() { + // We are not interested in whether it fails to close, as we check its closed status later. try{ this.connection.close(); } catch (Exception e) {}; try { return this.connection.isClosed(); @@ -59,12 +78,25 @@ public boolean disconnect() { } } + /** + * Closes queries and results. + * + * @param results The ResultSet to close, can be null. + * @param ps The PreparedStatement to close, can be null. + */ private void close(ResultSet results, PreparedStatement ps) { + // This method can take null arguments, so an exception is expected. try { results.close(); } catch (Exception e) {} try { ps.close(); } catch (Exception e) {} this.logger.info("results and ps closed"); } + /** + * Gets all users from the database. + * + * @return The users from the database returned as a List of Object arrays, where each Object + * array represents a user and a row in the users table in the database. + */ public List importUsers() { PreparedStatement ps = null; ResultSet results = null; @@ -93,7 +125,17 @@ public List importUsers() { return this.users; } - public int exportUsers(List data) { + /** + * Puts new users into the database. + * + * @param data The new users to put into the database. Each Object array in the List is a new + * user to add as a row. + * @return The number of rows affected in the transaction. + * @throws IllegalArgumentException This exception is thrown when data is null, its rows are not + * of length 6, any of the rows are null, any of the rows are duplicates or existing rows in + * the database, or any of the values in the rows can't be cast to the correct data-types. + */ + public int exportUsers(List data) throws IllegalArgumentException { this.importUsers(); if (data == null) { @@ -102,12 +144,14 @@ public int exportUsers(List data) { if (data.isEmpty()) { return 0; } + // TODO: change to check length for every row. if (data.get(0).length != 6) { throw new IllegalArgumentException("data's arrays must have a length of 6"); } if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { throw new IllegalArgumentException("One or more rows in data contains null values"); } + // TODO: change how existing rows are checked. if (this.users.size() > 0) { if ((int) data.getLast()[0] <= (int) this.users.getLast()[0]) { throw new IllegalArgumentException("data can't contain existing rows"); @@ -123,7 +167,13 @@ public int exportUsers(List data) { int rowsAffected = 0; try { ps = this.connection.prepareStatement( - "INSERT INTO users (user_id, role, first_name, last_name, email, password_hash) VALUES (?, ?, ?, ?, ?, ?)"); + """ + INSERT INTO users + (user_id, role, first_name, last_name, email, password_hash) + VALUES + (?, ?, ?, ?, ?, ?) + """ + ); for (Object[] row : data) { try { ps.setInt(1, (int) row[0]); @@ -144,10 +194,21 @@ public int exportUsers(List data) { return rowsAffected; } + /** + * Imports all donations. + * + * @return A List of Object arrays for each donation in the database. + */ public List fetchAllDonations() { return this.importDonations(0, true); } + /** + * Imports the donations of a specific user based on a given user_id. + * + * @param user_id The id of the user to get the donations of. + * @return A List of Object arrays for each donation in the database. + */ public List importDonations(int user_id) { return this.importDonations(user_id, false); } @@ -155,7 +216,7 @@ public List importDonations(int user_id) { private List importDonations(int user_id, boolean all) { PreparedStatement ps = null; ResultSet results = null; - try{ + try { if (all) { ps = this.connection.prepareStatement("SELECT * FROM donations"); } else { @@ -185,7 +246,17 @@ private List importDonations(int user_id, boolean all) { return this.donations; } - public int exportDonations(List data) { + /** + * Puts new donations into the database. + * + * @param data The new donation to put into the database. Each Object array in the List is a new + * donations to add as a row. + * @return The number of rows affected in the transaction. + * @throws IllegalArgumentException This exception is thrown when data is null, its rows are not + * of length 6, any of the rows are null, any of the rows are duplicates or existing rows in + * the database, or any of the values in the rows can't be cast to the correct data-types. + */ + public int exportDonations(List data) throws IllegalArgumentException { this.fetchAllDonations(); if (data == null) { @@ -212,7 +283,13 @@ public int exportDonations(List data) { int rowsAffected = 0; try { ps = this.connection.prepareStatement( - "INSERT INTO donations (donation_id, user_id, organization_id, amount, dating, payment_method) VALUES (?, (SELECT user_id FROM users WHERE user_id = ?), ?, ?, ?, ?)"); + """ + INSERT INTO donations + (donation_id, user_id, organization_id, amount, dating, payment_method) + VALUES + (?, (SELECT user_id FROM users WHERE user_id = ?), ?, ?, ?, ?) + """ + ); for (Object[] row : data) { try { for (int i = 0; i < 3; i++) { From bdd4854b66e2513da5a85db9c4bd66f1499c5297 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:08:13 +0200 Subject: [PATCH 35/98] style[DbWrapper]: add whitespaces after try --- src/main/java/edu/group5/app/model/wrapper/DbWrapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java index 2357e87..f453cc3 100644 --- a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -69,7 +69,7 @@ public boolean connect() { */ public boolean disconnect() { // We are not interested in whether it fails to close, as we check its closed status later. - try{ this.connection.close(); } catch (Exception e) {}; + try { this.connection.close(); } catch (Exception e) {}; try { return this.connection.isClosed(); } catch (Exception e) { @@ -100,7 +100,7 @@ private void close(ResultSet results, PreparedStatement ps) { public List importUsers() { PreparedStatement ps = null; ResultSet results = null; - try{ + try { ps = this.connection.prepareStatement("SELECT * FROM users"); results = ps.executeQuery(); List data = new ArrayList(); From c3b05558c529e463293699a442933407ff7ee316 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:20:20 +0200 Subject: [PATCH 36/98] style[ParameterValidator]: make code conform to google checks --- .../group5/app/utils/ParameterValidator.java | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/main/java/edu/group5/app/utils/ParameterValidator.java b/src/main/java/edu/group5/app/utils/ParameterValidator.java index 1a44e46..996bf6e 100644 --- a/src/main/java/edu/group5/app/utils/ParameterValidator.java +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -1,21 +1,24 @@ package edu.group5.app.utils; import java.math.BigDecimal; + /** - * ParameterValidator is a utility class that provides static methods for validating various types of parameters. - * It includes methods for checking strings, integers, objects, and BigDecimal values to ensure they meet specific - * criteria such as not being null, not being blank, or being positive. - * + * ParameterValidator is a utility class that provides static methods for validating various types + * of parameters. + * It includes methods for checking strings, integers, objects, and BigDecimal values to ensure + * they meet specific criteria such as not being null, not being blank, or being positive. */ public final class ParameterValidator { /** * Validates that a string parameter is not null and not blank. + * * @param stringArg the string parameter to validate * @param variableName the name of the variable being validated, used in exception messages * @throws IllegalArgumentException if the string is null or blank */ - public static final void stringChecker(String stringArg, String variableName) throws IllegalArgumentException { + public static final void stringChecker(String stringArg, String variableName) + throws IllegalArgumentException { nullCheck(stringArg, variableName); if (stringArg.isBlank()) { throw new IllegalArgumentException(String.format("%s can't be blank", variableName)); @@ -24,33 +27,41 @@ public static final void stringChecker(String stringArg, String variableName) th /** * Validates that an integer parameter is not null and is a positive integer. + * * @param intArg the integer parameter to validate * @param variableName the name of the variable being validated, used in exception messages * @throws IllegalArgumentException if the integer is null or not a positive integer */ - public static final void intChecker(int intArg, String variableName) throws IllegalArgumentException { + public static final void intChecker(int intArg, String variableName) + throws IllegalArgumentException { if (intArg <= 0) { - throw new IllegalArgumentException(String.format("%s must be a positive integer", variableName)); + throw new IllegalArgumentException( + String.format("%s must be a positive integer", variableName) + ); } } /** * Validates that an object parameter is not null. + * * @param objectArg the object parameter to validate * @param variableName the name of the variable being validated, used in exception messages * @throws IllegalArgumentException if the object is null */ - public static final void objectChecker(Object objectArg, String variableName) throws IllegalArgumentException { + public static final void objectChecker(Object objectArg, String variableName) + throws IllegalArgumentException { nullCheck(objectArg, variableName); } /** * Validates that a BigDecimal parameter is not null and is greater than zero. + * * @param bigDecimalArg the BigDecimal parameter to validate * @param variableName the name of the variable being validated, used in exception messages * @throws IllegalArgumentException if the BigDecimal is null or not greater than zero */ - public static final void bigDecimalChecker(BigDecimal bigDecimalArg, String variableName) throws IllegalArgumentException { + public static final void bigDecimalChecker(BigDecimal bigDecimalArg, String variableName) + throws IllegalArgumentException { nullCheck(bigDecimalArg, variableName); if (bigDecimalArg.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException(String.format("%s must be larger than 0", variableName)); @@ -58,12 +69,15 @@ public static final void bigDecimalChecker(BigDecimal bigDecimalArg, String vari } /** - * Helper method to check if a variable is null and throw an IllegalArgumentException with a formatted message if it is. + * Helper method to check if a variable is null and throw an IllegalArgumentException with a + * formatted message if it is. + * * @param variable the variable to check for null * @param variableName the name of the variable being checked, used in the exception message * @throws IllegalArgumentException if the variable is null */ - private static final void nullCheck(Object variable, String variableName) throws IllegalArgumentException { + private static final void nullCheck(Object variable, String variableName) + throws IllegalArgumentException { if (variable == null) { throw new IllegalArgumentException(String.format("%s can't be null", variableName)); } From e1cda6f1a4da7299e06254e3ce5f572182814001 Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Thu, 9 Apr 2026 11:24:42 +0200 Subject: [PATCH 37/98] feat: added javadoc to some pages, and changed signin to signup --- .../group5/app/control/LoginController.java | 4 +- .../app/control/NavigationController.java | 6 +-- .../view/donationpage/DonationPageView.java | 8 ++++ .../donationpage/PaymentCompletePageView.java | 7 +++ .../app/view/homepage/HomePageView.java | 8 ++++ .../app/view/loginpage/LoginHeader.java | 4 ++ .../app/view/loginpage/LoginPageView.java | 12 ++++- ...ignInPageView.java => SignUpPageView.java} | 42 +++++++++++------- .../{signin-image.png => signup-image.png} | Bin .../loginpage/{signin.css => signup.css} | 2 +- 10 files changed, 68 insertions(+), 25 deletions(-) rename src/main/java/edu/group5/app/view/loginpage/{SignInPageView.java => SignUpPageView.java} (75%) rename src/main/resources/loginpage/{signin-image.png => signup-image.png} (100%) rename src/main/resources/loginpage/{signin.css => signup.css} (89%) diff --git a/src/main/java/edu/group5/app/control/LoginController.java b/src/main/java/edu/group5/app/control/LoginController.java index cdd5b5f..649a1c4 100644 --- a/src/main/java/edu/group5/app/control/LoginController.java +++ b/src/main/java/edu/group5/app/control/LoginController.java @@ -4,7 +4,7 @@ import edu.group5.app.model.user.User; import edu.group5.app.model.user.UserService; import edu.group5.app.view.loginpage.LoginPageView; -import edu.group5.app.view.loginpage.SignInPageView; +import edu.group5.app.view.loginpage.SignUpPageView; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class LoginController { @@ -18,7 +18,7 @@ public LoginController(AppState appState, NavigationController nav, UserService this.userService = userService; } - public void handleSignIn(SignInPageView view, String firstName, String lastName, String email, char[] passwordChars) { + public void handleSignUp(SignUpPageView view, String firstName, String lastName, String email, char[] passwordChars) { if (firstName == null || firstName.trim().isEmpty() || lastName == null || lastName.trim().isEmpty() || email == null || email.trim().isEmpty() || diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index ddab7e2..1b794d5 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -11,7 +11,7 @@ import edu.group5.app.view.homepage.HomePageView; import edu.group5.app.view.loginpage.LoginHeader; import edu.group5.app.view.loginpage.LoginPageView; -import edu.group5.app.view.loginpage.SignInPageView; +import edu.group5.app.view.loginpage.SignUpPageView; import edu.group5.app.view.organizationpage.OrganizationPageView; import edu.group5.app.view.userpage.UserPageView; import javafx.scene.layout.BorderPane; @@ -49,9 +49,9 @@ public void showLoginPage() { root.setCenter(new LoginPageView(appState, this, loginController)); } - public void showSignInPage() { + public void showSignUpPage() { root.setTop(loginHeader); - root.setCenter(new SignInPageView(appState, this, loginController)); + root.setCenter(new SignUpPageView(appState, this, loginController)); } public void showPaymentCompletePage() { diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 63d0493..f9681fc 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -18,6 +18,14 @@ import java.math.BigDecimal; import java.util.*; +/** + * A view for the Donation Page. + * In the donation page a user can donate a chosen amount + * to the organization they have chosen to donate to. + * + *

    The donation page consists of payment amount buttons, + * payment method buttons, donation button, and a back to organization page button.

    + */ public class DonationPageView extends BorderPane { private final AppState appState; private final NavigationController nav; diff --git a/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java b/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java index fcefe97..e20cfee 100644 --- a/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java @@ -11,6 +11,13 @@ import java.util.Objects; +/** + * A view for the payment complete page. + * When a user have donated an amount, this page opens up. + * + *

    The page consist of an image that says "Tank You For The Donation", + * and a "back to home" button at the bottom center.

    + */ public class PaymentCompletePageView extends BorderPane { private final NavigationController nav; diff --git a/src/main/java/edu/group5/app/view/homepage/HomePageView.java b/src/main/java/edu/group5/app/view/homepage/HomePageView.java index b40299a..c24b89b 100644 --- a/src/main/java/edu/group5/app/view/homepage/HomePageView.java +++ b/src/main/java/edu/group5/app/view/homepage/HomePageView.java @@ -10,6 +10,14 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; +/** + * A view for the homepage. + * In the home page a user can navigate to pages in the heading, + * and they can press the "donate to a cause" button or the "about us" button. + * + *

    The homepage includes a heading, a "donate to a cause" button, + * and an about us button. The page also has a charity image at the bottom.

    + */ public class HomePageView extends BorderPane { private final AppState appState; private final NavigationController nav; diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java index ad6a412..b5f4296 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java @@ -6,6 +6,10 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; +/** + * A header for the login page and for the SignIn page. + *

    The header includes a logo of the Help Me Help app.

    + */ public class LoginHeader extends BorderPane { public LoginHeader() { diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java index af972e0..3fb9bb7 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java @@ -12,6 +12,14 @@ import java.util.Objects; +/** + * A view for the login page. + * A user can login with email and password. + * If the user does not have an account they can + * press the register button to the SignUp page. + *

    This page involves a {@code LoginHeader}, an image at the right, + * a login box, an email box, a login button, and a register button.

    + */ public class LoginPageView extends BorderPane { private final AppState appState; private final NavigationController nav; @@ -106,9 +114,9 @@ private Button getLoginBtn() { } public Button getRegisterBtn() { - Button registerBtn = new Button("Don't have an account? Sign In"); + Button registerBtn = new Button("Don't have an account? Sign Up"); registerBtn.setMaxWidth(300); - registerBtn.setOnMouseClicked(e -> nav.showSignInPage()); + registerBtn.setOnMouseClicked(e -> nav.showSignUpPage()); registerBtn.setId("register-btn"); return registerBtn; } diff --git a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java b/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java similarity index 75% rename from src/main/java/edu/group5/app/view/loginpage/SignInPageView.java rename to src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java index 946dae4..160f929 100644 --- a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java @@ -12,8 +12,16 @@ import java.util.Objects; - -public class SignInPageView extends BorderPane { +/** + * A view for the SignUp page. + * In this page a user can create an account by writing in first and last name, + * and by adding email and password. If the user already have an account, + * they can press the back to login button to login. + * + *

    This view contains a first name field, a surname field, a email field, + * a password field, a sign up button, and an back to login button.

    + */ +public class SignUpPageView extends BorderPane { private final AppState appState; private final NavigationController nav; private final LoginController loginController; @@ -24,7 +32,7 @@ public class SignInPageView extends BorderPane { private PasswordField passwordField; private Label errorLabel; - public SignInPageView(AppState appState, NavigationController nav, LoginController loginController) { + public SignUpPageView(AppState appState, NavigationController nav, LoginController loginController) { this.appState = appState; this.nav = nav; this.loginController = loginController; @@ -36,7 +44,7 @@ public SignInPageView(AppState appState, NavigationController nav, LoginControll content.getChildren().addAll(getOuterSection(), getImageSection()); String css = Objects.requireNonNull( - getClass().getResource("/loginpage/signin.css")).toExternalForm(); + getClass().getResource("/loginpage/signup.css")).toExternalForm(); content.getStylesheets().add(css); setCenter(content); @@ -68,16 +76,16 @@ private VBox getOuterSection() { VBox outerSection = new VBox(12); outerSection.setAlignment(Pos.CENTER); HBox.setHgrow(outerSection, Priority.ALWAYS); - outerSection.getChildren().addAll(getSignInBox(), getBackToLoginBtn()); + outerSection.getChildren().addAll(getSignUpBox(), getBackToLoginBtn()); return outerSection; } - private VBox getSignInBox() { - VBox signInSection = new VBox(12); - signInSection.setAlignment(Pos.CENTER); - signInSection.setId("login-box"); - signInSection.getChildren().addAll(getErrorLabel(), getNameRow(), getEmailBox(), getPasswordBox(), getSignInBtn()); - return signInSection; + private VBox getSignUpBox() { + VBox signUpSection = new VBox(12); + signUpSection.setAlignment(Pos.CENTER); + signUpSection.setId("login-box"); + signUpSection.getChildren().addAll(getErrorLabel(), getNameRow(), getEmailBox(), getPasswordBox(), getSignUpBtn()); + return signUpSection; } private Label getErrorLabel() { @@ -126,18 +134,18 @@ private VBox getPasswordBox() { return passwordBox; } - private Button getSignInBtn() { - Button signInBtn = new Button("Sign In"); - signInBtn.setMaxWidth(300); - signInBtn.setId("login-btn"); - signInBtn.setOnMouseClicked(e -> loginController.handleSignIn( + private Button getSignUpBtn() { + Button signUpBtn = new Button("Sign Up"); + signUpBtn.setMaxWidth(300); + signUpBtn.setId("login-btn"); + signUpBtn.setOnMouseClicked(e -> loginController.handleSignUp( this, getFirstName(), getLastName(), getEmail(), getPassword() )); - return signInBtn; + return signUpBtn; } public Button getBackToLoginBtn() { diff --git a/src/main/resources/loginpage/signin-image.png b/src/main/resources/loginpage/signup-image.png similarity index 100% rename from src/main/resources/loginpage/signin-image.png rename to src/main/resources/loginpage/signup-image.png diff --git a/src/main/resources/loginpage/signin.css b/src/main/resources/loginpage/signup.css similarity index 89% rename from src/main/resources/loginpage/signin.css rename to src/main/resources/loginpage/signup.css index 4ab0276..93ad133 100644 --- a/src/main/resources/loginpage/signin.css +++ b/src/main/resources/loginpage/signup.css @@ -1,5 +1,5 @@ #image-section { - -fx-background-image: url("/loginpage/signin-image.png"); + -fx-background-image: url("/loginpage/signup-image.png"); -fx-background-size: auto; -fx-background-position: right center; -fx-background-repeat: no-repeat; From ab64cda1599c09cccfca940bae3bf9565feeac04 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:47:07 +0200 Subject: [PATCH 38/98] refactor[DbWrapper&ParameterValidator]: move parameter validation in exports to ParameterValidator class --- .../group5/app/model/wrapper/DbWrapper.java | 45 +++---------------- .../group5/app/utils/ParameterValidator.java | 43 ++++++++++++++++++ 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java index f453cc3..c61bbc8 100644 --- a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -1,5 +1,6 @@ package edu.group5.app.model.wrapper; +import java.lang.reflect.Parameter; import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; @@ -15,6 +16,8 @@ import java.util.logging.Level; import java.util.logging.Logger; +import edu.group5.app.utils.ParameterValidator; + /** * A class for wrapping the database. */ @@ -137,30 +140,11 @@ public List importUsers() { */ public int exportUsers(List data) throws IllegalArgumentException { this.importUsers(); - - if (data == null) { - throw new IllegalArgumentException("data can't be null"); - } + if (data.isEmpty()) { return 0; } - // TODO: change to check length for every row. - if (data.get(0).length != 6) { - throw new IllegalArgumentException("data's arrays must have a length of 6"); - } - if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { - throw new IllegalArgumentException("One or more rows in data contains null values"); - } - // TODO: change how existing rows are checked. - if (this.users.size() > 0) { - if ((int) data.getLast()[0] <= (int) this.users.getLast()[0]) { - throw new IllegalArgumentException("data can't contain existing rows"); - } - } - Set ids = new HashSet<>(); - if (data.stream().anyMatch(i -> !ids.add(i[0]))) { - throw new IllegalArgumentException("data can't contain duplicate rows"); - } + ParameterValidator.exportChecker(data, "data", this.users); PreparedStatement ps = null; @@ -258,26 +242,11 @@ private List importDonations(int user_id, boolean all) { */ public int exportDonations(List data) throws IllegalArgumentException { this.fetchAllDonations(); - - if (data == null) { - throw new IllegalArgumentException("data can't be null"); - } + if (data.isEmpty()) { return 0; } - if (data.get(0).length != 6) { - throw new IllegalArgumentException("data's arrays must have a length of 6"); - } - if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { - throw new IllegalArgumentException("One or more rows in data contains null values"); - } - if (this.donations.size() > 0 && (int) data.getLast()[0] <= (int) this.donations.getLast()[0]) { - throw new IllegalArgumentException("data can't contain existing rows"); - } - Set ids = new HashSet<>(); - if (data.stream().anyMatch(i -> !ids.add(i[0]))) { - throw new IllegalArgumentException("data can't contain duplicate rows"); - } + ParameterValidator.exportChecker(data, "data", this.donations); PreparedStatement ps = null; int rowsAffected = 0; diff --git a/src/main/java/edu/group5/app/utils/ParameterValidator.java b/src/main/java/edu/group5/app/utils/ParameterValidator.java index 996bf6e..36a0d14 100644 --- a/src/main/java/edu/group5/app/utils/ParameterValidator.java +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -1,6 +1,10 @@ package edu.group5.app.utils; import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; /** * ParameterValidator is a utility class that provides static methods for validating various types @@ -82,4 +86,43 @@ private static final void nullCheck(Object variable, String variableName) throw new IllegalArgumentException(String.format("%s can't be null", variableName)); } } + + /** + * A method for checking if the data to be exported is valid. + * + * @param data The data to export. + * @param dataName The name of the variable of the data to export. + * @param oldData The existing data to compare to for checking row existence. + * @throws IllegalArgumentException This exception is thrown when data is null, its rows are not + * of length 6, any of the rows are null, any of the rows are duplicates or existing rows in + * the database, or any of the values in the rows can't be cast to the correct data-types. + */ + public static final void exportChecker( + List data, String dataName, List oldData + ) throws IllegalArgumentException { + if (data == null) { + throw new IllegalArgumentException(String.format("%s can't be null", dataName)); + } + // TODO: change to check length for every row. + if (data.get(0).length != 6) { + throw new IllegalArgumentException( + String.format("%s's arrays must have a length of 6", dataName) + ); + } + if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { + throw new IllegalArgumentException( + String.format("One or more rows in %s contains null values", dataName) + ); + } + // TODO: change how existing rows are checked. + if (oldData.size() > 0) { + if ((int) data.getLast()[0] <= (int) oldData.getLast()[0]) { + throw new IllegalArgumentException("data can't contain existing rows"); + } + } + Set ids = new HashSet<>(); + if (data.stream().anyMatch(i -> !ids.add(i[0]))) { + throw new IllegalArgumentException("data can't contain duplicate rows"); + } + } } From 69ec2474c492b8eb60b51b8b1fb64621aad421d2 Mon Sep 17 00:00:00 2001 From: emilfa Date: Thu, 9 Apr 2026 11:45:56 +0200 Subject: [PATCH 39/98] docs: renamed LoginController to AuthController and updated javadoc in controller clases to be more descriptive --- ...ginController.java => AuthController.java} | 59 ++++++++++++++----- .../app/control/DonationController.java | 39 ++++++++++-- .../app/control/NavigationController.java | 14 ++--- .../app/control/OrganizationController.java | 14 ++--- .../app/view/causespage/CausesPageView.java | 2 +- .../app/view/loginpage/LoginPageView.java | 10 ++-- .../app/view/loginpage/SignInPageView.java | 10 ++-- .../app/view/userpage/UserPageView.java | 20 +++---- 8 files changed, 111 insertions(+), 57 deletions(-) rename src/main/java/edu/group5/app/control/{LoginController.java => AuthController.java} (54%) diff --git a/src/main/java/edu/group5/app/control/LoginController.java b/src/main/java/edu/group5/app/control/AuthController.java similarity index 54% rename from src/main/java/edu/group5/app/control/LoginController.java rename to src/main/java/edu/group5/app/control/AuthController.java index d9cb35e..7e22cba 100644 --- a/src/main/java/edu/group5/app/control/LoginController.java +++ b/src/main/java/edu/group5/app/control/AuthController.java @@ -8,30 +8,48 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** - * A controller that handles the logic pertaining to {@link User} - * sign in, login and logout via {@link UserService}. + * Controller responsible for authentication-related operations. + * + *

    Coordinates between {@link AppState}, {@link NavigationController} + * and {@link UserService} to: + *

      + *
    • Sign-up a new user
    • + *
    • Login in a user
    • + *
    • Logout a user
    • + *
    + *

    */ -public class LoginController { +public class AuthController { private final AppState appState; private final NavigationController nav; private final UserService userService; - public LoginController(AppState appState, NavigationController nav, UserService userService) { + public AuthController(AppState appState, NavigationController nav, UserService userService) { this.appState = appState; this.nav = nav; this.userService = userService; } /** - * Handles the sign in of a new {@link User}. + * Handles the registration of a {@link User}. * - * @param view view connected to the sign in. - * @param firstName first name of the {@link User}. - * @param lastName last name of the {@link User}. - * @param email email of the {@link User}. - * @param passwordChars password of the {@link User}. + *
      + *
    • Validates the firstname, lastname, email and password fields
    • + *
    • Encrypts the password
    • + *
    • Invokes {@link UserService#registerUser(String, String, String, String, String)} to register the user
    • + *
    + * + *

    If the registration is successful, the user is stored in {@link AppState} and + * the application navigates to the home page. Otherwise, an error message + * is displayed in the provided view.

    + * + * @param view the view used to display feedback to the user + * @param firstName the user's first name + * @param lastName the user's last name + * @param email the user's email + * @param passwordChars the user's password */ - public void handleSignIn(SignInPageView view, String firstName, String lastName, String email, char[] passwordChars) { + public void handleSignUp(SignInPageView view, String firstName, String lastName, String email, char[] passwordChars) { if (firstName == null || firstName.trim().isEmpty() || lastName == null || lastName.trim().isEmpty() || email == null || email.trim().isEmpty() || @@ -60,9 +78,18 @@ public void handleSignIn(SignInPageView view, String firstName, String lastName, /** * Handles the login of a {@link User}. * - * @param view view connected to the login. - * @param email email of the {@link User}. - * @param passwordChars password of the {@link User}. + *
      + *
    • Validates the email and password of the user
    • + *
    • Invokes {@link UserService#login(String, char[])} to login in the user
    • + *
    + * + * If the login is successful, the user is stored in {@link AppState} and the + * application navigates to the home page. Otherwise, an error message is + * displayed within the provided view. + * + * @param view the view used to display feedback to the user + * @param email the user's email + * @param passwordChars the user's password */ public void handleLogin(LoginPageView view, String email, char[] passwordChars) { if (email == null || email.trim().isEmpty() || passwordChars == null || passwordChars.length == 0) { @@ -82,11 +109,15 @@ public void handleLogin(LoginPageView view, String email, char[] passwordChars) /** * Handles the logout of a {@link User}. + * + *

    Clears states in {@link AppState} and the application + * navigates to the login page.

    */ public void handleLogout() { appState.setCurrentUser(null); appState.setCurrentOrganization(null); appState.setCurrentDonationAmount(null); + appState.setCurrentPaymentMethod(null); nav.showLoginPage(); } } diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 5c700b0..e6fc80f 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -13,8 +13,17 @@ import java.util.Set; /** - * A controller that handles the logic pertaining to - * making donations and getting data via {@link DonationService}. + * Controller responsible for donation-related operations. + * + *

    + * Coordinates between {@link AppState}, {@link DonationService} + * and {@link NavigationController} to: + *

      + *
    • Retrieve donation data for the current user
    • + *
    • Process new donations
    • + *
    • Handle navigation after donation completion
    • + *
    + *

    */ public class DonationController { private final AppState appState; @@ -27,11 +36,23 @@ public DonationController(AppState appState, NavigationController nav, DonationS this.service = service; } + /** + * Retrieves all donations made by a specific user. + * + * @param userId the ID of the user + * @return a map of donations. + */ public Map getUserDonations(int userId) { return service.getDonationRepository().filterByUser(userId); } - public Set getUniqueOrganizations() { + /** + * Returns a set of unique organization IDs that the current user + * has donated to. + * + * @return a set of organization IDs + */ + public Set getUniqueOrganizationIDs() { Map userDonations = getUserDonations(appState.getCurrentUser().getUserId()); Set uniqueOrganizations = new HashSet<>(); @@ -43,8 +64,16 @@ public Set getUniqueOrganizations() { } /** - * Handles the creation of a new donation via {@link DonationService} - * and {@link AppState}. + * Processes a donation using data stored in {@link AppState}. + * + *

    + *

      + *
    • Validates the current user, organization, amount and payment method
    • + *
    • Invokes {@link DonationService#donate(Customer, int, BigDecimal, String)} to create the donation
    • + *
    • Clears temporary donation state
    • + *
    • Navigates to the payment complete view
    • + *
    + *

    */ public void handleDonate() { // Get session data from MainController diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index db16c95..2d1e6ab 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -17,7 +17,7 @@ import javafx.scene.layout.BorderPane; /** - * A controller that handles the switching of views in the root node. + * Controller responsible for navigating between views within the root node. */ public class NavigationController { private final BorderPane root; @@ -26,7 +26,7 @@ public class NavigationController { private final AppState appState; - private final LoginController loginController; + private final AuthController authController; private final DonationController donationController; private final OrganizationController organizationController; @@ -37,9 +37,9 @@ public NavigationController(BorderPane root, AppState appState, UserService user this.appState = appState; - this.loginController = new LoginController(appState, this, userService); + this.authController = new AuthController(appState, this, userService); this.donationController = new DonationController(appState, this, donationService); - this.organizationController = new OrganizationController(appState, this, organizationService); + this.organizationController = new OrganizationController(organizationService); } public void showHomePage() { @@ -49,12 +49,12 @@ public void showHomePage() { public void showLoginPage() { root.setTop(loginHeader); - root.setCenter(new LoginPageView(appState, this, loginController)); + root.setCenter(new LoginPageView(appState, this, authController)); } public void showSignInPage() { root.setTop(loginHeader); - root.setCenter(new SignInPageView(appState, this, loginController)); + root.setCenter(new SignInPageView(appState, this, authController)); } public void showPaymentCompletePage() { @@ -83,6 +83,6 @@ public void showAboutUsPage() { public void showUserPage() { root.setTop(header); - root.setCenter(new UserPageView(appState, this, loginController, donationController, organizationController)); + root.setCenter(new UserPageView(appState, this, authController, donationController, organizationController)); } } diff --git a/src/main/java/edu/group5/app/control/OrganizationController.java b/src/main/java/edu/group5/app/control/OrganizationController.java index 62a787c..fb65127 100644 --- a/src/main/java/edu/group5/app/control/OrganizationController.java +++ b/src/main/java/edu/group5/app/control/OrganizationController.java @@ -1,6 +1,5 @@ package edu.group5.app.control; -import edu.group5.app.model.AppState; import edu.group5.app.model.organization.Organization; import edu.group5.app.model.organization.OrganizationService; @@ -8,25 +7,20 @@ import java.util.concurrent.CompletableFuture; /** - * A controller that handles the data being passed to - * the view via the {@link OrganizationService}. + * Controller responsible for organization-related operations. */ public class OrganizationController { - private final AppState appState; - private final NavigationController nav; private final OrganizationService service; - public OrganizationController(AppState appState, NavigationController nav, OrganizationService service) { - this.appState = appState; - this.nav = nav; + public OrganizationController(OrganizationService service) { this.service = service; } - public Organization getOrgById(int orgId) { + public Organization getOrganizationById(int orgId) { return service.findByOrgNumber(orgId); } - public Map getTrustedOrgs() { + public Map getTrustedOrganizations() { return service.getTrustedOrganizations(); } diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 1cd6e83..45d80c5 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -77,7 +77,7 @@ private GridPane createOrganizationSection(String searchTerm) { } if (allOrganizations == null) { - allOrganizations = orgController.getTrustedOrgs(); + allOrganizations = orgController.getTrustedOrganizations(); //Show loading text while organizations and logos are fetched grid.add(new javafx.scene.control.Label("Loading..."), 0, 0); diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java index af972e0..a6db821 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java @@ -1,7 +1,7 @@ package edu.group5.app.view.loginpage; import edu.group5.app.control.NavigationController; -import edu.group5.app.control.LoginController; +import edu.group5.app.control.AuthController; import edu.group5.app.model.AppState; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -15,16 +15,16 @@ public class LoginPageView extends BorderPane { private final AppState appState; private final NavigationController nav; - private final LoginController loginController; + private final AuthController authController; private TextField emailField; private PasswordField passwordField; private Label errorLabel; - public LoginPageView(AppState appState, NavigationController nav, LoginController loginController) { + public LoginPageView(AppState appState, NavigationController nav, AuthController authController) { this.appState = appState; this.nav = nav; - this.loginController = loginController; + this.authController = authController; HBox content = new HBox(); content.setFillHeight(true); @@ -97,7 +97,7 @@ private Button getLoginBtn() { Button loginBtn = new Button("Log In"); loginBtn.setMaxWidth(300); loginBtn.setId("login-btn"); - loginBtn.setOnMouseClicked(e -> loginController.handleLogin( + loginBtn.setOnMouseClicked(e -> authController.handleLogin( this, getEmail(), getPassword() diff --git a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java index 946dae4..d823746 100644 --- a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java @@ -1,7 +1,7 @@ package edu.group5.app.view.loginpage; import edu.group5.app.control.NavigationController; -import edu.group5.app.control.LoginController; +import edu.group5.app.control.AuthController; import edu.group5.app.model.AppState; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -16,7 +16,7 @@ public class SignInPageView extends BorderPane { private final AppState appState; private final NavigationController nav; - private final LoginController loginController; + private final AuthController authController; private TextField nameField; private TextField surnameField; @@ -24,10 +24,10 @@ public class SignInPageView extends BorderPane { private PasswordField passwordField; private Label errorLabel; - public SignInPageView(AppState appState, NavigationController nav, LoginController loginController) { + public SignInPageView(AppState appState, NavigationController nav, AuthController authController) { this.appState = appState; this.nav = nav; - this.loginController = loginController; + this.authController = authController; HBox content = new HBox(); content.setFillHeight(true); @@ -130,7 +130,7 @@ private Button getSignInBtn() { Button signInBtn = new Button("Sign In"); signInBtn.setMaxWidth(300); signInBtn.setId("login-btn"); - signInBtn.setOnMouseClicked(e -> loginController.handleSignIn( + signInBtn.setOnMouseClicked(e -> authController.handleSignUp( this, getFirstName(), getLastName(), diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index d416916..216603d 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -3,7 +3,7 @@ import edu.group5.app.control.DonationController; import edu.group5.app.control.NavigationController; import edu.group5.app.control.OrganizationController; -import edu.group5.app.control.LoginController; +import edu.group5.app.control.AuthController; import edu.group5.app.model.AppState; import edu.group5.app.model.donation.Donation; import edu.group5.app.model.organization.Organization; @@ -27,14 +27,14 @@ public class UserPageView extends BorderPane { private final AppState appState; private final NavigationController nav; - private final LoginController loginController; + private final AuthController authController; private final DonationController donationController; private final OrganizationController organizationController; - public UserPageView(AppState appState, NavigationController nav, LoginController loginController, DonationController donationController, OrganizationController organizationController) { + public UserPageView(AppState appState, NavigationController nav, AuthController authController, DonationController donationController, OrganizationController organizationController) { this.appState = appState; this.nav = nav; - this.loginController = loginController; + this.authController = authController; this.donationController = donationController; this.organizationController = organizationController; @@ -66,7 +66,7 @@ private HBox createProfileSection() { Button logoutBtn = new Button("Logout"); logoutBtn.getStyleClass().add("logout-button"); - logoutBtn.setOnAction(e -> loginController.handleLogout()); + logoutBtn.setOnAction(e -> authController.handleLogout()); VBox info = new VBox(10, name, email, location, logoutBtn); info.setAlignment(Pos.CENTER_LEFT); @@ -85,15 +85,15 @@ private VBox createCausesSection() { User currentUser = appState.getCurrentUser(); - Set uniqueOrgs = donationController.getUniqueOrganizations(); + Set uniqueOrganizations = donationController.getUniqueOrganizationIDs(); - if (uniqueOrgs.isEmpty()) { + if (uniqueOrganizations.isEmpty()) { Label noCauses = new Label("No causes supported yet"); noCauses.setStyle("-fx-text-fill: #999;"); causesBox.getChildren().add(noCauses); } else { - for (int orgId : uniqueOrgs) { - Organization org = organizationController.getOrgById(orgId); + for (int orgId : uniqueOrganizations) { + Organization org = organizationController.getOrganizationById(orgId); if (org != null) { Label causeLabel = new Label("β€’ " + org.name()); causesBox.getChildren().add(causeLabel); @@ -125,7 +125,7 @@ private VBox createDonationsSection() { donationsBox.getChildren().add(noDonations); } else { for (Donation donation : userDonations.values()) { - Organization org = organizationController.getOrgById(donation.organizationId()); + Organization org = organizationController.getOrganizationById(donation.organizationId()); String orgName = (org != null) ? org.name() : "Unknown Organization"; Label donationLabel = new Label( From 85d6951e2c4c6697864d7516fd3f56ee781684f4 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 9 Apr 2026 12:01:58 +0200 Subject: [PATCH 40/98] update[UserPage]: Update Userpage with more visual appealing Javafx display of user account --- .../app/view/userpage/UserPageView.java | 78 +++++++++++---- src/main/resources/userpage/userpage.css | 94 +++++++++++++++---- 2 files changed, 133 insertions(+), 39 deletions(-) diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index c5d886d..ee5d980 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -12,12 +12,16 @@ import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.text.Text; +import java.text.SimpleDateFormat; import java.util.*; @@ -78,41 +82,46 @@ private VBox createCausesSection() { Text title = new Text("YOUR SUPPORTED CAUSES"); title.getStyleClass().add("section-title"); - VBox causesBox = new VBox(10); - causesBox.getStyleClass().add("section-box"); - causesBox.setPadding(new Insets(10)); - - User currentUser = appState.getCurrentUser(); + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + scrollPane.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); + + FlowPane causesFlow = new FlowPane(10, 10); + causesFlow.setStyle("-fx-padding: 10;"); + causesFlow.getStyleClass().add("section-box"); Set uniqueOrgs = donationController.getUniqueOrgs(); if (uniqueOrgs.isEmpty()) { Label noCauses = new Label("No causes supported yet"); noCauses.setStyle("-fx-text-fill: #999;"); - causesBox.getChildren().add(noCauses); + causesFlow.getChildren().add(noCauses); } else { for (int orgId : uniqueOrgs) { Organization org = organizationController.getOrgById(orgId); if (org != null) { - Label causeLabel = new Label("β€’ " + org.name()); - causesBox.getChildren().add(causeLabel); + causesFlow.getChildren().add(createCauseChip(org)); // SRP: delegate to helper } } } - return new VBox(10, title, causesBox); + scrollPane.setContent(causesFlow); + return new VBox(10, title, scrollPane); } private VBox createDonationsSection() { Text title = new Text("PREVIOUS DONATIONS"); title.getStyleClass().add("section-title"); - VBox donationsBox = new VBox(10); - donationsBox.getStyleClass().add("section-box"); + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + scrollPane.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); + + VBox donationsBox = new VBox(12); + donationsBox.getStyleClass().add("donation-list"); donationsBox.setPadding(new Insets(10)); User currentUser = appState.getCurrentUser(); - Map userDonations = donationController.getUserDonations(currentUser.getUserId()); if (userDonations.isEmpty()) { @@ -121,17 +130,46 @@ private VBox createDonationsSection() { donationsBox.getChildren().add(noDonations); } else { for (Donation donation : userDonations.values()) { - Organization org = organizationController.getOrgById(donation.organizationId()); - String orgName = (org != null) ? org.name() : "Unknown Organization"; - - Label donationLabel = new Label( - orgName + " β€’ " + donation.amount() + " kr" + " β€’ " + donation.date() - ); - donationsBox.getChildren().add(donationLabel); + donationsBox.getChildren().add(createDonationCard(donation)); // SRP: delegate to helper } } - return new VBox(10, title, donationsBox); + scrollPane.setContent(donationsBox); + return new VBox(10, title, scrollPane); } + + private Label createCauseChip(Organization org) { + Label chip = new Label(org.name()); + chip.getStyleClass().add("cause-chip"); + return chip; + } + + private HBox createDonationCard(Donation donation) { + Organization org = organizationController.getOrgById(donation.organizationId()); + String orgName = (org != null) ? org.name() : "Unknown Organization"; + + HBox card = new HBox(20); + card.getStyleClass().add("donation-card"); + card.setPadding(new Insets(12, 15, 12, 15)); + card.setAlignment(Pos.CENTER_LEFT); + + Text orgText = new Text(orgName); + orgText.getStyleClass().add("donation-org-name"); + + VBox details = new VBox(4); + details.setAlignment(Pos.CENTER_RIGHT); + Label amountLabel = + new Label(String.format("%.2f", donation.amount()) + " kr"); + amountLabel.getStyleClass().add("donation-amount"); + Label dateLabel = new Label( + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(donation.date())); + dateLabel.getStyleClass().add("donation-date"); + details.getChildren().addAll(amountLabel, dateLabel); + + card.getChildren().addAll(orgText, details); + HBox.setHgrow(orgText, Priority.ALWAYS); // Org name takes available space + + return card; + } } diff --git a/src/main/resources/userpage/userpage.css b/src/main/resources/userpage/userpage.css index 8401a77..5037280 100644 --- a/src/main/resources/userpage/userpage.css +++ b/src/main/resources/userpage/userpage.css @@ -1,31 +1,87 @@ #profile-name { - -fx-font-size: 28px; - -fx-font-weight: bold; + -fx-font-size: 28px; + -fx-font-weight: bold; } + .profile-info { - -fx-font-size: 16px; - -fx-text-fill: #444; + -fx-font-size: 16px; + -fx-text-fill: #444; } + .section-title { - -fx-font-size: 14px; - -fx-font-weight: bold; - -fx-fill: #888; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-fill: #888; } + .section-box { - -fx-background-color: #ddd; - -fx-pref-height: 120px; - -fx-pref-width: 700px; - -fx-background-radius: 6; + -fx-background-color: #ddd; + -fx-pref-height: 200px; + -fx-pref-width: 700px; + -fx-background-radius: 6; } + .logout-button { - -fx-background-color: #e03030; - -fx-text-fill: white; - -fx-font-size: 14px; - -fx-font-weight: bold; - -fx-padding: 8 20; - -fx-background-radius: 4; - -fx-cursor: hand; + -fx-background-color: #e03030; + -fx-text-fill: white; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-padding: 8 20; + -fx-background-radius: 4; + -fx-cursor: hand; } + .logout-button:hover { - -fx-background-color: #c02020; + -fx-background-color: #c02020; +} + +.cause-chip { + -fx-background-color: #4CAF50; + -fx-text-fill: white; + -fx-padding: 6 12; + -fx-background-radius: 20; + -fx-font-size: 13px; +} + +.cause-chip:hover { + -fx-background-color: #45a049; + -fx-cursor: hand; +} + +.donation-card { + -fx-background-color: #f5f5f5; + -fx-border-color: #ddd; + -fx-border-radius: 6; + -fx-background-radius: 6; +} + +.donation-card:hover { + -fx-background-color: #efefef; + -fx-border-color: #bbb; +} + +.donation-org-name { + -fx-font-size: 15px; + -fx-font-weight: bold; + -fx-fill: #333; +} + +.donation-amount { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: #2196F3; +} + +.donation-date { + -fx-font-size: 12px; + -fx-text-fill: #999; +} + +.donation-list { + -fx-pref-height: 400px; +} + +/* ScrollPane styling */ +.scroll-pane { + -fx-control-inner-background: #fafafa; } \ No newline at end of file From 2f72f759b3a656979fcecf969a81ac23f2ee3145 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:42:28 +0200 Subject: [PATCH 41/98] refactor[OrgApiWrapper]: put off parameter validation to ParameterValidator --- .../java/edu/group5/app/model/wrapper/OrgApiWrapper.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java b/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java index 910b110..2a493e4 100644 --- a/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java @@ -1,5 +1,6 @@ package edu.group5.app.model.wrapper; +import edu.group5.app.utils.ParameterValidator; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -24,11 +25,7 @@ public class OrgApiWrapper extends Wrapper { * @param urlString A string of the URL that's being connected to. */ public OrgApiWrapper(String urlString) { - if (urlString == null) { - throw new IllegalArgumentException("url can't be null"); - } else if (urlString.isBlank()) { - throw new IllegalArgumentException("url can't be blank"); - } + ParameterValidator.stringChecker(urlString, "url"); try { URI uri = URI.create(urlString); this.client = HttpClient.newHttpClient(); From 281d68dbba5e843e2e69de88b1638c7618660bd3 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:43:55 +0200 Subject: [PATCH 42/98] chore[utils] remove placeholder util classes --- src/main/java/edu/group5/app/utils/Utilities.java | 5 ----- src/test/java/edu/group5/app/utils/UtilitiesTest.java | 5 ----- 2 files changed, 10 deletions(-) delete mode 100644 src/main/java/edu/group5/app/utils/Utilities.java delete mode 100644 src/test/java/edu/group5/app/utils/UtilitiesTest.java diff --git a/src/main/java/edu/group5/app/utils/Utilities.java b/src/main/java/edu/group5/app/utils/Utilities.java deleted file mode 100644 index ce21d22..0000000 --- a/src/main/java/edu/group5/app/utils/Utilities.java +++ /dev/null @@ -1,5 +0,0 @@ -package edu.group5.app.utils; - -public class Utilities { - -} diff --git a/src/test/java/edu/group5/app/utils/UtilitiesTest.java b/src/test/java/edu/group5/app/utils/UtilitiesTest.java deleted file mode 100644 index 88aa0c9..0000000 --- a/src/test/java/edu/group5/app/utils/UtilitiesTest.java +++ /dev/null @@ -1,5 +0,0 @@ -package edu.group5.app.utils; - -public class UtilitiesTest { - -} From 1a5616e521fbe63361f93797d0217249ce555046 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:45:41 +0200 Subject: [PATCH 43/98] fix[DbWrapper&ParameterValidator]: fix checks so empty data exports skip data validation --- .../group5/app/model/wrapper/DbWrapper.java | 18 ++----- .../group5/app/utils/ParameterValidator.java | 47 +++++++++---------- 2 files changed, 26 insertions(+), 39 deletions(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java index c61bbc8..f357ae1 100644 --- a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -1,6 +1,6 @@ package edu.group5.app.model.wrapper; -import java.lang.reflect.Parameter; +import edu.group5.app.utils.ParameterValidator; import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; @@ -9,15 +9,10 @@ import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; -import edu.group5.app.utils.ParameterValidator; - /** * A class for wrapping the database. */ @@ -141,11 +136,7 @@ public List importUsers() { public int exportUsers(List data) throws IllegalArgumentException { this.importUsers(); - if (data.isEmpty()) { - return 0; - } - ParameterValidator.exportChecker(data, "data", this.users); - + ParameterValidator.exportChecker(data, "data", this.users, 6); PreparedStatement ps = null; int rowsAffected = 0; @@ -243,10 +234,7 @@ private List importDonations(int user_id, boolean all) { public int exportDonations(List data) throws IllegalArgumentException { this.fetchAllDonations(); - if (data.isEmpty()) { - return 0; - } - ParameterValidator.exportChecker(data, "data", this.donations); + ParameterValidator.exportChecker(data, "data", this.donations, 6); PreparedStatement ps = null; int rowsAffected = 0; diff --git a/src/main/java/edu/group5/app/utils/ParameterValidator.java b/src/main/java/edu/group5/app/utils/ParameterValidator.java index 36a0d14..3bc3e5a 100644 --- a/src/main/java/edu/group5/app/utils/ParameterValidator.java +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -98,31 +98,30 @@ private static final void nullCheck(Object variable, String variableName) * the database, or any of the values in the rows can't be cast to the correct data-types. */ public static final void exportChecker( - List data, String dataName, List oldData + List data, String dataName, List oldData, int rowLength ) throws IllegalArgumentException { - if (data == null) { - throw new IllegalArgumentException(String.format("%s can't be null", dataName)); - } - // TODO: change to check length for every row. - if (data.get(0).length != 6) { - throw new IllegalArgumentException( - String.format("%s's arrays must have a length of 6", dataName) - ); - } - if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { - throw new IllegalArgumentException( - String.format("One or more rows in %s contains null values", dataName) - ); - } - // TODO: change how existing rows are checked. - if (oldData.size() > 0) { - if ((int) data.getLast()[0] <= (int) oldData.getLast()[0]) { - throw new IllegalArgumentException("data can't contain existing rows"); + objectChecker(data, dataName); + if (!data.isEmpty()) { + if (data.stream().anyMatch(i -> i.length != rowLength)) { + throw new IllegalArgumentException( + String.format("%s's arrays must have a length of 6", dataName) + ); } - } - Set ids = new HashSet<>(); - if (data.stream().anyMatch(i -> !ids.add(i[0]))) { - throw new IllegalArgumentException("data can't contain duplicate rows"); - } + if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { + throw new IllegalArgumentException( + String.format("One or more rows in %s contains null values", dataName) + ); + } + + Set ids = new HashSet<>(); + if (data.stream().anyMatch(i -> !ids.add(i[0]))) { + throw new IllegalArgumentException("data can't contain duplicate rows"); + } + if (oldData.size() > 0) { + if (oldData.stream().anyMatch(i -> !ids.add(i[0]))) { + throw new IllegalArgumentException("data can't contain existing rows"); + } + } + } } } From ac865375627a2193382b03d6b3cfc1641df2aeab Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:51:28 +0200 Subject: [PATCH 44/98] style[test/wrapper]: order imports and split too long lines --- .../model/wrapper/DbWrapperDonationsTest.java | 32 ++++++++++++------- .../app/model/wrapper/DbWrapperUserTest.java | 22 ++++--------- .../app/model/wrapper/OrgApiWrapperTest.java | 2 -- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java b/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java index cf76092..1db7a90 100644 --- a/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java @@ -12,12 +12,12 @@ import java.util.Arrays; import java.util.Date; import java.util.List; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import edu.group5.app.model.wrapper.DbWrapper; - +/** + * A test class for interactions with the donations table using the DbWrapper. + */ public class DbWrapperDonationsTest { private Object[] johnDonation; private List users; @@ -54,13 +54,15 @@ void init() { users.add(row); } - this.johnDonation = new Object[] { 1, 1, 39, new BigDecimal(20.02), new Timestamp(new Date().getTime()), - "Paypal" }; + this.johnDonation = new Object[] { + 1, 1, 39, new BigDecimal(20.02), new Timestamp(new Date().getTime()), "Paypal" + }; this.donations = new ArrayList(); this.donations.add(this.johnDonation); - this.janeDonation = new Object[] { 2, 2, 39, new BigDecimal(20.00), new Timestamp(new Date().getTime()), - "Visa debit card" }; + this.janeDonation = new Object[] { + 2, 2, 39, new BigDecimal(20.00), new Timestamp(new Date().getTime()), "Visa debit card" + }; this.donations2 = new ArrayList(); this.donations2.add(this.johnDonation); this.donations2.add(this.janeDonation); @@ -104,7 +106,12 @@ public void importDonationsIsOnlyExportDonationsTest() { assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); assertTrue(this.db.exportDonations(this.donations) == 1); assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 1); - assertTrue(donationEquals(this.donations.get(0), this.db.importDonations((int) this.users.get(0)[0]).get(0))); + assertTrue( + donationEquals( + this.donations.get(0), + this.db.importDonations((int) this.users.get(0)[0]).get(0) + ) + ); assertTrue(this.db.disconnect()); }); } @@ -157,9 +164,12 @@ public void addingSameDonationTwiceThrowsExpectedException2() { assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); assertTrue(this.db.importDonations((int) this.users.get(1)[0]).size() == 0); assertEquals(2, this.db.exportDonations(this.donations2)); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - this.db.exportDonations(this.donations); - }); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> { + this.db.exportDonations(this.donations); + } + ); assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 1); assertTrue(this.db.importDonations((int) this.users.get(1)[0]).size() == 1); assertTrue(this.db.disconnect()); diff --git a/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java b/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java index 7aca3c0..00107de 100644 --- a/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java @@ -2,27 +2,18 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 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.math.BigDecimal; -import java.util.Date; -import java.sql.SQLException; -import java.sql.Time; -import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.List; - -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import edu.group5.app.model.wrapper.DbWrapper; -import javafx.util.converter.BigDecimalStringConverter; - +/** + * A class for testing interactions with the users table of the database using DbWrapper. + */ public class DbWrapperUserTest { private Object[] johnDoe; private Object[] janeDoe; @@ -54,8 +45,9 @@ void init() { this.users3 = new ArrayList(); this.users3.add(this.janeDoe); - this.repeatingJoeJoe = new Object[] { 3, "Customer", "Repeating", "JoeJoe", "repeatingjjoe@email.com", - "passwordpassword" }; + this.repeatingJoeJoe = new Object[] { + 3, "Customer", "Repeating", "JoeJoe", "repeatingjjoe@email.com", "passwordpassword" + }; this.repeatedUsers = new ArrayList(); this.repeatedUsers.add(this.repeatingJoeJoe); this.repeatedUsers.add(this.repeatingJoeJoe); @@ -187,6 +179,4 @@ public void addingUserListWithNullInRowThrowsExpectedException() { assertTrue(this.db.disconnect()); assertEquals("One or more rows in data contains null values", exception.getMessage()); } - - } diff --git a/src/test/java/edu/group5/app/model/wrapper/OrgApiWrapperTest.java b/src/test/java/edu/group5/app/model/wrapper/OrgApiWrapperTest.java index 0a643f2..0165456 100644 --- a/src/test/java/edu/group5/app/model/wrapper/OrgApiWrapperTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/OrgApiWrapperTest.java @@ -9,8 +9,6 @@ import java.lang.IllegalArgumentException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - -import edu.group5.app.model.wrapper.OrgApiWrapper; import tools.jackson.core.exc.StreamReadException; /** From 9d915ba679db416a032af138cf3e05ff37fb9c2c Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 9 Apr 2026 14:35:12 +0200 Subject: [PATCH 45/98] Update[UserPage]: Update Visual on UserPage to increase greater UX and CI --- .../app/view/userpage/UserPageView.java | 73 +++++++++++++++---- src/main/resources/userpage/userpage.css | 3 +- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index ee5d980..8dae614 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -13,6 +13,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; @@ -113,8 +114,18 @@ private VBox createDonationsSection() { Text title = new Text("PREVIOUS DONATIONS"); title.getStyleClass().add("section-title"); + HBox searchBox = new HBox(10); + searchBox.setStyle("-fx-padding: 10;"); + TextField searchField = new TextField(); + searchField.setPromptText("Search by organization name..."); + searchField.setPrefWidth(300); + searchBox.getChildren().add(searchField); + ScrollPane scrollPane = new ScrollPane(); - scrollPane.setFitToWidth(true); + scrollPane.setFitToWidth(false); + scrollPane.setPrefWidth(500); + scrollPane.setMaxWidth(500); + scrollPane.setPrefHeight(400); scrollPane.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); VBox donationsBox = new VBox(12); @@ -122,7 +133,40 @@ private VBox createDonationsSection() { donationsBox.setPadding(new Insets(10)); User currentUser = appState.getCurrentUser(); - Map userDonations = donationController.getUserDonations(currentUser.getUserId()); + Map userDonations = + donationController.getUserDonations(currentUser.getUserId()); + + // Filter donations based on search + searchField.textProperty().addListener((obs, oldVal, newVal) -> { + donationsBox.getChildren().clear(); + + if (userDonations.isEmpty()) { + Label noDonations = new Label("No donations yet"); + noDonations.setStyle("-fx-text-fill: #999;"); + donationsBox.getChildren().add(noDonations); + return; + } + + String searchTerm = newVal.toLowerCase().trim(); + boolean found = false; + + for (Donation donation : userDonations.values()) { + Organization org = organizationController.getOrgById(donation.organizationId()); + String orgName = (org != null) ? org.name() : "Unknown Organization"; + + // Filter by search term + if (searchTerm.isEmpty() || orgName.toLowerCase().contains(searchTerm)) { + donationsBox.getChildren().add(createDonationCard(donation)); + found = true; + } + } + + if (!found && !searchTerm.isEmpty()) { + Label noResults = new Label("No donations found for \"" + newVal + "\""); + noResults.setStyle("-fx-text-fill: #999;"); + donationsBox.getChildren().add(noResults); + } + }); if (userDonations.isEmpty()) { Label noDonations = new Label("No donations yet"); @@ -130,12 +174,12 @@ private VBox createDonationsSection() { donationsBox.getChildren().add(noDonations); } else { for (Donation donation : userDonations.values()) { - donationsBox.getChildren().add(createDonationCard(donation)); // SRP: delegate to helper + donationsBox.getChildren().add(createDonationCard(donation)); } } scrollPane.setContent(donationsBox); - return new VBox(10, title, scrollPane); + return new VBox(10, title, searchBox, scrollPane); } @@ -145,31 +189,34 @@ private Label createCauseChip(Organization org) { return chip; } - private HBox createDonationCard(Donation donation) { + private BorderPane createDonationCard(Donation donation) { Organization org = organizationController.getOrgById(donation.organizationId()); String orgName = (org != null) ? org.name() : "Unknown Organization"; - HBox card = new HBox(20); + // Use BorderPane to fix columns: LEFT | SPACE | RIGHT + BorderPane card = new BorderPane(); card.getStyleClass().add("donation-card"); card.setPadding(new Insets(12, 15, 12, 15)); - card.setAlignment(Pos.CENTER_LEFT); + // LEFT: Organization name Text orgText = new Text(orgName); orgText.getStyleClass().add("donation-org-name"); - + card.setLeft(orgText); + + // RIGHT: Amount and date (stacked vertically) VBox details = new VBox(4); details.setAlignment(Pos.CENTER_RIGHT); - Label amountLabel = - new Label(String.format("%.2f", donation.amount()) + " kr"); + + Label amountLabel = new Label(String.format("%.2f", donation.amount()) + " kr"); amountLabel.getStyleClass().add("donation-amount"); + Label dateLabel = new Label( new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(donation.date())); dateLabel.getStyleClass().add("donation-date"); + details.getChildren().addAll(amountLabel, dateLabel); + card.setRight(details); - card.getChildren().addAll(orgText, details); - HBox.setHgrow(orgText, Priority.ALWAYS); // Org name takes available space - return card; } } diff --git a/src/main/resources/userpage/userpage.css b/src/main/resources/userpage/userpage.css index 5037280..8d68841 100644 --- a/src/main/resources/userpage/userpage.css +++ b/src/main/resources/userpage/userpage.css @@ -16,8 +16,7 @@ .section-box { -fx-background-color: #ddd; - -fx-pref-height: 200px; - -fx-pref-width: 700px; + -fx-pref-height: 150px; -fx-background-radius: 6; } From cfef39ae705932765ef8918092cf270d019f3da6 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 9 Apr 2026 15:16:32 +0200 Subject: [PATCH 46/98] fix[]UserPage: fix sizing of donation section --- .../app/view/userpage/UserPageView.java | 4 +- src/main/resources/donationpage/donation.css | 73 ++++++++++--------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index 8dae614..08600f9 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -123,8 +123,8 @@ private VBox createDonationsSection() { ScrollPane scrollPane = new ScrollPane(); scrollPane.setFitToWidth(false); - scrollPane.setPrefWidth(500); - scrollPane.setMaxWidth(500); + scrollPane.setPrefWidth(650); + scrollPane.setMaxWidth(650); scrollPane.setPrefHeight(400); scrollPane.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); diff --git a/src/main/resources/donationpage/donation.css b/src/main/resources/donationpage/donation.css index 32433df..f690337 100644 --- a/src/main/resources/donationpage/donation.css +++ b/src/main/resources/donationpage/donation.css @@ -1,53 +1,58 @@ .donation-button { - -fx-background-color: white; - -fx-border-color: #ccc; - -fx-border-width: 2px; - -fx-border-radius: 8; - -fx-background-radius: 8; - -fx-cursor: hand; - -fx-font-size: 18px; - -fx-font-weight: bold; + -fx-background-color: white; + -fx-border-color: #ccc; + -fx-border-width: 2px; + -fx-border-radius: 8; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-font-size: 18px; + -fx-font-weight: bold; } .donation-button:hover { - -fx-border-color: #f0f0f0; + -fx-border-color: #f0f0f0; } .donation-button-selected { - -fx-background-color: #111; - -fx-text-fill: white; - -fx-border-color: #111; + -fx-background-color: #111; + -fx-text-fill: white; + -fx-border-color: #111; } + .donation-title { - -fx-font-size: 18px; - -fx-font-weight: bold; - -fx-fill: #111; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-fill: #111; } + .donation-amount { - -fx-font-size: 18px; - -fx-fill: #111; + -fx-font-size: 18px; + -fx-fill: #111; } + .donation-input { - -fx-font-size:16px; - -fx-pref-width: 140px; - -fx-background-color: transparent; - -fx-border-color: transparent transparent #333 transparent; - -fx-alignment: center; + -fx-font-size: 16px; + -fx-pref-width: 140px; + -fx-background-color: transparent; + -fx-border-color: transparent transparent #333 transparent; + -fx-alignment: center; } + .donation-input:focused { - -fx-border-color: transparent transparent #4a90d9 transparent; + -fx-border-color: transparent transparent #4a90d9 transparent; } + .donate-button { - -fx-pref-height: 55px; - -fx-background-color: #e03030; - -fx-text-fill: white; - -fx-font-size: 22px; - -fx-font-weight: bold; - -fx-background-radius: 8; - -fx-cursor: hand; - -fx-padding: 0 40 0 40; -} -.donate-button:hover { - -fx-background-color: #c02020; + -fx-pref-height: 55px; + -fx-background-color: #e03030; + -fx-text-fill: white; + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-padding: 0 40 0 40; } +.donate-button:hover { + -fx-background-color: #c02020; +} \ No newline at end of file From bd56a1d22e6df099479c23fc82ddc4b09adaab8c Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Thu, 9 Apr 2026 22:54:26 +0200 Subject: [PATCH 47/98] feat: added javadoc to more pages --- src/main/java/edu/group5/app/view/Header.java | 8 ++++++++ .../app/view/causespage/CausesPageView.java | 17 +++++++++++++++++ .../app/view/causespage/OrganizationCard.java | 10 ++++++++++ .../organizationpage/OrganizationPageView.java | 9 +++++++++ 4 files changed, 44 insertions(+) diff --git a/src/main/java/edu/group5/app/view/Header.java b/src/main/java/edu/group5/app/view/Header.java index 35472e1..6bc6133 100644 --- a/src/main/java/edu/group5/app/view/Header.java +++ b/src/main/java/edu/group5/app/view/Header.java @@ -7,6 +7,14 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.*; +/** + * A main header for the app. + * + *

    The header consists of a logo button to homepage, + * a navigation bar with buttons to home page, causes page, + * and about us page. The header also has a profile button + * in the upper right corner.

    + */ public class Header extends BorderPane { private final NavigationController controller; diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 1cd6e83..fbf438a 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -13,6 +13,23 @@ import java.util.Map; import java.util.stream.Collectors; +/** + * A view for the causes page. + * + *

    This page allows users to browse and search + * for organizations they may want to donate to. + * Organizations are displayed in a grid layout, + * with four organizations per row. Each organization + * is represented as a clickable card containing its name and logo, + * which navigates to the organization's detail page.

    + * + *

    The page includes a search field that filters the + * displayed organizations based on user input.

    + * + *

    Logos are fetched asynchronously, and a loading indicator + * is shown while the data is being retrieved. If a logo cannot be loaded, + * a fallback "no image" is displayed.

    + */ public class CausesPageView extends BorderPane { private final AppState appState; private final NavigationController nav; diff --git a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java index 31b25ce..e42ebd5 100644 --- a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java +++ b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java @@ -10,6 +10,16 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; +/** + * OrganizationCard represent a single organization card + * in the causes page. + * + *

    The card displays the organization's logo, name, and verification + * checkmark. If no logo is available, a fallback text ("No image") is shown.

    + * + *

    The card is clickable. When pressed it navigates + * to the organization's detail page.

    + */ public class OrganizationCard extends VBox { private final AppState appState; private final Organization organization; diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 684a1a6..77ea7ab 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -19,6 +19,15 @@ import java.util.Objects; +/** + * A view for displaying information about a selected organization. + * + *

    The page shows the organization's logo, name, and description. + * If no logo is available, a fallback "No image" is displayed.

    + * + *

    The page also includes a donate button that navigates to the + * donation page, and a back button to return to the causes page.

    + */ public class OrganizationPageView extends BorderPane { private final AppState appState; private final NavigationController nav; From 427f4e5068ddd602c14cf946b4e4df3a59e0beb1 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Fri, 10 Apr 2026 09:27:43 +0200 Subject: [PATCH 48/98] Step 3: Upgrade Spring Framework Dependency - Compile: SUCCESS Updated spring-core dependency from 6.1.10 to 6.2.0 in pom.xml. Verified main and test source compilation succeeds. Known limitation: full test suite deferred to Final Validation. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 75290fc..5bbba22 100644 --- a/pom.xml +++ b/pom.xml @@ -55,7 +55,7 @@ org.springframework spring-core - 6.1.10 + 6.2.0 org.slf4j From 441155e907935da5de272185e228c669ee9492bc Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Fri, 10 Apr 2026 14:35:42 +0200 Subject: [PATCH 49/98] update[OrganizationPage]: Update OrganizationPage's description to fetch and display the description in a more neatly manner --- .../organization/OrganizationRepository.java | 3 +- .../organization/OrganizationScraper.java | 17 +++++++--- .../OrganizationPageView.java | 21 +++++++++--- .../organizationpage/organizationpage.css | 34 +++++++++++++++---- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java index f31efdc..61155b7 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java @@ -50,7 +50,8 @@ public OrganizationRepository(Object[] input, OrganizationScraper scraper) { boolean trusted = "approved".equalsIgnoreCase((String) contentMap.get("status")); String websiteURL = (String) contentMap.get("url"); boolean isPreApproved = Boolean.TRUE.equals(contentMap.get("is_pre_approved")); - String description = scraper.fetchDescription(websiteURL) != null ? scraper.fetchDescription(websiteURL) : "Information about " + name; + String description = scraper.fetchDescription(websiteURL); + description = description != null ? description : "Information about " + name; Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description, null); grandMap.put(org.orgNumber(), org); diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java index edb41fd..c2c7f3b 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -3,6 +3,9 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; +import org.jsoup.nodes.TextNode; +import org.jsoup.select.Elements; + import java.util.stream.Collectors; import java.util.HashMap; @@ -47,18 +50,22 @@ public String fetchDescription(String pageUrl) { Element section = doc.selectFirst("section.information"); if (section != null) { - // Try to get all

    tags (skip first one if multiple exist) - String description = section.select("p").stream() - .skip(1) // Skip first paragraph (usually a heading) + section.select("div.extra-info").remove(); + section.select("a.read-more").remove(); + + // Extract all

    tags and

    elements as separate paragraphs + String description = section.select("p, div").stream() + .filter(el -> !el.hasClass("extra-info") && !el.hasClass("logo")) .map(Element::text) + .map(text -> text.replace("Les mer", "").trim()) .filter(text -> !text.isBlank()) - .map(String::trim) .collect(Collectors.joining("\n\n")); - // Fallback: if no paragraphs after first, get all text from section + // Fallback: if no paragraphs found, get all text from section if (description.isBlank()) { description = section.text().trim(); } + description = description.replace("Les mer", "").trim(); // Only cache and return if we found something meaningful if (!description.isBlank()) { diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 2d10328..f9dbd22 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -99,19 +99,30 @@ private VBox createOrgInfoSection() { VBox descriptionBox = new VBox(); descriptionBox.setSpacing(15); descriptionBox.setId("description-container"); + descriptionBox.setMaxWidth(750); if (org != null && org.description() != null) { - String[] paragraphs = org.description().split("\n\n"); - for (String para : paragraphs) { - if (!para.isBlank()) { - Label paragraph = new Label(para.trim()); + String[] rawParagraphs = org.description().split("\n{2,}"); + + for (String para : rawParagraphs) { + String cleaned = para.trim(); + if (!cleaned.isBlank()) { + Label paragraph = new Label(cleaned); + paragraph.setId("description-paragraph"); paragraph.setWrapText(true); descriptionBox.getChildren().add(paragraph); } } } - orgNameAndDescription.getChildren().addAll(orgName, descriptionBox); + ScrollPane descriptionScroll = new ScrollPane(descriptionBox); + descriptionScroll.setId("description-scroll"); + descriptionScroll.setFitToWidth(true); + descriptionScroll.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); + descriptionScroll.setPrefHeight(400); + descriptionScroll.setMaxHeight(400); + + orgNameAndDescription.getChildren().addAll(orgName, descriptionScroll); Button donateBtn = new Button("Donate"); donateBtn.setId("donate-button"); diff --git a/src/main/resources/organizationpage/organizationpage.css b/src/main/resources/organizationpage/organizationpage.css index e5263f2..17d2b0c 100644 --- a/src/main/resources/organizationpage/organizationpage.css +++ b/src/main/resources/organizationpage/organizationpage.css @@ -13,19 +13,21 @@ } #description-container { - -fx-padding: 30 0 30 0; - -fx-spacing: 25; + -fx-padding: 30; + -fx-spacing: 22; + -fx-max-width: 750; + -fx-background-color: #f8f9fa; + -fx-border-radius: 8; } #description-paragraph { - -fx-font-size: 16; - -fx-text-fill: #222; + -fx-font-size: 18; + -fx-text-fill: #333; -fx-font-family: "Segoe UI", Arial, sans-serif; - -fx-padding: 15 50 15 50; - -fx-line-spacing: 6; + -fx-line-spacing: 13; -fx-text-alignment: Left; -fx-wrap-text: true; - + -fx-padding: 10 0 10 0; } #donate-button { @@ -42,4 +44,22 @@ #donate-button:hover { -fx-background-color: #c02020; +} + +#description-scroll { + -fx-control-inner-background: #f8f9fa; + -fx-padding: 0; +} + +#description-scroll .scroll-bar:vertical { + -fx-pref-width: 8; +} + +#description-scroll .scroll-bar:vertical .thumb { + -fx-background-radius: 4; + -fx-background-color: #cccccc; +} + +#description-scroll .scroll-bar:vertical .thumb:hover { + -fx-background-color: #999999; } \ No newline at end of file From 4edd823e950aa8a0ba9a22d7414303d54940f321 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Fri, 10 Apr 2026 22:14:35 +0200 Subject: [PATCH 50/98] fix&update[OrganizationPage]: Fix duplication and update paragraph spacing on the description fetched from API --- .../group5/app/model/organization/OrganizationScraper.java | 1 + .../app/view/organizationpage/OrganizationPageView.java | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java index c2c7f3b..6a8c230 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -55,6 +55,7 @@ public String fetchDescription(String pageUrl) { // Extract all

    tags and

    elements as separate paragraphs String description = section.select("p, div").stream() + .filter(el -> el.tagName().equals("p") || el.select("p").isEmpty()) .filter(el -> !el.hasClass("extra-info") && !el.hasClass("logo")) .map(Element::text) .map(text -> text.replace("Les mer", "").trim()) diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index f9dbd22..bbc1666 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -61,7 +61,7 @@ private StackPane createImageContainer() { imageContainer.setId("imageContainer"); imageContainer.setPrefHeight(120); imageContainer.setPrefWidth(120); - imageContainer.setMaxWidth(Double.MAX_VALUE); + imageContainer.setMaxWidth(120); Organization org = appState.getCurrentOrganization(); if (org != null && org.logoUrl() != null && !org.logoUrl().isBlank()) { @@ -121,6 +121,8 @@ private VBox createOrgInfoSection() { descriptionScroll.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); descriptionScroll.setPrefHeight(400); descriptionScroll.setMaxHeight(400); + descriptionScroll.setPrefWidth(750); + descriptionScroll.setMinWidth(750); orgNameAndDescription.getChildren().addAll(orgName, descriptionScroll); From 5767aaab5cc6485f317899ee0c8205e5c6b60c94 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Sun, 12 Apr 2026 18:33:17 +0200 Subject: [PATCH 51/98] update[DonationPage]: Refactor donation selection, add confirmation dialog, and improve custom amount UI --- .../app/control/DonationController.java | 54 +++++++-- .../view/donationpage/DonationPageView.java | 104 +++++++++++------- src/main/resources/donationpage/donation.css | 38 +++++++ 3 files changed, 152 insertions(+), 44 deletions(-) diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 97a1c9d..907caa3 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -12,6 +12,9 @@ import java.util.Map; import java.util.Set; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; + public class DonationController { private final AppState appState; private final NavigationController nav; @@ -38,26 +41,55 @@ public Set getUniqueOrgs() { return uniqueOrgs; } - public void handleDonate() { - // Get session data from MainController + public void requestDonationConfirmation() { + // Get session data User currentUser = appState.getCurrentUser(); Organization currentOrg = appState.getCurrentOrganization(); BigDecimal amount = appState.getCurrentDonationAmount(); + // Validate before showing dialog if (currentUser == null) { - System.err.println("Error: No user logged in"); + showError("Error: No user logged in"); return; } - if (!(currentUser instanceof Customer customer)) { - System.err.println("Error: Only customers can donate"); + if (!(currentUser instanceof Customer)) { + showError("Error: Only customers can donate"); return; } if (currentOrg == null) { - System.err.println("Error: No organization selected"); + showError("Error: No organization selected"); return; } if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { - System.err.println("Error: Invalid donation amount"); + showError("Please select a donation amount first"); + return; + } + + // Show confirmation dialog + Alert confirmDialog = new Alert(Alert.AlertType.CONFIRMATION); + confirmDialog.setTitle("Confirm Donation"); + confirmDialog.setHeaderText("Confirm Your Donation"); + confirmDialog.setContentText( + "Organization: " + currentOrg.name() + "\n" + + "Amount: " + amount + " kr\n\n" + + "Are you sure you want to proceed?" + ); + + // If user clicks OK, process donation + if (confirmDialog.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { + handleDonate(); + } + // If Cancel, dialog just closes and nothing happens + } + + private void handleDonate() { + // This now only handles the actual donation processing + User currentUser = appState.getCurrentUser(); + Organization currentOrg = appState.getCurrentOrganization(); + BigDecimal amount = appState.getCurrentDonationAmount(); + + if (!(currentUser instanceof Customer customer)) { + System.err.println("Error: Only customers can donate"); return; } @@ -81,4 +113,12 @@ public void handleDonate() { // Navigate to payment complete nav.showPaymentCompletePage(); } + + private void showError(String message) { + Alert errorAlert = new Alert(Alert.AlertType.WARNING); + errorAlert.setTitle("Donation Error"); + errorAlert.setHeaderText("Cannot Process Donation"); + errorAlert.setContentText(message); + errorAlert.showAndWait(); + } } diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 5087267..49a9a73 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -5,8 +5,8 @@ import edu.group5.app.model.AppState; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.Button; import javafx.scene.control.TextField; +import javafx.scene.control.Button; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.TilePane; @@ -16,18 +16,14 @@ import javafx.scene.Node; import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; public class DonationPageView extends BorderPane { private final AppState appState; private final NavigationController nav; private final DonationController donationController; - private final List allDonationElements = new ArrayList<>(); - private final Map elementAmounts = new HashMap<>(); + private Node currentlySelected = null; + private TextField customAmountField; public DonationPageView(AppState appState, NavigationController nav, DonationController donationController) { this.appState = appState; @@ -38,6 +34,12 @@ public DonationPageView(AppState appState, NavigationController nav, DonationCon VBox content = new VBox(); content.getChildren().addAll(createDonationGrid(), createDonateSection()); + + content.setOnMouseClicked(e -> { + if (e.getTarget() == content) { + clearSelection(); + } + }); setCenter(content); } @@ -75,14 +77,12 @@ public Button createDonationButton(String title, String amount) { button.getStyleClass().add("donation-button"); BigDecimal parsedAmount = parseAmount(amount); - elementAmounts.put(button, parsedAmount); + button.setUserData(parsedAmount); - button.setOnAction(e -> { - selectDonationElement(button); - }); - allDonationElements.add(button); + button.setOnAction(e -> selectDonation(button)); return button; } + private VBox createCustomButton() { Text titleText = new Text("Custom Donation"); titleText.getStyleClass().add("donation-title"); @@ -93,8 +93,10 @@ private VBox createCustomButton() { Text krText = new Text("kr"); krText.getStyleClass().add("donation-amount"); - TextField amountField = new TextField(); + this.customAmountField = new TextField(); + TextField amountField = customAmountField; amountField.getStyleClass().add("donation-input"); + amountField.setPromptText("Enter amount"); amountRow.getChildren().addAll(amountField, krText); @@ -103,48 +105,76 @@ private VBox createCustomButton() { box.getStyleClass().add("donation-button"); box.setOnMouseClicked(e -> { - try { - BigDecimal amount = new BigDecimal(amountField.getText().trim()); - elementAmounts.put(box, amount); - selectDonationElement(box); - } catch (NumberFormatException exception) { - System.err.println("Invalid custom donation amount: " + amountField.getText()); - } + selectDonation(box); + amountField.requestFocus(); + }); + + amountField.setOnMouseClicked(e -> + { + selectDonation(box); + amountField.requestFocus(); }); - allDonationElements.add(box); + // NEW: On text field input - update the amount in real-time + amountField.textProperty().addListener((obs, oldVal, newVal) -> { + if (!newVal.trim().isEmpty()) { + try { + BigDecimal amount = new BigDecimal(newVal.trim()); + box.setUserData(amount); + updateDonationAmount(amount); + } catch (NumberFormatException ignored) { + // User is still typing, silently ignore + } + } else { + box.setUserData(null); + if (currentlySelected == box) { + updateDonationAmount(null); + } + } + }); return box; } + private HBox createDonateSection() { Button donateBtn = new Button("Donate"); donateBtn.getStyleClass().add("donate-button"); - donateBtn.setOnAction(e -> donationController.handleDonate()); + donateBtn.setOnAction(e -> donationController.requestDonationConfirmation()); - HBox section = new HBox(donateBtn); + Button clearBtn = new Button("Clear"); + clearBtn.getStyleClass().add("clear-button"); + clearBtn.setOnAction(e -> clearSelection()); + + HBox section = new HBox(20, clearBtn, donateBtn); section.setAlignment(Pos.CENTER); section.setPadding(new Insets(20, 0, 30, 0)); return section; } - private void selectDonationElement(Node element) { - // Remove selected class from all elements - for (Node node : allDonationElements) { - node.getStyleClass().remove("donation-button-selected"); + private void selectDonation(Node element) { + if (currentlySelected != null) { + currentlySelected.getStyleClass().remove("donation-button-selected"); } + currentlySelected = element; + currentlySelected.getStyleClass().add("donation-button-selected"); - element.getStyleClass().add("donation-button-selected"); - - // Extract and store the amount - extractAndStoreAmount(element); + BigDecimal amount = (BigDecimal) element.getUserData(); + updateDonationAmount(amount); } - private void extractAndStoreAmount(Node element) { - BigDecimal amount = elementAmounts.get(element); - if (amount != null) { - appState.setCurrentDonationAmount(amount); - } else { - System.err.println("Error: No amount found for selected element"); + private void clearSelection() { + if (currentlySelected != null) { + currentlySelected.getStyleClass().remove("donation-button-selected"); + currentlySelected = null; + updateDonationAmount(null); } + + if (customAmountField != null) { + customAmountField.clear(); + } + } + + private void updateDonationAmount(BigDecimal amount) { + appState.setCurrentDonationAmount(amount); } private BigDecimal parseAmount(String amountStr) { diff --git a/src/main/resources/donationpage/donation.css b/src/main/resources/donationpage/donation.css index f690337..edacdd3 100644 --- a/src/main/resources/donationpage/donation.css +++ b/src/main/resources/donationpage/donation.css @@ -55,4 +55,42 @@ .donate-button:hover { -fx-background-color: #c02020; +} + +.clear-button { + -fx-pref-height: 55px; + -fx-background-color: #f0f0f0; + -fx-text-fill: #333; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-padding: 0 30 0 30; + -fx-border-color: #ccc; + -fx-border-width: 1; +} + +.clear-button:hover { + -fx-background-color: #e0e0e0; +} + +.clear-button:pressed { + -fx-background-color: #d0d0d0; +} + +.donation-button-selected .donation-title { + -fx-fill: white; +} + +.donation-button-selected .donation-amount { + -fx-fill: white; +} + +.donation-button-selected .donation-input { + -fx-text-fill: white; +} + +.donation-button-selected .donation-input:focused { + -fx-text-fill: white; + -fx-border-color: transparent transparent white transparent; } \ No newline at end of file From b1a1fb8134fe2d707e40cb66746fa12cc235ad40 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 14 Apr 2026 13:33:22 +0200 Subject: [PATCH 52/98] feat&Update[donationPage]: Add PaymentMethod and backButton from release, fixing up upcoming mege conflict --- .../view/donationpage/DonationPageView.java | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 49a9a73..bdc8b47 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -16,6 +16,7 @@ import javafx.scene.Node; import java.math.BigDecimal; +import java.util.Objects; public class DonationPageView extends BorderPane { private final AppState appState; @@ -24,25 +25,38 @@ public class DonationPageView extends BorderPane { private Node currentlySelected = null; private TextField customAmountField; + private Node selectedPaymentMethod = null; + private Button donateBtn; public DonationPageView(AppState appState, NavigationController nav, DonationController donationController) { this.appState = appState; this.nav = nav; this.donationController = donationController; - getStylesheets().add(getClass().getResource("/donationpage/donation.css").toExternalForm()); + getStylesheets().add(Objects.requireNonNull(getClass().getResource("/donationpage/donation.css")).toExternalForm()); VBox content = new VBox(); - content.getChildren().addAll(createDonationGrid(), createDonateSection()); + content.getChildren().addAll(createBackButton(), createDonationGrid(), createPaymentMethodSection(), createDonateSection()); content.setOnMouseClicked(e -> { if (e.getTarget() == content) { - clearSelection(); + clearSelection(); } }); setCenter(content); } + + private HBox createBackButton() { + Button backBtn = new Button("←"); + backBtn.getStyleClass().add("back-button"); + backBtn.setOnAction(e -> nav.showOrganizationPage()); + + HBox container = new HBox(backBtn); + container.setPadding(new Insets(10, 0, 0, 10)); + return container; + } + private TilePane createDonationGrid(){ TilePane body = new TilePane(); body.setAlignment(Pos.CENTER); @@ -136,8 +150,9 @@ private VBox createCustomButton() { } private HBox createDonateSection() { - Button donateBtn = new Button("Donate"); + donateBtn = new Button("Donate"); donateBtn.getStyleClass().add("donate-button"); + donateBtn.setDisable(true); donateBtn.setOnAction(e -> donationController.requestDonationConfirmation()); Button clearBtn = new Button("Clear"); @@ -150,6 +165,23 @@ private HBox createDonateSection() { return section; } + public HBox createPaymentMethodSection() { + Button appleBtn = new Button("Apple Pay"); + Button vippsBtn = new Button("Vipps"); + Button visaBtn = new Button("Visa"); + + for (Button btn : new Button[]{appleBtn, vippsBtn, visaBtn}) { + btn.getStyleClass().add("payment-method-button"); + btn.setOnAction(e -> selectPaymentMethod(btn)); + } + + HBox sectionPm = new HBox(appleBtn, vippsBtn, visaBtn); + sectionPm.setAlignment(Pos.CENTER); + sectionPm.setSpacing(20); + sectionPm.setPadding(new Insets(20, 20, 20, 20)); + return sectionPm; + } + private void selectDonation(Node element) { if (currentlySelected != null) { currentlySelected.getStyleClass().remove("donation-button-selected"); @@ -159,6 +191,16 @@ private void selectDonation(Node element) { BigDecimal amount = (BigDecimal) element.getUserData(); updateDonationAmount(amount); + updateDonationButtonState(); + } + + private void selectPaymentMethod(Node element) { + if (selectedPaymentMethod != null) { + selectedPaymentMethod.getStyleClass().remove("payment-method-selected"); + } + selectedPaymentMethod = element; + selectedPaymentMethod.getStyleClass().add("payment-method-selected"); + updateDonationButtonState(); } private void clearSelection() { @@ -168,9 +210,16 @@ private void clearSelection() { updateDonationAmount(null); } + if (selectedPaymentMethod != null) { + selectedPaymentMethod.getStyleClass().remove("payment-method-selected"); + selectedPaymentMethod = null; + } + if (customAmountField != null) { customAmountField.clear(); } + + updateDonationButtonState(); } private void updateDonationAmount(BigDecimal amount) { @@ -185,4 +234,8 @@ private BigDecimal parseAmount(String amountStr) { } } + private void updateDonationButtonState() { + donateBtn.setDisable(currentlySelected == null || selectedPaymentMethod == null); + } + } \ No newline at end of file From 0556c5b534e6bdc03c1df585d569fcf7dcba2453 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 14 Apr 2026 14:07:56 +0200 Subject: [PATCH 53/98] fix&Update[DonationPage]: fix up merge conflict problems and update donationPage to also include payment method --- .../java/edu/group5/app/control/DonationController.java | 4 +++- .../edu/group5/app/view/donationpage/DonationPageView.java | 7 +++++++ .../java/edu/group5/app/view/userpage/UserPageView.java | 5 ----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 8e965ed..a579a9b 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -72,7 +72,8 @@ public void requestDonationConfirmation() { confirmDialog.setHeaderText("Confirm Your Donation"); confirmDialog.setContentText( "Organization: " + currentOrg.name() + "\n" + - "Amount: " + amount + " kr\n\n" + + "Amount: " + amount + " kr\n" + + "Payment Method: " + paymentMethod + "\n\n" + "Are you sure you want to proceed?" ); @@ -88,6 +89,7 @@ private void handleDonate() { User currentUser = appState.getCurrentUser(); Organization currentOrg = appState.getCurrentOrganization(); BigDecimal amount = appState.getCurrentDonationAmount(); + String paymentMethod = appState.getCurrentPaymentMethod(); if (!(currentUser instanceof Customer customer)) { System.err.println("Error: Only customers can donate"); diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index bdc8b47..0c5c6c3 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -170,6 +170,10 @@ public HBox createPaymentMethodSection() { Button vippsBtn = new Button("Vipps"); Button visaBtn = new Button("Visa"); + appleBtn.setUserData("Apple Pay"); + vippsBtn.setUserData("Vipps"); + visaBtn.setUserData("Visa"); + for (Button btn : new Button[]{appleBtn, vippsBtn, visaBtn}) { btn.getStyleClass().add("payment-method-button"); btn.setOnAction(e -> selectPaymentMethod(btn)); @@ -200,6 +204,9 @@ private void selectPaymentMethod(Node element) { } selectedPaymentMethod = element; selectedPaymentMethod.getStyleClass().add("payment-method-selected"); + + String paymentMethod = (String) element.getUserData(); + appState.setCurrentPaymentMethod(paymentMethod); updateDonationButtonState(); } diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index 7dc82a2..6d46f5b 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -105,8 +105,6 @@ private VBox createCausesSection() { } } } - ScrollPane scrollPane = new ScrollPane(causesBox); - scrollPane.setFitToWidth(true); scrollPane.setPrefHeight(150); scrollPane.setContent(causesFlow); @@ -181,9 +179,6 @@ private VBox createDonationsSection() { } } - ScrollPane scrollPane = new ScrollPane(donationsBox); - scrollPane.setFitToWidth(true); - scrollPane.setPrefHeight(200); scrollPane.setContent(donationsBox); return new VBox(10, title, searchBox, scrollPane); From d792af2e54bcea8079e757e76ae04740e9de9be2 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:19:21 +0200 Subject: [PATCH 54/98] update[LoginController]: improve password handling by clearing passward char array, and by not saving a password String as a variable --- .../java/edu/group5/app/control/LoginController.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/group5/app/control/LoginController.java b/src/main/java/edu/group5/app/control/LoginController.java index cdd5b5f..2a860c9 100644 --- a/src/main/java/edu/group5/app/control/LoginController.java +++ b/src/main/java/edu/group5/app/control/LoginController.java @@ -5,6 +5,9 @@ import edu.group5.app.model.user.UserService; import edu.group5.app.view.loginpage.LoginPageView; import edu.group5.app.view.loginpage.SignInPageView; + +import java.util.Arrays; + import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class LoginController { @@ -27,9 +30,13 @@ public void handleSignIn(SignInPageView view, String firstName, String lastName, return; } - String password = new String(passwordChars); BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - String hashedPassword = encoder.encode(password); + + // Clears password char array after creating a hash. + String hashedPassword = encoder.encode(new String(passwordChars)); + for (int i = 0; i < passwordChars.length; i++) { + passwordChars[0] = '0'; + } boolean success = userService.registerUser( "Customer", firstName, lastName, email, hashedPassword); From d6c8d3baa2aa39ab62ab00307ec707ac6dc9ce33 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:20:19 +0200 Subject: [PATCH 55/98] feat[LoginController]: add dialog box for accepting privacy policy when signing up --- .../group5/app/control/LoginController.java | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/group5/app/control/LoginController.java b/src/main/java/edu/group5/app/control/LoginController.java index 2a860c9..59a1785 100644 --- a/src/main/java/edu/group5/app/control/LoginController.java +++ b/src/main/java/edu/group5/app/control/LoginController.java @@ -5,6 +5,9 @@ import edu.group5.app.model.user.UserService; import edu.group5.app.view.loginpage.LoginPageView; import edu.group5.app.view.loginpage.SignInPageView; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; import java.util.Arrays; @@ -38,17 +41,30 @@ public void handleSignIn(SignInPageView view, String firstName, String lastName, passwordChars[0] = '0'; } - boolean success = userService.registerUser( - "Customer", firstName, lastName, email, hashedPassword); + Alert privacyPolicy = new Alert(Alert.AlertType.CONFIRMATION); + privacyPolicy.setTitle("Accept Privacy Policy"); + privacyPolicy.setHeaderText("Accept Privacy Policy"); + privacyPolicy.setContentText( + "Your user information like:\n" + + "Name and emailβ€”as well as donations tied to your accountβ€”will be saved locally on your machine.\n" + + "By creating an account, you accept the right of our app to store this information."); - if (success) { - User user = userService.getUserByEmail(email); + if (privacyPolicy.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { + boolean success = userService.registerUser( + "Customer", firstName, lastName, email, hashedPassword); - appState.setCurrentUser(user); - nav.showHomePage(); + if (success) { + User user = userService.getUserByEmail(email); + + appState.setCurrentUser(user); + nav.showHomePage(); + } else { + view.showError("Registration failed. Email may already be in use."); + } } else { - view.showError("Registration failed. Email may already be in use."); + view.showError("Registration failed. Must Accept Privacy Policy to create account."); } + } public void handleLogin(LoginPageView view, String email, char[] passwordChars) { From 85f47f736ae6a40ac6311f734e19fc505e3ce20b Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:28:05 +0200 Subject: [PATCH 56/98] fix[UserService]: perform a check for existing emails when registering a new user --- .../group5/app/model/user/UserService.java | 68 ++++++++++++------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/src/main/java/edu/group5/app/model/user/UserService.java b/src/main/java/edu/group5/app/model/user/UserService.java index 628c785..b237eae 100644 --- a/src/main/java/edu/group5/app/model/user/UserService.java +++ b/src/main/java/edu/group5/app/model/user/UserService.java @@ -1,16 +1,21 @@ package edu.group5.app.model.user; /** - * Service class for managing user-related operations, such as registration and login. - * It interacts with the UserRepository to perform these operations and contains the business logic - * associated with user management, including validation of input data and handling of user authentication. + * Service class for managing user-related operations, such as registration and + * login. + * It interacts with the UserRepository to perform these operations and contains + * the business logic + * associated with user management, including validation of input data and + * handling of user authentication. */ public class UserService { private UserRepository userRepository; /** * Constructs a UserService with the given UserRepository. - * @param userRepository the UserRepository to use for managing user data; must not be null + * + * @param userRepository the UserRepository to use for managing user data; must + * not be null * @throws IllegalArgumentException if userRepository is null */ public UserService(UserRepository userRepository) { @@ -22,7 +27,9 @@ public UserService(UserRepository userRepository) { /** * Getter for the UserRepository used by this service. - * This method allows access to the user repository for managing user data and performing operations such as registration and login. + * This method allows access to the user repository for managing user data and + * performing operations such as registration and login. + * * @return the UserRepository instance used by this service */ public UserRepository getUserRepository() { @@ -30,31 +37,39 @@ public UserRepository getUserRepository() { } /** - * Registers a new user with the given information. Validates the input data and creates a new User object - * based on the specified role. Currently supports registration for customers only. - * @param role the role of the user (e.g., "Customer"); must not be null or empty - * @param firstName the first name of the user; must not be null or empty - * @param lastName the last name of the user; must not be null or empty - * @param email the email address of the user; must not be null or empty - * @param passwordHash the hashed password of the user; must not be null or empty - * @return true if the user was successfully registered, false if any input is invalid or - * if the role is not supported - * @throws IllegalArgumentException if any of the input parameters are null or empty - * or if the role is not supported + * Registers a new user with the given information. Validates the input data and + * creates a new User object + * based on the specified role. Currently supports registration for customers + * only. + * + * @param role the role of the user (e.g., "Customer"); must not be null + * or empty + * @param firstName the first name of the user; must not be null or empty + * @param lastName the last name of the user; must not be null or empty + * @param email the email address of the user; must not be null or empty + * @param passwordHash the hashed password of the user; must not be null or + * empty + * @return true if the user was successfully registered, false if any input is + * invalid or + * if the role is not supported + * @throws IllegalArgumentException if any of the input parameters are null or + * empty + * or if the role is not supported */ public boolean registerUser(String role, String firstName, String lastName, - String email, String passwordHash) { + String email, String passwordHash) { if (role == null || role.trim().isEmpty() || firstName == null || firstName.trim().isEmpty() || lastName == null || lastName.trim().isEmpty() || email == null || email.trim().isEmpty() || - passwordHash == null || passwordHash.trim().isEmpty()) { + passwordHash == null || passwordHash.trim().isEmpty() || + this.getUserByEmail(email) != null) { return false; } User user; if (role.equalsIgnoreCase("Customer")) { user = new Customer(userRepository.getNextUserId(), firstName, lastName, email, passwordHash); - } else { /* TODO when you switch to a real DB, replace getNextUserId with DB auto-increment/identity and ignore manual ID generation in service*/ + } else { return false; } this.userRepository.addContent(user); @@ -63,15 +78,19 @@ public boolean registerUser(String role, String firstName, String lastName, /** * Authenticates a user based on the provided email and password. - * @param email the email address of the user attempting to log in; must not be null or empty - * @param password the plaintext password of the user attempting to log in; must not be null or empty + * + * @param email the email address of the user attempting to log in; must not + * be null or empty + * @param password the plaintext password of the user attempting to log in; must + * not be null or empty * @return the authenticated User object if the login is successful - * (i.e., the user exists and the password is correct), null otherwise - * @throws IllegalArgumentException if email is null or empty, or if password is null or empty + * (i.e., the user exists and the password is correct), null otherwise + * @throws IllegalArgumentException if email is null or empty, or if password is + * null or empty */ public User login(String email, char[] password) { if (email == null || email.trim().isEmpty() || password == null || password.length == 0) { - return null; + return null; } User user = this.userRepository.findUserByEmail(email); if (user != null && user.verifyPassword(password)) { @@ -82,6 +101,7 @@ public User login(String email, char[] password) { /** * Retrieves a user by email address. + * * @param email the email address of the user to find; must not be null or empty * @return the User object if found, null otherwise */ From 26c615c4e348477c9f2aa500782acb28fa4babad Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Tue, 14 Apr 2026 16:21:29 +0200 Subject: [PATCH 57/98] fix: button styling for donationpage --- src/main/resources/donationpage/donation.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/donationpage/donation.css b/src/main/resources/donationpage/donation.css index 18680e3..bc777b6 100644 --- a/src/main/resources/donationpage/donation.css +++ b/src/main/resources/donationpage/donation.css @@ -108,7 +108,7 @@ .donation-button-selected .donation-input:focused { -fx-text-fill: white; -fx-border-color: transparent transparent white transparent; - +} .payment-method-button { -fx-background-color: #111; -fx-text-fill: white; From 1a32ddd488895b076ff293017302f887c7400f06 Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Tue, 14 Apr 2026 16:22:02 +0200 Subject: [PATCH 58/98] fix: size of causes section --- src/main/java/edu/group5/app/view/userpage/UserPageView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index 6d46f5b..e3b7c8b 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -105,7 +105,7 @@ private VBox createCausesSection() { } } } - scrollPane.setPrefHeight(150); + scrollPane.setPrefHeight(275); scrollPane.setContent(causesFlow); return new VBox(10, title, scrollPane); From b91eecd83f462925586c590b55d74c0e7022fe0a Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 16 Apr 2026 13:43:38 +0200 Subject: [PATCH 59/98] Update[ParameterValidator]: Update codebase to use ParameterValidator class where it suits the best --- src/main/java/edu/group5/app/App.java | 9 ++++++- .../group5/app/control/AuthController.java | 11 +++++--- .../app/control/DonationController.java | 20 +++++++++++++-- .../app/control/NavigationController.java | 10 +++++++- .../app/control/OrganizationController.java | 4 +++ .../edu/group5/app/model/DBRepository.java | 4 +++ .../java/edu/group5/app/model/Repository.java | 3 +++ .../model/donation/DonationRepository.java | 23 ++++++----------- .../app/model/donation/DonationService.java | 10 +++----- .../app/model/organization/Organization.java | 6 ++--- .../organization/OrganizationRepository.java | 22 ++++++++-------- .../organization/OrganizationScraper.java | 18 +++---------- .../organization/OrganizationService.java | 12 ++++----- .../edu/group5/app/model/user/Customer.java | 11 ++++---- .../java/edu/group5/app/model/user/User.java | 25 ++++++++----------- .../group5/app/model/user/UserRepository.java | 19 +++++--------- .../group5/app/model/user/UserService.java | 10 +++----- .../group5/app/utils/ParameterValidator.java | 4 +-- .../app/view/causespage/CausesPageView.java | 5 ++++ .../app/view/causespage/OrganizationCard.java | 1 + .../view/donationpage/DonationPageView.java | 5 ++++ 21 files changed, 123 insertions(+), 109 deletions(-) diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index b9d67da..6402156 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -35,6 +35,7 @@ public class App extends Application { NavigationController nav; private Logger logger; + static int MAX_ReTRIES = 3; @Override public void init() { @@ -44,8 +45,14 @@ public void init() { this.dbWrapper = new DbWrapper(false); OrgApiWrapper orgApiWrapper = new OrgApiWrapper("https://app.innsamlingskontrollen.no/api/public/v1/all"); - while (!dbWrapper.connect()) { + int retries = 0; + while (!dbWrapper.connect() && retries < MAX_ReTRIES) { this.logger.warning("Failed to connect to database"); + retries++; + } + if (retries == MAX_ReTRIES) { + this.logger.severe("Failed to connect to database after " + MAX_ReTRIES + " attempts"); + throw new RuntimeException("Failed to connect to database"); } // Load data from database diff --git a/src/main/java/edu/group5/app/control/AuthController.java b/src/main/java/edu/group5/app/control/AuthController.java index 249a436..4911a3b 100644 --- a/src/main/java/edu/group5/app/control/AuthController.java +++ b/src/main/java/edu/group5/app/control/AuthController.java @@ -3,6 +3,7 @@ import edu.group5.app.model.AppState; import edu.group5.app.model.user.User; import edu.group5.app.model.user.UserService; +import edu.group5.app.utils.ParameterValidator; import edu.group5.app.view.loginpage.LoginPageView; import edu.group5.app.view.loginpage.SignUpPageView; import javafx.scene.control.Alert; @@ -31,6 +32,9 @@ public class AuthController { private final UserService userService; public AuthController(AppState appState, NavigationController nav, UserService userService) { + ParameterValidator.objectChecker(appState, "AppState"); + ParameterValidator.objectChecker(nav, "NavigationController"); + ParameterValidator.objectChecker(userService, "UserService"); this.appState = appState; this.nav = nav; this.userService = userService; @@ -69,7 +73,7 @@ public void handleSignUp(SignUpPageView view, String firstName, String lastName, // Clears password char array after creating a hash. String hashedPassword = encoder.encode(new String(passwordChars)); for (int i = 0; i < passwordChars.length; i++) { - passwordChars[0] = '0'; + passwordChars[i] = '\u0000'; } Alert privacyPolicy = new Alert(Alert.AlertType.CONFIRMATION); @@ -86,9 +90,8 @@ public void handleSignUp(SignUpPageView view, String firstName, String lastName, if (success) { User user = userService.getUserByEmail(email); - - appState.setCurrentUser(user); - nav.showHomePage(); + appState.setCurrentUser(user); + nav.showHomePage(); } else { view.showError("Registration failed. Email may already be in use."); } diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 8a8ec79..61ab172 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -6,6 +6,7 @@ import edu.group5.app.model.organization.Organization; import edu.group5.app.model.user.Customer; import edu.group5.app.model.user.User; +import edu.group5.app.utils.ParameterValidator; import java.math.BigDecimal; import java.util.HashSet; @@ -34,6 +35,9 @@ public class DonationController { private final DonationService service; public DonationController(AppState appState, NavigationController nav, DonationService service) { + ParameterValidator.objectChecker(appState, "AppState"); + ParameterValidator.objectChecker(nav, "NavigationController"); + ParameterValidator.objectChecker(service, "DonationService"); this.appState = appState; this.nav = nav; this.service = service; @@ -46,6 +50,7 @@ public DonationController(AppState appState, NavigationController nav, DonationS * @return a map of donations. */ public Map getUserDonations(int userId) { + ParameterValidator.intChecker(userId, "User ID"); return service.getDonationRepository().filterByUser(userId); } @@ -56,7 +61,11 @@ public Map getUserDonations(int userId) { * @return a set of organization IDs */ public Set getUniqueOrganizationIDs() { - Map userDonations = getUserDonations(appState.getCurrentUser().getUserId()); + User currentUser = appState.getCurrentUser(); + if (currentUser == null) { + throw new IllegalStateException("No user logged in"); + } + Map userDonations = getUserDonations(currentUser.getUserId()); Set uniqueOrganizations = new HashSet<>(); for (Donation donation : userDonations.values()) { @@ -146,19 +155,26 @@ private void handleDonate() { ); if (success) { - System.out.println("Donation created: " + amount + " kr to " + currentOrg.name() + ", with payment method: " + paymentMethod); + System.out.println("Donation created: " + + amount + " kr to " + currentOrg.name() + + ", with payment method: " + paymentMethod); } else { System.err.println("Failed to create donation"); } // Clear donation session state appState.setCurrentDonationAmount(null); + // Clear org + appState.setCurrentOrganization(null); + // Clear payment method + appState.setCurrentPaymentMethod(null); // Navigate to payment complete nav.showPaymentCompletePage(); } private void showError(String message) { + ParameterValidator.stringChecker(message, "message"); Alert errorAlert = new Alert(Alert.AlertType.WARNING); errorAlert.setTitle("Donation Error"); errorAlert.setHeaderText("Cannot Process Donation"); diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index 7bfbc8c..326389b 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -4,6 +4,7 @@ import edu.group5.app.model.donation.DonationService; import edu.group5.app.model.organization.OrganizationService; import edu.group5.app.model.user.UserService; +import edu.group5.app.utils.ParameterValidator; import edu.group5.app.view.Header; import edu.group5.app.view.causespage.CausesPageView; import edu.group5.app.view.donationpage.DonationPageView; @@ -30,7 +31,14 @@ public class NavigationController { private final DonationController donationController; private final OrganizationController organizationController; - public NavigationController(BorderPane root, AppState appState, UserService userService, DonationService donationService, OrganizationService organizationService) { + public NavigationController(BorderPane root, AppState appState, UserService userService, + DonationService donationService, OrganizationService organizationService) { + ParameterValidator.objectChecker(root, "Root BorderPane"); + ParameterValidator.objectChecker(appState, "AppState"); + ParameterValidator.objectChecker(userService, "UserService"); + ParameterValidator.objectChecker(donationService, "DonationService"); + ParameterValidator.objectChecker(organizationService, "OrganizationService"); + this.root = root; this.header = new Header(this); this.loginHeader = new LoginHeader(); diff --git a/src/main/java/edu/group5/app/control/OrganizationController.java b/src/main/java/edu/group5/app/control/OrganizationController.java index fb65127..fc1252f 100644 --- a/src/main/java/edu/group5/app/control/OrganizationController.java +++ b/src/main/java/edu/group5/app/control/OrganizationController.java @@ -2,6 +2,8 @@ import edu.group5.app.model.organization.Organization; import edu.group5.app.model.organization.OrganizationService; +import edu.group5.app.utils.ParameterValidator; +import tools.jackson.databind.deser.bean.CreatorCandidate.Param; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -13,10 +15,12 @@ public class OrganizationController { private final OrganizationService service; public OrganizationController(OrganizationService service) { + ParameterValidator.objectChecker(service, "OrganizationService"); this.service = service; } public Organization getOrganizationById(int orgId) { + ParameterValidator.intChecker(orgId, "Organization ID"); return service.findByOrgNumber(orgId); } diff --git a/src/main/java/edu/group5/app/model/DBRepository.java b/src/main/java/edu/group5/app/model/DBRepository.java index 3cd1fa9..88ac3d9 100644 --- a/src/main/java/edu/group5/app/model/DBRepository.java +++ b/src/main/java/edu/group5/app/model/DBRepository.java @@ -2,6 +2,9 @@ import java.util.HashMap; import java.util.Map; + +import edu.group5.app.utils.ParameterValidator; + import java.util.List; /** @@ -22,6 +25,7 @@ public abstract class DBRepository extends Repository { * @param content the HashMap used to store repository entities */ protected DBRepository(Map content) { + ParameterValidator.objectChecker(content, "Content"); super(content); this.contentLock = new HashMap<>(); } diff --git a/src/main/java/edu/group5/app/model/Repository.java b/src/main/java/edu/group5/app/model/Repository.java index 032f307..64d4b90 100644 --- a/src/main/java/edu/group5/app/model/Repository.java +++ b/src/main/java/edu/group5/app/model/Repository.java @@ -2,6 +2,8 @@ import java.util.Map; +import edu.group5.app.utils.ParameterValidator; + /** * Represents a repository. */ @@ -13,6 +15,7 @@ public abstract class Repository { * @param content the underlying data structure used to store entities */ protected Repository(Map content) { + ParameterValidator.objectChecker(content, "content"); this.content = content; } diff --git a/src/main/java/edu/group5/app/model/donation/DonationRepository.java b/src/main/java/edu/group5/app/model/donation/DonationRepository.java index a55ea74..f1102ba 100644 --- a/src/main/java/edu/group5/app/model/donation/DonationRepository.java +++ b/src/main/java/edu/group5/app/model/donation/DonationRepository.java @@ -1,6 +1,7 @@ package edu.group5.app.model.donation; import edu.group5.app.model.DBRepository; +import edu.group5.app.utils.ParameterValidator; import java.util.Comparator; import java.util.HashMap; @@ -32,9 +33,7 @@ public class DonationRepository extends DBRepository { */ public DonationRepository(List rows) { super(new HashMap<>()); - if (rows == null) { - throw new IllegalArgumentException("The list of rows cannot be null"); - } + ParameterValidator.objectChecker(rows, "List of donation rows"); this.content = new HashMap<>(); for (Object[] row : rows) { if (row == null || row.length != 6) { @@ -88,9 +87,7 @@ public List export() { * @throws IllegalArgumentException if the donationId is not positive */ public Donation getDonationById(int donationId) { - if (donationId <= 0) { - throw new IllegalArgumentException("Donation ID must be positive"); - } + ParameterValidator.intChecker(donationId, "Donation ID"); return content.get(donationId); } @@ -102,7 +99,7 @@ public Donation getDonationById(int donationId) { */ public int getNextDonationId() { return content.keySet().stream().max(Integer::compareTo).orElse(0) + 1; - } /* TODO change this when data database is introduced */ + } public Map getAllDonations() { return new HashMap<>(content); @@ -122,9 +119,7 @@ public Map getAllDonations() { */ @Override public boolean addContent(Donation donation) { - if (donation == null) { - throw new IllegalArgumentException("Donation cannot be null"); - } + ParameterValidator.objectChecker(donation, "Donation"); if (content.containsKey(donation.donationId())) { return false; } @@ -180,9 +175,7 @@ public HashMap sortByAmount() { * @throws IllegalArgumentException if the orgNumber is not positive */ public HashMap filterByOrganization(int orgNumber) { - if (orgNumber <= 0) { - throw new IllegalArgumentException("Organization number must be positive"); - } + ParameterValidator.intChecker(orgNumber, "Organization number"); return content.entrySet() .stream() .filter(entry -> entry.getValue().organizationId() == orgNumber) @@ -201,9 +194,7 @@ public HashMap filterByOrganization(int orgNumber) { * @throws IllegalArgumentException if the userId is not positive */ public HashMap filterByUser(int userId) { - if (userId <= 0) { - throw new IllegalArgumentException("User ID must be positive"); - } + ParameterValidator.intChecker(userId, "User ID"); return content.entrySet().stream() .filter(entry -> entry.getValue().userId() == userId) .collect(Collectors.toMap( diff --git a/src/main/java/edu/group5/app/model/donation/DonationService.java b/src/main/java/edu/group5/app/model/donation/DonationService.java index 690a632..6f978b1 100644 --- a/src/main/java/edu/group5/app/model/donation/DonationService.java +++ b/src/main/java/edu/group5/app/model/donation/DonationService.java @@ -1,11 +1,11 @@ package edu.group5.app.model.donation; import java.time.Instant; - import java.math.BigDecimal; import java.sql.Timestamp; import edu.group5.app.model.organization.Organization; import edu.group5.app.model.organization.OrganizationRepository; import edu.group5.app.model.user.Customer; +import edu.group5.app.utils.ParameterValidator; /** * DonationService class provides functionality for handling donations in the system. @@ -27,12 +27,8 @@ public class DonationService { */ public DonationService(DonationRepository donationRepository, OrganizationRepository organizationRepository) { - if (donationRepository == null) { - throw new IllegalArgumentException("DonationRepository cannot be null"); - } - if (organizationRepository == null) { - throw new IllegalArgumentException("OrganizationRepository cannot be null"); - } + ParameterValidator.objectChecker(donationRepository, "DonationRepository"); + ParameterValidator.objectChecker(organizationRepository, "OrganizationRepository"); this.donationRepository = donationRepository; this.organizationRepository = organizationRepository; } diff --git a/src/main/java/edu/group5/app/model/organization/Organization.java b/src/main/java/edu/group5/app/model/organization/Organization.java index 7016567..4c66be0 100644 --- a/src/main/java/edu/group5/app/model/organization/Organization.java +++ b/src/main/java/edu/group5/app/model/organization/Organization.java @@ -2,6 +2,8 @@ import java.util.Objects; +import edu.group5.app.utils.ParameterValidator; + /** * Represents an organization. * @@ -45,9 +47,7 @@ public record Organization( */ public Organization(int orgNumber, String name, boolean trusted, String websiteUrl, boolean isPreApproved, String description, String logoUrl) { - if (orgNumber < 0) { - throw new IllegalArgumentException("orgNumber cannot be negative"); - } + ParameterValidator.intChecker(orgNumber, "Organization number"); this.orgNumber = orgNumber; this.name = Objects.requireNonNull(name, "name cannot be null"); this.trusted = trusted; diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java index 61155b7..5da9a97 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java @@ -1,6 +1,7 @@ package edu.group5.app.model.organization; import edu.group5.app.model.Repository; +import edu.group5.app.utils.ParameterValidator; import tools.jackson.core.type.TypeReference; import tools.jackson.databind.ObjectMapper; @@ -30,12 +31,8 @@ public class OrganizationRepository extends Repository { public OrganizationRepository(Object[] input, OrganizationScraper scraper) { super(new HashMap<>()); this.grandMap = new HashMap<>(); - if (input == null) { - throw new IllegalArgumentException("The input cannot be null"); - } - if (scraper == null) { - throw new IllegalArgumentException("The scraper cannot be null"); - } + ParameterValidator.objectChecker(input, "Input data"); + ParameterValidator.objectChecker(scraper, "Scraper"); this.scraper = scraper; ObjectMapper mapper = new ObjectMapper(); @@ -44,6 +41,11 @@ public OrganizationRepository(Object[] input, OrganizationScraper scraper) { HashMap contentMap = mapper.convertValue(obj, new TypeReference>() {}); + Object orgNumberObj = contentMap.get("org_number"); + if (orgNumberObj == null) { + System.err.println("Skipping organization: missing org_number"); + continue; + } String orgNumberStr = ((String) contentMap.get("org_number")).replaceAll("\\s", ""); int orgNumber = Integer.parseInt(orgNumberStr); String name = (String) contentMap.get("name"); @@ -101,9 +103,7 @@ public Map getTrustedOrganizations() { * @throws IllegalArgumentException if the organization number is not a positive integer */ public Organization findByOrgNumber(int orgNumber) { - if (orgNumber <= 0) { - throw new IllegalArgumentException("The Organization number must be a positive integer"); - } + ParameterValidator.intChecker(orgNumber, "Organization number"); return grandMap.get(orgNumber); } @@ -114,9 +114,7 @@ public Organization findByOrgNumber(int orgNumber) { * @throws IllegalArgumentException if the name is null or empty */ public Organization findByOrgName(String name) { - if (name == null || name.isEmpty()) { - throw new IllegalArgumentException("The name cannot be null"); - } + ParameterValidator.stringChecker(name, "Organization name"); return grandMap.values().stream() .filter(org -> org.name().equalsIgnoreCase(name)) .findFirst() diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java index 6a8c230..fdeec82 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -3,13 +3,9 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; -import org.jsoup.nodes.TextNode; -import org.jsoup.select.Elements; - import java.util.stream.Collectors; - -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * Handles web scraping of organization information from Innsamlingskontrollen. @@ -17,8 +13,8 @@ * All results are cached to avoid redundant network requests. */ public class OrganizationScraper { - private final Map logoCache = new HashMap<>(); - private final Map descriptionCache = new HashMap<>(); + private final Map logoCache = new ConcurrentHashMap<>(); + private final Map descriptionCache = new ConcurrentHashMap<>(); /** * Fetches the description for the given URL by scraping all text content @@ -35,10 +31,6 @@ public class OrganizationScraper { * @return the description text, or null if not found or pageUrl is invalid */ public String fetchDescription(String pageUrl) { - if (pageUrl == null || pageUrl.isBlank()) { - return null; - } - if (descriptionCache.containsKey(pageUrl)) { return descriptionCache.get(pageUrl); } @@ -88,10 +80,6 @@ public String fetchDescription(String pageUrl) { * @return the absolute logo URL, or null if not found or pageUrl is invalid */ public String fetchLogoUrl(String pageUrl) { - if (pageUrl == null || pageUrl.isBlank()) { - return null; - } - if (logoCache.containsKey(pageUrl)) { return logoCache.get(pageUrl); } diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java index e19c273..acb1110 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -2,6 +2,9 @@ import java.util.stream.Collectors; +import edu.group5.app.utils.ParameterValidator; +import tools.jackson.databind.deser.bean.CreatorCandidate.Param; + import java.util.Comparator; import java.util.LinkedHashMap; import java.util.Map; @@ -27,12 +30,8 @@ public class OrganizationService { * @throws IllegalArgumentException if organizationRepository or scraper is null */ public OrganizationService(OrganizationRepository organizationRepository, OrganizationScraper scraper) { - if (organizationRepository == null) { - throw new IllegalArgumentException("OrganizationRepository cannot be null"); - } - if (scraper == null) { - throw new IllegalArgumentException("OrganizationScraper cannot be null"); - } + ParameterValidator.objectChecker(organizationRepository, "OrganizationRepository"); + ParameterValidator.objectChecker(scraper, "Scraper"); this.organizationRepository = organizationRepository; this.scraper = scraper; } @@ -71,6 +70,7 @@ public Organization findByOrgNumber(int orgNumber) { * @return the Organization if found, null otherwise */ public Organization findByOrgName(String name) { + ParameterValidator.stringChecker(name, "name"); return organizationRepository.findByOrgName(name); } diff --git a/src/main/java/edu/group5/app/model/user/Customer.java b/src/main/java/edu/group5/app/model/user/Customer.java index 93f03ac..d04139d 100644 --- a/src/main/java/edu/group5/app/model/user/Customer.java +++ b/src/main/java/edu/group5/app/model/user/Customer.java @@ -1,6 +1,9 @@ package edu.group5.app.model.user; import java.util.List; + +import edu.group5.app.utils.ParameterValidator; + import java.util.ArrayList; /** @@ -38,9 +41,7 @@ public String getRole() { } public void addPreference(int orgNumber) { - if (orgNumber <= 0) { - throw new IllegalArgumentException("Organization number must be a positive integer"); - } + ParameterValidator.intChecker(orgNumber,"Organization number"); if (preferences.contains(orgNumber)) { throw new IllegalArgumentException("Organization number already in preferences"); } @@ -48,9 +49,7 @@ public void addPreference(int orgNumber) { } public void removePreference(int orgNumber) { - if (orgNumber <= 0) { - throw new IllegalArgumentException("Organization number must be a positive integer"); - } + ParameterValidator.intChecker(orgNumber, "Organization number"); if (!preferences.contains(orgNumber)) { throw new IllegalArgumentException("Organization number not found in preferences"); } diff --git a/src/main/java/edu/group5/app/model/user/User.java b/src/main/java/edu/group5/app/model/user/User.java index 411538d..54c9e16 100644 --- a/src/main/java/edu/group5/app/model/user/User.java +++ b/src/main/java/edu/group5/app/model/user/User.java @@ -1,5 +1,9 @@ package edu.group5.app.model.user; +import java.lang.reflect.Parameter; + import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import edu.group5.app.utils.ParameterValidator; /** * User class represents a user in the system. It is an abstract class that will be extended by specific user types such as Donor, Recipient, and Admin. * Each user has a unique userId, a role that defines their permissions in the system, and personal information such as first name, last name, email, and password hash. @@ -27,21 +31,12 @@ public abstract class User { */ public User(int userId, String firstName, String lastName, String email, String passwordHash) { - if (userId <= 0) { - throw new IllegalArgumentException("User ID must be positive"); - } - if (firstName == null || firstName.trim().isEmpty()) { - throw new IllegalArgumentException("First name cannot be null or empty"); - } - if (lastName == null || lastName.trim().isEmpty()) { - throw new IllegalArgumentException("Last name cannot be null or empty"); - } - if (email == null || email.trim().isEmpty()) { - throw new IllegalArgumentException("Email cannot be null or empty"); - } - if (passwordHash == null || passwordHash.trim().isEmpty()) { - throw new IllegalArgumentException("Password hash cannot be null or empty"); - } + ParameterValidator.intChecker(userId, "User ID"); + ParameterValidator.stringChecker(firstName, "First name"); + ParameterValidator.stringChecker(lastName, "Last name"); + ParameterValidator.stringChecker(email, "Email"); + ParameterValidator.stringChecker(passwordHash, "Password hash"); + this.userId = userId; this.firstName = firstName.trim(); this.lastName = lastName.trim(); diff --git a/src/main/java/edu/group5/app/model/user/UserRepository.java b/src/main/java/edu/group5/app/model/user/UserRepository.java index 193f433..07f9ca8 100644 --- a/src/main/java/edu/group5/app/model/user/UserRepository.java +++ b/src/main/java/edu/group5/app/model/user/UserRepository.java @@ -5,6 +5,7 @@ import java.util.Map; import edu.group5.app.model.DBRepository; +import edu.group5.app.utils.ParameterValidator; public class UserRepository extends DBRepository { public final static String ROLE_CUSTOMER = "Customer"; @@ -18,9 +19,7 @@ public class UserRepository extends DBRepository { */ public UserRepository(List rows) { super(new HashMap<>()); - if (rows == null) { - throw new IllegalArgumentException("The list of rows cannot be null"); - } + ParameterValidator.objectChecker(rows, "List of User rows"); for (Object[] row : rows) { if (row == null || row.length != 6) { throw new IllegalArgumentException("Each row must contain exactly 6 elements"); @@ -83,9 +82,7 @@ public HashMap getUsers() { * @throws IllegalArgumentException if the userId is not positive */ public User getUserById(int userId) { - if (userId <= 0) { - throw new IllegalArgumentException("User ID must be positive"); - } + ParameterValidator.intChecker(userId, "User ID"); return content.get(userId); } @@ -104,7 +101,7 @@ public int getNextUserId() { () -> new IllegalStateException("No keys found")); int nextId = maxKey + 1; return nextId; - } /* TODO change this when data database is introduced */ + } /** * Adds a new user to the repository @@ -120,9 +117,7 @@ public int getNextUserId() { */ @Override public boolean addContent(User user) { - if (user == null) { - throw new IllegalArgumentException("User cannot be null"); - } + ParameterValidator.objectChecker(user, "User"); if (content.containsKey(user.getUserId())) { return false; } @@ -138,9 +133,7 @@ public boolean addContent(User user) { * user exists */ public User findUserByEmail(String email) { - if (email == null || email.trim().isEmpty()) { - throw new IllegalArgumentException("Email cannot be null or empty"); - } + ParameterValidator.stringChecker(email, "Email"); return content.values().stream() .filter(user -> user.getEmail().equals(email)) .findFirst() diff --git a/src/main/java/edu/group5/app/model/user/UserService.java b/src/main/java/edu/group5/app/model/user/UserService.java index b237eae..a6d316e 100644 --- a/src/main/java/edu/group5/app/model/user/UserService.java +++ b/src/main/java/edu/group5/app/model/user/UserService.java @@ -1,5 +1,7 @@ package edu.group5.app.model.user; +import edu.group5.app.utils.ParameterValidator; + /** * Service class for managing user-related operations, such as registration and * login. @@ -19,9 +21,7 @@ public class UserService { * @throws IllegalArgumentException if userRepository is null */ public UserService(UserRepository userRepository) { - if (userRepository == null) { - throw new IllegalArgumentException("UserRepository cannot be null"); - } + ParameterValidator.objectChecker(userRepository, "UserRepository"); this.userRepository = userRepository; } @@ -106,9 +106,7 @@ public User login(String email, char[] password) { * @return the User object if found, null otherwise */ public User getUserByEmail(String email) { - if (email == null || email.trim().isEmpty()) { - return null; - } + ParameterValidator.stringChecker(email, "email"); return this.userRepository.findUserByEmail(email); } } diff --git a/src/main/java/edu/group5/app/utils/ParameterValidator.java b/src/main/java/edu/group5/app/utils/ParameterValidator.java index 3bc3e5a..28a8dc0 100644 --- a/src/main/java/edu/group5/app/utils/ParameterValidator.java +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -24,7 +24,7 @@ public final class ParameterValidator { public static final void stringChecker(String stringArg, String variableName) throws IllegalArgumentException { nullCheck(stringArg, variableName); - if (stringArg.isBlank()) { + if (stringArg.trim().isBlank()) { throw new IllegalArgumentException(String.format("%s can't be blank", variableName)); } } @@ -104,7 +104,7 @@ public static final void exportChecker( if (!data.isEmpty()) { if (data.stream().anyMatch(i -> i.length != rowLength)) { throw new IllegalArgumentException( - String.format("%s's arrays must have a length of 6", dataName) + String.format("%s's arrays must have a length of %d", dataName, rowLength) ); } if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 7db3ee9..346df19 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -1,6 +1,7 @@ package edu.group5.app.view.causespage; import edu.group5.app.model.organization.Organization; +import edu.group5.app.utils.ParameterValidator; import edu.group5.app.control.NavigationController; import edu.group5.app.control.OrganizationController; import edu.group5.app.model.AppState; @@ -41,6 +42,10 @@ public class CausesPageView extends BorderPane { private Map allOrganizations; public CausesPageView(AppState appState, NavigationController nav, OrganizationController orgController) { + ParameterValidator.objectChecker(appState, "AppState"); + ParameterValidator.objectChecker(nav, "NavigationController"); + ParameterValidator.objectChecker(orgController, "OrganizationController"); + this.appState = appState; this.nav = nav; this.orgController = orgController; diff --git a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java index 2ca30bb..c24b32a 100644 --- a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java +++ b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java @@ -3,6 +3,7 @@ import edu.group5.app.control.NavigationController; import edu.group5.app.model.AppState; import edu.group5.app.model.organization.Organization; +import edu.group5.app.utils.ParameterValidator; import javafx.geometry.Pos; import javafx.scene.image.Image; import javafx.scene.image.ImageView; diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 9444421..61ca29c 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -3,6 +3,7 @@ import edu.group5.app.control.DonationController; import edu.group5.app.control.NavigationController; import edu.group5.app.model.AppState; +import edu.group5.app.utils.ParameterValidator; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.TextField; @@ -37,6 +38,10 @@ public class DonationPageView extends BorderPane { private Button donateBtn; public DonationPageView(AppState appState, NavigationController nav, DonationController donationController) { + ParameterValidator.objectChecker(appState, "AppState"); + ParameterValidator.objectChecker(nav, "NavigationController"); + ParameterValidator.objectChecker(donationController, "DonationController"); + this.appState = appState; this.nav = nav; this.donationController = donationController; From 996e43ecba33fd11993c2405702a0ba4929d9ceb Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 16 Apr 2026 14:02:36 +0200 Subject: [PATCH 60/98] update[]: add more JavaDoc to the rest of th ecodebase --- .../group5/app/control/AuthController.java | 3 -- .../app/control/NavigationController.java | 43 ++++++++++++++++++- .../app/control/OrganizationController.java | 9 +++- .../java/edu/group5/app/model/AppState.java | 43 ++++++++++++++++++- .../edu/group5/app/model/DBRepository.java | 11 +++++ .../java/edu/group5/app/model/Repository.java | 11 +++-- .../organization/OrganizationService.java | 1 - .../OrganizationPageView.java | 1 - 8 files changed, 111 insertions(+), 11 deletions(-) diff --git a/src/main/java/edu/group5/app/control/AuthController.java b/src/main/java/edu/group5/app/control/AuthController.java index 4911a3b..291e0df 100644 --- a/src/main/java/edu/group5/app/control/AuthController.java +++ b/src/main/java/edu/group5/app/control/AuthController.java @@ -7,11 +7,8 @@ import edu.group5.app.view.loginpage.LoginPageView; import edu.group5.app.view.loginpage.SignUpPageView; import javafx.scene.control.Alert; -import javafx.scene.control.Button; import javafx.scene.control.ButtonType; -import java.util.Arrays; - import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index 326389b..d94bb53 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -18,7 +18,14 @@ import javafx.scene.layout.BorderPane; /** - * Controller responsible for navigating between views within the root node. + * Controller responsible for handling navigation between different pages of the application. + * Coordinates between {@link AppState}, {@link AuthController}, {@link DonationController}, + * {@link OrganizationController} and the various views to manage page transitions and state updates. + *

    + * Provides methods to navigate to the home page, login page, sign-up page, causes page, + * organization page, donation page, payment complete page and user profile page. Each method updates the + * top header and center content of the main application layout accordingly. + *

    */ public class NavigationController { private final BorderPane root; @@ -50,36 +57,65 @@ public NavigationController(BorderPane root, AppState appState, UserService user this.organizationController = new OrganizationController(organizationService); } + /** + * Navigates to the home page by setting the top header and center content of the main layout. + * The home page serves as the landing page of the application, providing an overview and access to various features. + */ public void showHomePage() { root.setTop(header); root.setCenter(new HomePageView(appState, this)); } + /** + * Navigates to the login page by setting the top header and center content of the main layout. + * The login page allows existing users to enter their credentials and access their account. + */ public void showLoginPage() { root.setTop(loginHeader); root.setCenter(new LoginPageView(appState, this, authController)); } + /** + * Navigates to the sign-up page by setting the top header and center content of the main layout. + * The sign-up page allows new users to create an account by providing their details. + */ public void showSignUpPage() { root.setTop(loginHeader); root.setCenter(new SignUpPageView(appState, this, authController)); } + /** + * Navigates to the payment complete page by setting the top header and center content of the main layout. + * The payment complete page confirms the successful completion of a donation transaction. + */ public void showPaymentCompletePage() { root.setTop(header); root.setCenter(new PaymentCompletePageView(this)); } + /** + * Navigates to the causes page by setting the top header and center content of the main layout. + * The causes page allows users to browse and search for organizations they may want to donate to. + */ public void showCausesPage() { root.setTop(header); root.setCenter(new CausesPageView(appState, this, organizationController)); } + /** + * Navigates to the organization page by setting the top header and center content of the main layout. + * The organization page provides detailed information about a specific organization, including its mission, + * impact, and donation options, allowing users to learn more before making a donation. + */ public void showOrganizationPage() { root.setTop(header); root.setCenter(new OrganizationPageView(appState, this, donationController)); } + /** + * Navigates to the donation page by setting the top header and center content of the main layout. + * The donation page allows users to make new donations to selected organizations. + */ public void showDonationPage() { root.setTop(header); root.setCenter(new DonationPageView(appState, this, donationController)); @@ -89,6 +125,11 @@ public void showAboutUsPage() { root.setTop(header); } + /** + * Navigates to the user profile page by setting the top header and center content of the main layout. + * The user profile page allows users to view and manage their account information, + * donation history, and other personalized features. + */ public void showUserPage() { root.setTop(header); root.setCenter(new UserPageView(appState, this, authController, donationController, organizationController)); diff --git a/src/main/java/edu/group5/app/control/OrganizationController.java b/src/main/java/edu/group5/app/control/OrganizationController.java index fc1252f..064aab2 100644 --- a/src/main/java/edu/group5/app/control/OrganizationController.java +++ b/src/main/java/edu/group5/app/control/OrganizationController.java @@ -3,13 +3,20 @@ import edu.group5.app.model.organization.Organization; import edu.group5.app.model.organization.OrganizationService; import edu.group5.app.utils.ParameterValidator; -import tools.jackson.databind.deser.bean.CreatorCandidate.Param; import java.util.Map; import java.util.concurrent.CompletableFuture; /** * Controller responsible for organization-related operations. + * + *

    + * Coordinates with {@link OrganizationService} to: + *

      + *
    • Retrieve organization information by ID
    • + *
    • Retrieve all trusted organizations
    • + *
    + *

    */ public class OrganizationController { private final OrganizationService service; diff --git a/src/main/java/edu/group5/app/model/AppState.java b/src/main/java/edu/group5/app/model/AppState.java index 73d5cfc..bd56ba0 100644 --- a/src/main/java/edu/group5/app/model/AppState.java +++ b/src/main/java/edu/group5/app/model/AppState.java @@ -4,41 +4,82 @@ import edu.group5.app.model.user.User; import java.math.BigDecimal; - +/** + * AppState class represents the current state of the application, including: + *
  • the current user
  • + *
  • the selected organization
  • + *
  • current donation amount
  • + *
  • current payment method
  • + *
    + * It serves as a centralized data store for the application's + * stateful information, allowing different components of the application to access and modify this information as needed. + */ public class AppState { private User currentUser; private BigDecimal currentDonationAmount; private Organization currentOrganization; private String currentDonation; + /** + * Gets the current user of the application. + * @return the current user + */ public User getCurrentUser() { return this.currentUser; } + /** + * Sets the current user of the application. + * @param user the user to set as the current user + */ public void setCurrentUser(User user) { this.currentUser = user; } + /** + * Gets the selected organization. + * @return the selected organization + */ public Organization getCurrentOrganization() { return this.currentOrganization; } + /** + * Sets the selected organization. + * @param organization the organization to set as the current organization + */ public void setCurrentOrganization(Organization organization) { this.currentOrganization = organization; } + /** + * Gets the current donation amount. + * @return the current donation amount + */ public BigDecimal getCurrentDonationAmount() { return this.currentDonationAmount; } + /** + * Sets the current donation amount. + * @param amount the amount to set as the current donation amount + */ public void setCurrentDonationAmount(BigDecimal amount) { this.currentDonationAmount = amount; } + /** + * Gets the current payment method. + * @return the current payment method + */ public String getCurrentPaymentMethod() { return this.currentDonation; } + /** + * Sets the current payment method. + * @param paymentMethod the payment method to set as the current payment method + */ public void setCurrentPaymentMethod(String paymentMethod){ this.currentDonation = paymentMethod; } diff --git a/src/main/java/edu/group5/app/model/DBRepository.java b/src/main/java/edu/group5/app/model/DBRepository.java index 88ac3d9..d36d0e2 100644 --- a/src/main/java/edu/group5/app/model/DBRepository.java +++ b/src/main/java/edu/group5/app/model/DBRepository.java @@ -30,8 +30,19 @@ protected DBRepository(Map content) { this.contentLock = new HashMap<>(); } + /** + * Updates the content lock with the current state of the content. + * This method should be called whenever the content is modified to ensure + * that the content lock reflects the latest state of the repository. + */ protected abstract void updateContentLock(); + /** + * Adds a new entity to the repository content. + * @param value the entity to be added to the repository + * @return true if the entity was successfully added, false otherwise + * @throws IllegalArgumentException if the value is null + */ public abstract boolean addContent(V value); /** diff --git a/src/main/java/edu/group5/app/model/Repository.java b/src/main/java/edu/group5/app/model/Repository.java index 64d4b90..6dd425e 100644 --- a/src/main/java/edu/group5/app/model/Repository.java +++ b/src/main/java/edu/group5/app/model/Repository.java @@ -5,7 +5,12 @@ import edu.group5.app.utils.ParameterValidator; /** - * Represents a repository. + * Abstract base class for repositories. + *

    + * Repositories are responsible for managing collections of entities, providing + * basic operations for accessing and manipulating these entities. The specific + * type of entities and the underlying data structure are defined by subclasses. + *

    */ public abstract class Repository { protected final Map content; @@ -20,8 +25,8 @@ protected Repository(Map content) { } /** - * Gets the content of the repo - * @return content of the repo + * Gets the content of the repository. + * @return the content of the repository */ public Map getContent() { return content; diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java index acb1110..d0c84a3 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -3,7 +3,6 @@ import java.util.stream.Collectors; import edu.group5.app.utils.ParameterValidator; -import tools.jackson.databind.deser.bean.CreatorCandidate.Param; import java.util.Comparator; import java.util.LinkedHashMap; diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 53c623b..2268d4d 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -9,7 +9,6 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; -import javafx.scene.control.TextArea; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; From c67227b549712138cbaf56e2c00e91c22c336cb8 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 16 Apr 2026 17:39:39 +0200 Subject: [PATCH 61/98] update[control]: Update controller to handle control of the appstate --- .../group5/app/control/AuthController.java | 18 +++++++++++ .../app/control/DonationController.java | 32 +++++++++++++++++++ .../app/control/NavigationController.java | 2 +- .../app/control/OrganizationController.java | 23 ++++++++++++- 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/group5/app/control/AuthController.java b/src/main/java/edu/group5/app/control/AuthController.java index 291e0df..c2f923b 100644 --- a/src/main/java/edu/group5/app/control/AuthController.java +++ b/src/main/java/edu/group5/app/control/AuthController.java @@ -1,6 +1,7 @@ package edu.group5.app.control; import edu.group5.app.model.AppState; +import edu.group5.app.model.organization.Organization; import edu.group5.app.model.user.User; import edu.group5.app.model.user.UserService; import edu.group5.app.utils.ParameterValidator; @@ -37,6 +38,23 @@ public AuthController(AppState appState, NavigationController nav, UserService u this.userService = userService; } + + /** + * Sets the current logged-in user. + * @param user the user to set as current + */ + public void setCurrentUser(User user) { + appState.setCurrentUser(user); + } + + /** + * Gets the current logged-in user. + * @return the current user, or null if no user logged in + */ + public User getCurrentUser() { + return appState.getCurrentUser(); + } + /** * Handles the registration of a {@link User}. * diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 61ab172..70d6444 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -43,6 +43,38 @@ public DonationController(AppState appState, NavigationController nav, DonationS this.service = service; } + /** + * Sets the current donation amount. + * @param amount the amount to donate + */ + public void setDonationAmount(BigDecimal amount) { + appState.setCurrentDonationAmount(amount); + } + + /** + * Gets the current donation amount. + * @return the donation amount + */ + public BigDecimal getDonationAmount() { + return appState.getCurrentDonationAmount(); + } + + /** + * Sets the current payment method. + * @param paymentMethod the payment method + */ + public void setPaymentMethod(String paymentMethod) { + appState.setCurrentPaymentMethod(paymentMethod); + } + + /** + * Gets the current payment method. + * @return the payment method + */ + public String getPaymentMethod() { + return appState.getCurrentPaymentMethod(); + } + /** * Retrieves all donations made by a specific user. * diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index d94bb53..04fb5e2 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -54,7 +54,7 @@ public NavigationController(BorderPane root, AppState appState, UserService user this.authController = new AuthController(appState, this, userService); this.donationController = new DonationController(appState, this, donationService); - this.organizationController = new OrganizationController(organizationService); + this.organizationController = new OrganizationController(appState, organizationService); } /** diff --git a/src/main/java/edu/group5/app/control/OrganizationController.java b/src/main/java/edu/group5/app/control/OrganizationController.java index 064aab2..6ffe830 100644 --- a/src/main/java/edu/group5/app/control/OrganizationController.java +++ b/src/main/java/edu/group5/app/control/OrganizationController.java @@ -1,5 +1,6 @@ package edu.group5.app.control; +import edu.group5.app.model.AppState; import edu.group5.app.model.organization.Organization; import edu.group5.app.model.organization.OrganizationService; import edu.group5.app.utils.ParameterValidator; @@ -19,13 +20,33 @@ *

    */ public class OrganizationController { + private final AppState appState; private final OrganizationService service; - public OrganizationController(OrganizationService service) { + public OrganizationController(AppState appState, OrganizationService service) { + ParameterValidator.objectChecker(appState, "AppState"); ParameterValidator.objectChecker(service, "OrganizationService"); + this.appState = appState; this.service = service; } + + /** + * Sets the current selected organization. + * @param organization the organization to set as current + */ + public void setCurrentOrganization(Organization org) { + appState.setCurrentOrganization(org); + } + + /** + * Gets the current selected organization. + * @return the current organization, or null if none selected + */ + public Organization getCurrentOrganization() { + return appState.getCurrentOrganization(); + } + public Organization getOrganizationById(int orgId) { ParameterValidator.intChecker(orgId, "Organization ID"); return service.findByOrgNumber(orgId); From 92b41fd180d67c8c1d6adc6bd82ff34100130a1f Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 16 Apr 2026 18:28:56 +0200 Subject: [PATCH 62/98] refactor: implement proper MVC architecture with controller-service abstraction --- .../group5/app/control/AuthController.java | 1 - .../app/control/DonationController.java | 2 +- .../app/control/NavigationController.java | 12 +++++------ .../app/model/donation/DonationService.java | 21 ++++++++++--------- .../organization/OrganizationService.java | 8 ------- .../java/edu/group5/app/model/user/User.java | 1 - .../group5/app/model/user/UserService.java | 11 ---------- .../app/view/causespage/CausesPageView.java | 11 ++++------ .../app/view/causespage/OrganizationCard.java | 12 +++++------ .../view/donationpage/DonationPageView.java | 10 +++------ .../app/view/homepage/HomePageView.java | 5 +---- .../app/view/loginpage/LoginPageView.java | 5 +---- .../app/view/loginpage/SignUpPageView.java | 5 +---- .../OrganizationPageView.java | 19 ++++++++++------- .../app/view/userpage/UserPageView.java | 11 +++++----- .../model/donation/DonationServiceTest.java | 10 --------- .../organization/OrganizationServiceTest.java | 5 ----- .../app/model/user/UserServiceTest.java | 5 ----- 18 files changed, 51 insertions(+), 103 deletions(-) diff --git a/src/main/java/edu/group5/app/control/AuthController.java b/src/main/java/edu/group5/app/control/AuthController.java index c2f923b..7c34afd 100644 --- a/src/main/java/edu/group5/app/control/AuthController.java +++ b/src/main/java/edu/group5/app/control/AuthController.java @@ -1,7 +1,6 @@ package edu.group5.app.control; import edu.group5.app.model.AppState; -import edu.group5.app.model.organization.Organization; import edu.group5.app.model.user.User; import edu.group5.app.model.user.UserService; import edu.group5.app.utils.ParameterValidator; diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 70d6444..07af6fd 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -83,7 +83,7 @@ public String getPaymentMethod() { */ public Map getUserDonations(int userId) { ParameterValidator.intChecker(userId, "User ID"); - return service.getDonationRepository().filterByUser(userId); + return service.getUserDonations(userId); } /** diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index 04fb5e2..92ea26a 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -63,7 +63,7 @@ public NavigationController(BorderPane root, AppState appState, UserService user */ public void showHomePage() { root.setTop(header); - root.setCenter(new HomePageView(appState, this)); + root.setCenter(new HomePageView(this)); } /** @@ -72,7 +72,7 @@ public void showHomePage() { */ public void showLoginPage() { root.setTop(loginHeader); - root.setCenter(new LoginPageView(appState, this, authController)); + root.setCenter(new LoginPageView(this, authController)); } /** @@ -81,7 +81,7 @@ public void showLoginPage() { */ public void showSignUpPage() { root.setTop(loginHeader); - root.setCenter(new SignUpPageView(appState, this, authController)); + root.setCenter(new SignUpPageView(this, authController)); } /** @@ -99,7 +99,7 @@ public void showPaymentCompletePage() { */ public void showCausesPage() { root.setTop(header); - root.setCenter(new CausesPageView(appState, this, organizationController)); + root.setCenter(new CausesPageView(this, organizationController)); } /** @@ -109,7 +109,7 @@ public void showCausesPage() { */ public void showOrganizationPage() { root.setTop(header); - root.setCenter(new OrganizationPageView(appState, this, donationController)); + root.setCenter(new OrganizationPageView(this, organizationController, donationController)); } /** @@ -118,7 +118,7 @@ public void showOrganizationPage() { */ public void showDonationPage() { root.setTop(header); - root.setCenter(new DonationPageView(appState, this, donationController)); + root.setCenter(new DonationPageView(this, donationController)); } public void showAboutUsPage() { diff --git a/src/main/java/edu/group5/app/model/donation/DonationService.java b/src/main/java/edu/group5/app/model/donation/DonationService.java index 6f978b1..5eb964b 100644 --- a/src/main/java/edu/group5/app/model/donation/DonationService.java +++ b/src/main/java/edu/group5/app/model/donation/DonationService.java @@ -1,5 +1,6 @@ package edu.group5.app.model.donation; import java.time.Instant; +import java.util.Map; import java.math.BigDecimal; import java.sql.Timestamp; import edu.group5.app.model.organization.Organization; @@ -34,21 +35,21 @@ public DonationService(DonationRepository donationRepository, } /** - * Getter for the DonationRepository used by this service. - * This method allows access to the donation repository for managing donation records and retrieving donation information. - * @return the DonationRepository instance used by this service + * Gets all donations made by a specific user. + * @param userId the ID of the user + * @return a map of donations made by that user */ - public DonationRepository getDonationRepository() { - return this.donationRepository; + public Map getUserDonations(int userId) { + return donationRepository.filterByUser(userId); } /** - * Getter for the OrganizationRepository used by this service. - * This method allows access to the organization repository for validating organization information when processing donations. - * @return the OrganizationRepository instance used by this service + * Gets all donations to a specific organization. + * @param organizationId the organization ID + * @return map of donations to that organization */ - public OrganizationRepository getOrganizationRepository() { - return this.organizationRepository; + public Map getOrganizationDonations(int organizationId) { + return donationRepository.filterByOrganization(organizationId); } /** diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java index d0c84a3..df4d04e 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -35,14 +35,6 @@ public OrganizationService(OrganizationRepository organizationRepository, Organi this.scraper = scraper; } - /** - * Getter for the OrganizationRepository used by this service. - * @return the OrganizationRepository instance used by this service - */ - public OrganizationRepository getOrganizationRepository() { - return this.organizationRepository; - } - /** * Retrieves all trusted organizations. * @return a map of trusted organizations by organization number diff --git a/src/main/java/edu/group5/app/model/user/User.java b/src/main/java/edu/group5/app/model/user/User.java index 54c9e16..806f1f7 100644 --- a/src/main/java/edu/group5/app/model/user/User.java +++ b/src/main/java/edu/group5/app/model/user/User.java @@ -1,5 +1,4 @@ package edu.group5.app.model.user; -import java.lang.reflect.Parameter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; diff --git a/src/main/java/edu/group5/app/model/user/UserService.java b/src/main/java/edu/group5/app/model/user/UserService.java index a6d316e..9d215d0 100644 --- a/src/main/java/edu/group5/app/model/user/UserService.java +++ b/src/main/java/edu/group5/app/model/user/UserService.java @@ -25,17 +25,6 @@ public UserService(UserRepository userRepository) { this.userRepository = userRepository; } - /** - * Getter for the UserRepository used by this service. - * This method allows access to the user repository for managing user data and - * performing operations such as registration and login. - * - * @return the UserRepository instance used by this service - */ - public UserRepository getUserRepository() { - return this.userRepository; - } - /** * Registers a new user with the given information. Validates the input data and * creates a new User object diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 346df19..271bfa9 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -34,19 +34,16 @@ * a fallback "no image" is displayed.

    */ public class CausesPageView extends BorderPane { - private final AppState appState; private final NavigationController nav; private final OrganizationController orgController; private GridPane organizationGrid; private Map allOrganizations; - public CausesPageView(AppState appState, NavigationController nav, OrganizationController orgController) { - ParameterValidator.objectChecker(appState, "AppState"); + public CausesPageView(NavigationController nav, OrganizationController orgController) { ParameterValidator.objectChecker(nav, "NavigationController"); ParameterValidator.objectChecker(orgController, "OrganizationController"); - this.appState = appState; this.nav = nav; this.orgController = orgController; @@ -94,9 +91,9 @@ private BorderPane createBody() { card.updateLogo(entry.getValue().logoUrl()); } } - Organization currentOrg = appState.getCurrentOrganization(); + Organization currentOrg = orgController.getCurrentOrganization(); if (currentOrg != null && orgs.containsKey(currentOrg.orgNumber())) { - appState.setCurrentOrganization(orgs.get(currentOrg.orgNumber())); + orgController.setCurrentOrganization(orgs.get(currentOrg.orgNumber())); } }); }); @@ -165,7 +162,7 @@ private GridPane createOrganizationSection(String searchTerm) { ? org.logoUrl() : null; - OrganizationCard card = new OrganizationCard(appState, nav, org, img); + OrganizationCard card = new OrganizationCard(nav, orgController, org, img); grid.add(card, column, row); diff --git a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java index c24b32a..4645d0b 100644 --- a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java +++ b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java @@ -1,9 +1,8 @@ package edu.group5.app.view.causespage; import edu.group5.app.control.NavigationController; -import edu.group5.app.model.AppState; +import edu.group5.app.control.OrganizationController; import edu.group5.app.model.organization.Organization; -import edu.group5.app.utils.ParameterValidator; import javafx.geometry.Pos; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -22,15 +21,16 @@ * to the organization's detail page.

    */ public class OrganizationCard extends VBox { - private final AppState appState; private final Organization organization; private final NavigationController nav; + private final OrganizationController organizationController; private StackPane imageContainer; private String currentLogoUrl; - public OrganizationCard(AppState appstate, NavigationController nav, Organization org, String img) { - this.appState = appstate; + public OrganizationCard(NavigationController nav, OrganizationController organizationController, + Organization org, String img) { this.nav = nav; + this.organizationController = organizationController; this.organization = org; setId("mainContainer"); getStylesheets().add(getClass().getResource("/browsepage/browse_org.css").toExternalForm()); @@ -43,7 +43,7 @@ public OrganizationCard(AppState appstate, NavigationController nav, Organizatio ); setOnMouseClicked(e -> { - appstate.setCurrentOrganization(getOrganizationWithCurrentLogo()); + organizationController.setCurrentOrganization(getOrganizationWithCurrentLogo()); nav.showOrganizationPage(); }); diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 61ca29c..ed0288c 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -2,7 +2,6 @@ import edu.group5.app.control.DonationController; import edu.group5.app.control.NavigationController; -import edu.group5.app.model.AppState; import edu.group5.app.utils.ParameterValidator; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -28,7 +27,6 @@ * payment method buttons, donation button, and a back to organization page button.

    */ public class DonationPageView extends BorderPane { - private final AppState appState; private final NavigationController nav; private final DonationController donationController; @@ -37,12 +35,10 @@ public class DonationPageView extends BorderPane { private Node selectedPaymentMethod = null; private Button donateBtn; - public DonationPageView(AppState appState, NavigationController nav, DonationController donationController) { - ParameterValidator.objectChecker(appState, "AppState"); + public DonationPageView(NavigationController nav, DonationController donationController) { ParameterValidator.objectChecker(nav, "NavigationController"); ParameterValidator.objectChecker(donationController, "DonationController"); - this.appState = appState; this.nav = nav; this.donationController = donationController; @@ -219,7 +215,7 @@ private void selectPaymentMethod(Node element) { selectedPaymentMethod.getStyleClass().add("payment-method-selected"); String paymentMethod = (String) element.getUserData(); - appState.setCurrentPaymentMethod(paymentMethod); + donationController.setPaymentMethod(paymentMethod); updateDonationButtonState(); } @@ -243,7 +239,7 @@ private void clearSelection() { } private void updateDonationAmount(BigDecimal amount) { - appState.setCurrentDonationAmount(amount); + donationController.setDonationAmount(amount); } private BigDecimal parseAmount(String amountStr) { diff --git a/src/main/java/edu/group5/app/view/homepage/HomePageView.java b/src/main/java/edu/group5/app/view/homepage/HomePageView.java index c24b89b..137e0b1 100644 --- a/src/main/java/edu/group5/app/view/homepage/HomePageView.java +++ b/src/main/java/edu/group5/app/view/homepage/HomePageView.java @@ -1,7 +1,6 @@ package edu.group5.app.view.homepage; import edu.group5.app.control.NavigationController; -import edu.group5.app.model.AppState; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.ScrollPane; @@ -19,11 +18,9 @@ * and an about us button. The page also has a charity image at the bottom.

    */ public class HomePageView extends BorderPane { - private final AppState appState; private final NavigationController nav; - public HomePageView(AppState appState, NavigationController nav) { - this.appState = appState; + public HomePageView(NavigationController nav) { this.nav = nav; getStylesheets().add(getClass().getResource("/homepage/homepage.css").toExternalForm()); diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java index 11b3ba7..927a8af 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java @@ -2,7 +2,6 @@ import edu.group5.app.control.NavigationController; import edu.group5.app.control.AuthController; -import edu.group5.app.model.AppState; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -21,7 +20,6 @@ * a login box, an email box, a login button, and a register button.

    */ public class LoginPageView extends BorderPane { - private final AppState appState; private final NavigationController nav; private final AuthController authController; @@ -29,8 +27,7 @@ public class LoginPageView extends BorderPane { private PasswordField passwordField; private Label errorLabel; - public LoginPageView(AppState appState, NavigationController nav, AuthController authController) { - this.appState = appState; + public LoginPageView(NavigationController nav, AuthController authController) { this.nav = nav; this.authController = authController; diff --git a/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java b/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java index 8638a1f..bdcfb54 100644 --- a/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java @@ -2,7 +2,6 @@ import edu.group5.app.control.NavigationController; import edu.group5.app.control.AuthController; -import edu.group5.app.model.AppState; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -22,7 +21,6 @@ * a password field, a sign up button, and an back to login button.

    */ public class SignUpPageView extends BorderPane { - private final AppState appState; private final NavigationController nav; private final AuthController authController; @@ -32,8 +30,7 @@ public class SignUpPageView extends BorderPane { private PasswordField passwordField; private Label errorLabel; - public SignUpPageView(AppState appState, NavigationController nav, AuthController authController) { - this.appState = appState; + public SignUpPageView(NavigationController nav, AuthController authController) { this.nav = nav; this.authController = authController; diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 2268d4d..3bea2ff 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -2,7 +2,7 @@ import edu.group5.app.control.DonationController; import edu.group5.app.control.NavigationController; -import edu.group5.app.model.AppState; +import edu.group5.app.control.OrganizationController; import edu.group5.app.model.organization.Organization; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -29,16 +29,21 @@ * donation page, and a back button to return to the causes page.

    */ public class OrganizationPageView extends BorderPane { - private final AppState appState; private final NavigationController nav; + private final OrganizationController organizationController; private final DonationController donationController; - public OrganizationPageView(AppState appState, NavigationController nav, DonationController donationController) { - this.appState = appState; + public OrganizationPageView(NavigationController nav, + OrganizationController organizationController, + DonationController donationController) { this.nav = nav; + this.organizationController = organizationController; this.donationController = donationController; - getStylesheets().add(Objects.requireNonNull(getClass().getResource("/organizationpage/organizationpage.css")).toExternalForm()); + getStylesheets().add(Objects + .requireNonNull(getClass() + .getResource("/organizationpage/organizationpage.css")) + .toExternalForm()); VBox content = new VBox(); content.getChildren().addAll(createBackButton(), createBody()); @@ -84,7 +89,7 @@ private StackPane createImageContainer() { imageContainer.setPrefWidth(120); imageContainer.setMaxWidth(120); - Organization org = appState.getCurrentOrganization(); + Organization org = organizationController.getCurrentOrganization(); if (org != null && org.logoUrl() != null && !org.logoUrl().isBlank()) { ImageView logo = new ImageView(new Image(org.logoUrl(), true)); logo.setId("logo"); @@ -106,7 +111,7 @@ private StackPane createImageContainer() { } private VBox createOrgInfoSection() { - Organization org = appState.getCurrentOrganization(); + Organization org = organizationController.getCurrentOrganization(); VBox orgInfoSection = new VBox(); orgInfoSection.setSpacing(50); diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index 5e2370a..c46af3c 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -19,12 +19,9 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import java.text.SimpleDateFormat; - -import java.math.RoundingMode; import java.util.*; @@ -51,13 +48,15 @@ public UserPageView(AppState appState, NavigationController nav, AuthController } private HBox createProfileSection() { - ImageView avatar = new ImageView(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/userpage/account_circle.png")))); + ImageView avatar = new ImageView(new Image(Objects + .requireNonNull(getClass() + .getResourceAsStream("/userpage/account_circle.png")))); avatar.setFitWidth(150); avatar.setFitHeight(150); avatar.setPreserveRatio(true); avatar.setId("avatar"); - User currentUser = appState.getCurrentUser(); + User currentUser = authController.getCurrentUser(); Text name = new Text(currentUser.getFirstName() + " " + currentUser.getLastName()); name.setId("profile-name"); @@ -133,7 +132,7 @@ private VBox createDonationsSection() { donationsBox.getStyleClass().add("donation-list"); donationsBox.setPadding(new Insets(10)); - User currentUser = appState.getCurrentUser(); + User currentUser = authController.getCurrentUser(); Map userDonations = donationController.getUserDonations(currentUser.getUserId()); diff --git a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java index 3ed1aec..f57b559 100644 --- a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java +++ b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java @@ -61,16 +61,6 @@ void testConstructorThrowsIfOrganizationRepositoryIsNull() { assertEquals("OrganizationRepository cannot be null", exception.getMessage()); } - @Test - void testGetDonationRepository() { - assertEquals(donationRepository, donationService.getDonationRepository()); - } - - @Test - void testGetOrganizationRepository() { - assertEquals(organizationRepository, donationService.getOrganizationRepository()); - } - @Test void donateReturnsFalseIfCustomerNull() { boolean result = donationService.donate(null, diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java index 2d76391..78f3e1a 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java @@ -43,11 +43,6 @@ void constructor_throwsIfScraperIsNull() { assertEquals("OrganizationScraper cannot be null", ex.getMessage()); } - @Test - void testGetOrganizationRepository() { - assertEquals(repo, service.getOrganizationRepository()); - } - @Test void testGetTrustedOrganizationsWithLogos() { Map orgsWithLogos = service.getTrustedOrganizationsWithLogos(); diff --git a/src/test/java/edu/group5/app/model/user/UserServiceTest.java b/src/test/java/edu/group5/app/model/user/UserServiceTest.java index acec10b..99a6913 100644 --- a/src/test/java/edu/group5/app/model/user/UserServiceTest.java +++ b/src/test/java/edu/group5/app/model/user/UserServiceTest.java @@ -34,11 +34,6 @@ void constructorthrowsIfNull() { assertEquals("UserRepository cannot be null", ex.getMessage()); } - @Test - void testGetUserRepository() { - assertEquals(repo, service.getUserRepository()); - } - @Test void registerUserValid() { boolean result = service.registerUser("Customer", "Alice", "Smith", From c350ee2a152054e8dd9662d672fa0304ef850232 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 16 Apr 2026 18:35:59 +0200 Subject: [PATCH 63/98] update&fix[UserPageView]: Remove appstate to increase MVC Structure --- .../java/edu/group5/app/control/NavigationController.java | 2 +- .../java/edu/group5/app/view/causespage/CausesPageView.java | 1 - .../java/edu/group5/app/view/userpage/UserPageView.java | 6 ++---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index 92ea26a..70079ae 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -132,6 +132,6 @@ public void showAboutUsPage() { */ public void showUserPage() { root.setTop(header); - root.setCenter(new UserPageView(appState, this, authController, donationController, organizationController)); + root.setCenter(new UserPageView(this, authController, donationController, organizationController)); } } diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 271bfa9..488a868 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -4,7 +4,6 @@ import edu.group5.app.utils.ParameterValidator; import edu.group5.app.control.NavigationController; import edu.group5.app.control.OrganizationController; -import edu.group5.app.model.AppState; import javafx.application.Platform; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextField; diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index c46af3c..cf6f801 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -4,7 +4,6 @@ import edu.group5.app.control.NavigationController; import edu.group5.app.control.OrganizationController; import edu.group5.app.control.AuthController; -import edu.group5.app.model.AppState; import edu.group5.app.model.donation.Donation; import edu.group5.app.model.organization.Organization; import edu.group5.app.model.user.User; @@ -26,14 +25,13 @@ public class UserPageView extends BorderPane { - private final AppState appState; private final NavigationController nav; private final AuthController authController; private final DonationController donationController; private final OrganizationController organizationController; - public UserPageView(AppState appState, NavigationController nav, AuthController authController, DonationController donationController, OrganizationController organizationController) { - this.appState = appState; + public UserPageView(NavigationController nav, AuthController authController, + DonationController donationController, OrganizationController organizationController) { this.nav = nav; this.authController = authController; this.donationController = donationController; From d9cfc518054569cc03cba310f288b6436e25de8c Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 16 Apr 2026 19:19:13 +0200 Subject: [PATCH 64/98] fix: update tests and code to use new ParameterValidator error messages --- .../organization/OrganizationScraper.java | 4 +- .../group5/app/model/user/UserService.java | 7 +- .../donation/DonationRepositoryTest.java | 10 +- .../model/donation/DonationServiceTest.java | 4 +- .../OrganizationRepositoryTest.java | 312 +++++++++--------- .../organization/OrganizationServiceTest.java | 4 +- .../group5/app/model/user/CustomerTest.java | 20 +- .../app/model/user/UserRepositoryTest.java | 10 +- .../app/model/user/UserServiceTest.java | 2 +- 9 files changed, 187 insertions(+), 186 deletions(-) diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java index fdeec82..ad93736 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -31,7 +31,7 @@ public class OrganizationScraper { * @return the description text, or null if not found or pageUrl is invalid */ public String fetchDescription(String pageUrl) { - if (descriptionCache.containsKey(pageUrl)) { + if (pageUrl != null && descriptionCache.containsKey(pageUrl)) { return descriptionCache.get(pageUrl); } @@ -80,7 +80,7 @@ public String fetchDescription(String pageUrl) { * @return the absolute logo URL, or null if not found or pageUrl is invalid */ public String fetchLogoUrl(String pageUrl) { - if (logoCache.containsKey(pageUrl)) { + if (pageUrl != null && logoCache.containsKey(pageUrl)) { return logoCache.get(pageUrl); } diff --git a/src/main/java/edu/group5/app/model/user/UserService.java b/src/main/java/edu/group5/app/model/user/UserService.java index 9d215d0..6bee483 100644 --- a/src/main/java/edu/group5/app/model/user/UserService.java +++ b/src/main/java/edu/group5/app/model/user/UserService.java @@ -51,8 +51,7 @@ public boolean registerUser(String role, String firstName, String lastName, firstName == null || firstName.trim().isEmpty() || lastName == null || lastName.trim().isEmpty() || email == null || email.trim().isEmpty() || - passwordHash == null || passwordHash.trim().isEmpty() || - this.getUserByEmail(email) != null) { + passwordHash == null || passwordHash.trim().isEmpty()) { return false; } User user; @@ -95,7 +94,9 @@ public User login(String email, char[] password) { * @return the User object if found, null otherwise */ public User getUserByEmail(String email) { - ParameterValidator.stringChecker(email, "email"); + if (email == null || email.trim().isEmpty()) { + return null; + } return this.userRepository.findUserByEmail(email); } } diff --git a/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java b/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java index ce81b2a..82140a7 100644 --- a/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java @@ -33,7 +33,7 @@ void setUp() { void constructorThrowsIfNullList() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new DonationRepository(null)); - assertEquals("The list of rows cannot be null", ex.getMessage()); + assertEquals("List of donation rows can't be null", ex.getMessage()); } @Test @@ -88,7 +88,7 @@ void addContentDuplicateIdFails() { void addContentNullThrows() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> repo.addContent(null)); - assertEquals("Donation cannot be null", ex.getMessage()); + assertEquals("Donation can't be null", ex.getMessage()); } @Test @@ -102,7 +102,7 @@ void getDonationByIdSuccessfully() { void getDonationByIdThrowsIfNegative() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> repo.getDonationById(0)); - assertEquals("Donation ID must be positive", ex.getMessage()); + assertEquals("Donation ID must be a positive integer", ex.getMessage()); } @Test @@ -182,7 +182,7 @@ void filterByOrganizationNoMatch() { void filterByOrganizationThrowsIfNegative() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> repo.filterByOrganization(0)); - assertEquals("Organization number must be positive", ex.getMessage()); + assertEquals("Organization number must be a positive integer", ex.getMessage()); } @Test @@ -261,7 +261,7 @@ void filterByUserIdNoMatch() { void filterByUserIdThrowsIfNegative() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> repo.filterByUser(0)); - assertEquals("User ID must be positive", ex.getMessage()); + assertEquals("User ID must be a positive integer", ex.getMessage()); } @Test diff --git a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java index f57b559..58bd170 100644 --- a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java +++ b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java @@ -50,7 +50,7 @@ void testConstructorThrowsIfDonationRepositoryIsNull() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { new DonationService(null, organizationRepository); }); - assertEquals("DonationRepository cannot be null", exception.getMessage()); + assertEquals("DonationRepository can't be null", exception.getMessage()); } @Test @@ -58,7 +58,7 @@ void testConstructorThrowsIfOrganizationRepositoryIsNull() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { new DonationService(donationRepository, null); }); - assertEquals("OrganizationRepository cannot be null", exception.getMessage()); + assertEquals("OrganizationRepository can't be null", exception.getMessage()); } @Test diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java index 8906395..a5eab05 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java @@ -1,157 +1,157 @@ -package edu.group5.app.model.organization; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -class OrganizationRepositoryTest { - - private OrganizationRepository repository; - private OrganizationScraper scraper; - - @BeforeEach - void setUp() { - scraper = new OrganizationScraper(); - Object[] content = new Object[] { - Map.of( - "org_number", "1", - "name", "Trusted Org1", - "status", "approved", - "url", "org.com", - "is_pre_approved", true - ), - Map.of( - "org_number", "2", - "name", "Trusted Org2", - "status", "approved", - "url", "org.com", - "is_pre_approved", true - ), - Map.of( - "org_number", "3", - "name", "Untrusted Org1", - "status", "pending", - "url", "org.com", - "is_pre_approved", true - ), - Map.of( - "org_number", "4", - "name", "Untrusted Org2", - "status", "pending", - "url", "org.com", - "is_pre_approved", true - ) - }; - repository = new OrganizationRepository(content, scraper); - } - - private void constructorTest(Object[] input, String expectedMessage) { - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> new OrganizationRepository(input, scraper) - ); - assertEquals(expectedMessage, exception.getMessage()); - } - @Test - void constructor_ThrowsWhenContentIsNull() { - constructorTest(null, "The input cannot be null"); - } - - @Test - void constructor_ThrowsWhenScraperIsNull() { - Object[] content = new Object[] { - Map.of( - "org_number", "1", - "name", "Org", - "status", "approved", - "url", "org.com", - "is_pre_approved", true - ) - }; - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> new OrganizationRepository(content, null) - ); - assertEquals("The scraper cannot be null", exception.getMessage()); - } - - @Test - void getTrustedOrganizations_OnlyReturnsTrustedOrganizations() { - Map trusted = repository.getTrustedOrganizations(); - - assertEquals(2, trusted.size()); - assertTrue(trusted.containsKey(1)); - assertTrue(trusted.containsKey(2)); - assertFalse(trusted.containsKey(3)); - assertFalse(trusted.containsKey(4)); - assertTrue(trusted.values().stream().allMatch(Organization::trusted)); - } - - @Test - void testFindByOrgNumberReturnsOrganization() { - assertEquals(new Organization(1, "Trusted Org1", true, - "org.com", true, "Information about Trusted Org1", null), - repository.findByOrgNumber(1)); - } - - @Test - void testFindByOrgNumberIfOrgNumberIsIllegal() { - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> repository.findByOrgNumber(-1)); - assertEquals("The Organization number must be a positive integer", exception.getMessage()); - } - - @Test - void testFindByOrgNumberIfOrgNumberNotFound() { - assertNull(repository.findByOrgNumber(999)); - } - - @Test - void testFindByOrgNameReturnsOrganization() { - assertEquals(new Organization(1, "Trusted Org1", true, - "org.com", true, "Information about Trusted Org1", null), - repository.findByOrgName("Trusted Org1")); - } - - @Test - void testFindByOrgNameIfNameIsIllegalThrowsException() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> repository.findByOrgName(null)); - assertEquals("The name cannot be null", exception.getMessage()); - - IllegalArgumentException exception2 = assertThrows(IllegalArgumentException.class, - () -> repository.findByOrgName("")); - assertEquals("The name cannot be null", exception2.getMessage()); - } - - @Test - void testFindByOrgNameIfNameNotFound() { - assertNull(repository.findByOrgName("Nonexistent Org")); - } - - @Test - void testFindByOrgNameIsCaseInsensitive() { - assertEquals(new Organization(1, "Trusted Org1", true, - "org.com", true, "Information about Trusted Org1", null), - repository.findByOrgName("trusted org1")); - } - - @Test - void testExportAllOrganizations() { - Object[] allOrgs = repository.export(); - assertEquals(4, allOrgs.length); - } - - @Test - void testExportAllOrganizationsThrowsWhenRepositoryIsEmpty() { - OrganizationRepository emptyRepo = new OrganizationRepository(new Object[0], scraper); - IllegalStateException exception = assertThrows( - IllegalStateException.class, () -> emptyRepo.export() - ); - assertEquals("The repository is empty", exception.getMessage()); - } +package edu.group5.app.model.organization; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class OrganizationRepositoryTest { + + private OrganizationRepository repository; + private OrganizationScraper scraper; + + @BeforeEach + void setUp() { + scraper = new OrganizationScraper(); + Object[] content = new Object[] { + Map.of( + "org_number", "1", + "name", "Trusted Org1", + "status", "approved", + "url", "org.com", + "is_pre_approved", true + ), + Map.of( + "org_number", "2", + "name", "Trusted Org2", + "status", "approved", + "url", "org.com", + "is_pre_approved", true + ), + Map.of( + "org_number", "3", + "name", "Untrusted Org1", + "status", "pending", + "url", "org.com", + "is_pre_approved", true + ), + Map.of( + "org_number", "4", + "name", "Untrusted Org2", + "status", "pending", + "url", "org.com", + "is_pre_approved", true + ) + }; + repository = new OrganizationRepository(content, scraper); + } + + private void constructorTest(Object[] input, String expectedMessage) { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new OrganizationRepository(input, scraper) + ); + assertEquals(expectedMessage, exception.getMessage()); + } + @Test + void constructor_ThrowsWhenContentIsNull() { + constructorTest(null, "Input data can't be null"); + } + + @Test + void constructor_ThrowsWhenScraperIsNull() { + Object[] content = new Object[] { + Map.of( + "org_number", "1", + "name", "Org", + "status", "approved", + "url", "org.com", + "is_pre_approved", true + ) + }; + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new OrganizationRepository(content, null) + ); + assertEquals("Scraper can't be null", exception.getMessage()); + } + + @Test + void getTrustedOrganizations_OnlyReturnsTrustedOrganizations() { + Map trusted = repository.getTrustedOrganizations(); + + assertEquals(2, trusted.size()); + assertTrue(trusted.containsKey(1)); + assertTrue(trusted.containsKey(2)); + assertFalse(trusted.containsKey(3)); + assertFalse(trusted.containsKey(4)); + assertTrue(trusted.values().stream().allMatch(Organization::trusted)); + } + + @Test + void testFindByOrgNumberReturnsOrganization() { + assertEquals(new Organization(1, "Trusted Org1", true, + "org.com", true, "Information about Trusted Org1", null), + repository.findByOrgNumber(1)); + } + + @Test + void testFindByOrgNumberIfOrgNumberIsIllegal() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> repository.findByOrgNumber(-1)); + assertEquals("Organization number must be a positive integer", exception.getMessage()); + } + + @Test + void testFindByOrgNumberIfOrgNumberNotFound() { + assertNull(repository.findByOrgNumber(999)); + } + + @Test + void testFindByOrgNameReturnsOrganization() { + assertEquals(new Organization(1, "Trusted Org1", true, + "org.com", true, "Information about Trusted Org1", null), + repository.findByOrgName("Trusted Org1")); + } + + @Test + void testFindByOrgNameIfNameIsIllegalThrowsException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> repository.findByOrgName(null)); + assertEquals("Organization name can't be null", exception.getMessage()); + + IllegalArgumentException exception2 = assertThrows(IllegalArgumentException.class, + () -> repository.findByOrgName("")); + assertEquals("Organization name can't be blank", exception2.getMessage()); + } + + @Test + void testFindByOrgNameIfNameNotFound() { + assertNull(repository.findByOrgName("Nonexistent Org")); + } + + @Test + void testFindByOrgNameIsCaseInsensitive() { + assertEquals(new Organization(1, "Trusted Org1", true, + "org.com", true, "Information about Trusted Org1", null), + repository.findByOrgName("trusted org1")); + } + + @Test + void testExportAllOrganizations() { + Object[] allOrgs = repository.export(); + assertEquals(4, allOrgs.length); + } + + @Test + void testExportAllOrganizationsThrowsWhenRepositoryIsEmpty() { + OrganizationRepository emptyRepo = new OrganizationRepository(new Object[0], scraper); + IllegalStateException exception = assertThrows( + IllegalStateException.class, () -> emptyRepo.export() + ); + assertEquals("The repository is empty", exception.getMessage()); + } } \ No newline at end of file diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java index 78f3e1a..3765bf5 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java @@ -33,14 +33,14 @@ public void setUp() { void constructor_throwsIfRepositoryIsNull() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new OrganizationService(null, scraper)); - assertEquals("OrganizationRepository cannot be null", ex.getMessage()); + assertEquals("OrganizationRepository can't be null", ex.getMessage()); } @Test void constructor_throwsIfScraperIsNull() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new OrganizationService(repo, null)); - assertEquals("OrganizationScraper cannot be null", ex.getMessage()); + assertEquals("Scraper can't be null", ex.getMessage()); } @Test diff --git a/src/test/java/edu/group5/app/model/user/CustomerTest.java b/src/test/java/edu/group5/app/model/user/CustomerTest.java index 7c96438..147343f 100644 --- a/src/test/java/edu/group5/app/model/user/CustomerTest.java +++ b/src/test/java/edu/group5/app/model/user/CustomerTest.java @@ -59,55 +59,55 @@ void testInstanceOfCustomer() { @Test void constructorWithNegativeUserIdThrowsException() { constructorTest(-1, testFirstName, testLastName, - testEmail, testPasswordHash, "User ID must be positive"); + testEmail, testPasswordHash, "User ID must be a positive integer"); } @Test void constructorWithNullFirstNameThrowsException() { constructorTest(testUserId, null, testLastName, - testEmail, testPasswordHash, "First name cannot be null or empty"); + testEmail, testPasswordHash, "First name can't be null"); } @Test void constructorWithEmptyFirstNameThrowsException() { constructorTest(testUserId, "", testLastName, - testEmail, testPasswordHash, "First name cannot be null or empty"); + testEmail, testPasswordHash, "First name can't be blank"); } @Test void constructorWithNullLastNameThrowsException() { constructorTest(testUserId, testFirstName, null, - testEmail, testPasswordHash, "Last name cannot be null or empty"); + testEmail, testPasswordHash, "Last name can't be null"); } @Test void constructorWithEmptyLastNameThrowsException() { - constructorTest(testUserId, testFirstName, - "", testEmail, testPasswordHash, "Last name cannot be null or empty"); + constructorTest(testUserId, testFirstName, + "", testEmail, testPasswordHash, "Last name can't be blank"); } @Test void constructorWithNullEmailThrowsException() { constructorTest(testUserId, testFirstName, testLastName, - null, testPasswordHash, "Email cannot be null or empty"); + null, testPasswordHash, "Email can't be null"); } @Test void constructorWithEmptyEmailThrowsException() { constructorTest(testUserId, testFirstName, testLastName, - "", testPasswordHash, "Email cannot be null or empty"); + "", testPasswordHash, "Email can't be blank"); } @Test void constructorWithNullPasswordHashThrowsException() { constructorTest(testUserId, testFirstName, testLastName, - testEmail, null, "Password hash cannot be null or empty"); + testEmail, null, "Password hash can't be null"); } @Test void constructorWithEmptyPasswordHashThrowsException() { constructorTest(testUserId, testFirstName, testLastName, - testEmail, "", "Password hash cannot be null or empty"); + testEmail, "", "Password hash can't be blank"); } diff --git a/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java b/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java index eaf2c2c..fd13b13 100644 --- a/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java @@ -27,7 +27,7 @@ void setUp() { void constructorThrowsIfNull() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new UserRepository(null)); - assertEquals("The list of rows cannot be null", ex.getMessage()); + assertEquals("List of User rows can't be null", ex.getMessage()); } @Test @@ -77,7 +77,7 @@ void addContentSuccessfully() { void addContentNullThrows() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> repo.addContent(null)); - assertEquals("User cannot be null", ex.getMessage()); + assertEquals("User can't be null", ex.getMessage()); } @Test @@ -101,11 +101,11 @@ void findUserByEmailReturnsNullIfNotFound() { void findUserByEmailThrowsIfNullOrEmpty() { IllegalArgumentException ex1 = assertThrows(IllegalArgumentException.class, () -> repo.findUserByEmail(null)); - assertEquals("Email cannot be null or empty", ex1.getMessage()); + assertEquals("Email can't be null", ex1.getMessage()); IllegalArgumentException ex2 = assertThrows(IllegalArgumentException.class, () -> repo.findUserByEmail(" ")); - assertEquals("Email cannot be null or empty", ex2.getMessage()); + assertEquals("Email can't be blank", ex2.getMessage()); } @Test @@ -123,7 +123,7 @@ void getUserByIdReturnsNullIfNotFound() { void getUserByIdThrowsIfNonPositive() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> repo.getUserById(0)); - assertEquals("User ID must be positive", ex.getMessage()); + assertEquals("User ID must be a positive integer", ex.getMessage()); } diff --git a/src/test/java/edu/group5/app/model/user/UserServiceTest.java b/src/test/java/edu/group5/app/model/user/UserServiceTest.java index 99a6913..104e2f1 100644 --- a/src/test/java/edu/group5/app/model/user/UserServiceTest.java +++ b/src/test/java/edu/group5/app/model/user/UserServiceTest.java @@ -31,7 +31,7 @@ void setUp() { void constructorthrowsIfNull() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new UserService(null)); - assertEquals("UserRepository cannot be null", ex.getMessage()); + assertEquals("UserRepository can't be null", ex.getMessage()); } @Test From 829537973866a2050a8008ecf1218867feaab783 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Sat, 18 Apr 2026 15:57:19 +0200 Subject: [PATCH 65/98] Update[Organization]: Update rendering of organization Logo to render more quicker --- .../app/view/causespage/CausesPageView.java | 42 +++++++++++++++---- .../app/view/causespage/OrganizationCard.java | 4 +- .../OrganizationPageView.java | 40 ++++++++++++------ 3 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 488a868..0bdddbc 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -38,6 +38,7 @@ public class CausesPageView extends BorderPane { private GridPane organizationGrid; private Map allOrganizations; + private Map cardCache = new HashMap<>(); public CausesPageView(NavigationController nav, OrganizationController orgController) { ParameterValidator.objectChecker(nav, "NavigationController"); @@ -205,18 +206,41 @@ private Map filterOrganizations(String searchTerm) { )); } + private void updateOrganizationGrid(String searchTerm) { - if (organizationGrid == null) { - return; + if (organizationGrid == null) return; + + // Save existing cards into cache before clearing + for (var node : organizationGrid.getChildren()) { + if (node instanceof OrganizationCard card) { + cardCache.put(card.getOrganization().orgNumber(), card); + } } - + + Map filtered = filterOrganizations(searchTerm); + organizationGrid.getChildren().clear(); organizationGrid.getColumnConstraints().clear(); - - // Rebuild grid with filtered organizations - GridPane updated = createOrganizationSection(searchTerm); - - organizationGrid.getChildren().addAll(updated.getChildren()); - organizationGrid.getColumnConstraints().addAll(updated.getColumnConstraints()); + + int column = 0; + int row = 0; + + for (Organization org : filtered.values()) { + OrganizationCard card = cardCache.get(org.orgNumber()); + if (card != null) { + organizationGrid.add(card, column, row); + column++; + if (column == 4) { + column = 0; + row++; + } + } + } + + for (int i = 0; i < 4; i++) { + ColumnConstraints col = new ColumnConstraints(); + col.setPercentWidth(25); + organizationGrid.getColumnConstraints().add(col); + } } } diff --git a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java index 4645d0b..483d0e7 100644 --- a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java +++ b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java @@ -61,7 +61,7 @@ public void updateLogo(String logoUrl) { if (imageContainer == null) return; imageContainer.getChildren().clear(); if (logoUrl != null && !logoUrl.isBlank()) { - ImageView logo = new ImageView(new Image(logoUrl, true)); + ImageView logo = new ImageView(new Image(logoUrl, 80, 80, true, true, true)); logo.setId("logo"); logo.setSmooth(true); logo.setPreserveRatio(true); @@ -96,7 +96,7 @@ private StackPane createImageContainer(String img) { if (img != null && !img.isBlank()) { - ImageView logo = new ImageView(new Image(img, true)); + ImageView logo = new ImageView(new Image(img, 80, 80, true, true, true)); logo.setId("logo"); logo.setSmooth(true); logo.setPreserveRatio(true); diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 3bea2ff..f591d26 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -4,6 +4,7 @@ import edu.group5.app.control.NavigationController; import edu.group5.app.control.OrganizationController; import edu.group5.app.model.organization.Organization; +import javafx.application.Platform; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -90,23 +91,38 @@ private StackPane createImageContainer() { imageContainer.setMaxWidth(120); Organization org = organizationController.getCurrentOrganization(); + if (org != null && org.logoUrl() != null && !org.logoUrl().isBlank()) { - ImageView logo = new ImageView(new Image(org.logoUrl(), true)); - logo.setId("logo"); - logo.setSmooth(true); - logo.setPreserveRatio(true); - logo.setFitHeight(350); - logo.setFitWidth(350); - imageContainer.getChildren().add(logo); + // Load image in background thread to avoid blocking UI + new Thread(() -> { + try { + Image image = new Image(org.logoUrl(), 120, 120, true, true); + Platform.runLater(() -> { + ImageView logo = new ImageView(image); + logo.setId("logo"); + logo.setSmooth(true); + logo.setPreserveRatio(true); + logo.setFitHeight(350); + logo.setFitWidth(350); + imageContainer.getChildren().clear(); + imageContainer.getChildren().add(logo); + }); + } catch (Exception e) { + // Logo failed to load, show placeholder + Platform.runLater(() -> { + imageContainer.getChildren().clear(); + Text text = new Text("No image"); + text.setStyle("-fx-font-size: 10;"); + imageContainer.getChildren().add(text); + }); + } + }, "LogoLoader").start(); } else { - StackPane placeholder = new StackPane(); - Text text = new Text("No image"); text.setStyle("-fx-font-size: 10;"); - - placeholder.getChildren().add(text); - imageContainer.getChildren().add(placeholder); + imageContainer.getChildren().add(text); } + return imageContainer; } From c4d31dcebe112a03305fdb95f1649d4d5ed4a839 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Sat, 18 Apr 2026 16:45:31 +0200 Subject: [PATCH 66/98] feat[NavigationController]: Add a small about us pop up feature that summarizes what this application is about --- src/main/java/edu/group5/app/App.java | 2 +- .../app/control/NavigationController.java | 38 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index 6402156..17f99de 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -82,7 +82,7 @@ public void init() { OrganizationService organizationService = new OrganizationService(organizationRepository, orgScraper); this.root = new BorderPane(); this.appState = new AppState(); - this.nav = new NavigationController(root, appState, userService, donationService, organizationService); + this.nav = new NavigationController(root, appState, userService, donationService, organizationService, getHostServices()); } @Override diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index 70079ae..2c47621 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -15,7 +15,13 @@ import edu.group5.app.view.loginpage.SignUpPageView; import edu.group5.app.view.organizationpage.OrganizationPageView; import edu.group5.app.view.userpage.UserPageView; +import javafx.application.HostServices; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; /** * Controller responsible for handling navigation between different pages of the application. @@ -33,23 +39,25 @@ public class NavigationController { private final LoginHeader loginHeader; private final AppState appState; + private HostServices hostServices; private final AuthController authController; private final DonationController donationController; private final OrganizationController organizationController; public NavigationController(BorderPane root, AppState appState, UserService userService, - DonationService donationService, OrganizationService organizationService) { + DonationService donationService, OrganizationService organizationService, HostServices hostServices) { ParameterValidator.objectChecker(root, "Root BorderPane"); ParameterValidator.objectChecker(appState, "AppState"); ParameterValidator.objectChecker(userService, "UserService"); ParameterValidator.objectChecker(donationService, "DonationService"); ParameterValidator.objectChecker(organizationService, "OrganizationService"); + ParameterValidator.objectChecker(hostServices, "HostServices"); this.root = root; this.header = new Header(this); this.loginHeader = new LoginHeader(); - + this.hostServices = hostServices; this.appState = appState; this.authController = new AuthController(appState, this, userService); @@ -122,7 +130,31 @@ public void showDonationPage() { } public void showAboutUsPage() { - root.setTop(header); + Alert aboutUs = new Alert(Alert.AlertType.INFORMATION); + aboutUs.setTitle("About us"); + aboutUs.setHeaderText("Help Me Help - About Us"); + + Label description = new Label( + "Help Me Help is a charity donation application designed to connect donors with organizations in need. " + + "Our mission is to make it easy for people to support causes they care about and make a positive impact in the world.\n\n" + + "This application was developed by Team 5 as part of a IDATT1005 course project at NTNU spring 2026." + ); + description.setWrapText(true); + description.maxWidthProperty().bind(aboutUs.getDialogPane().widthProperty().subtract(40)); + + Hyperlink websiteLink = new Hyperlink("For more information about the project, visit our GitHub repository"); + websiteLink.setOnAction(e -> hostServices.showDocument("https://git.ntnu.no/Group-5/Help-Me-Help")); + + VBox content = new VBox(10, description, websiteLink); + content.setPrefWidth(420); + + aboutUs.getDialogPane().setContentText(null); + aboutUs.getDialogPane().setContent(content); + + // Optional: single Close button instead of OK + aboutUs.getButtonTypes().setAll(ButtonType.CLOSE); + + aboutUs.showAndWait(); } /** From 3b3376f6dd024c239299be1132efc2a1434db0f5 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Mon, 20 Apr 2026 13:35:24 +0200 Subject: [PATCH 67/98] fix[App]: fix spelling of static variable MAX_RETRIES --- src/main/java/edu/group5/app/App.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index 17f99de..8658fb1 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -35,7 +35,7 @@ public class App extends Application { NavigationController nav; private Logger logger; - static int MAX_ReTRIES = 3; + static int MAX_RETRIES = 3; @Override public void init() { @@ -46,12 +46,12 @@ public void init() { OrgApiWrapper orgApiWrapper = new OrgApiWrapper("https://app.innsamlingskontrollen.no/api/public/v1/all"); int retries = 0; - while (!dbWrapper.connect() && retries < MAX_ReTRIES) { + while (!dbWrapper.connect() && retries < MAX_RETRIES) { this.logger.warning("Failed to connect to database"); retries++; } - if (retries == MAX_ReTRIES) { - this.logger.severe("Failed to connect to database after " + MAX_ReTRIES + " attempts"); + if (retries == MAX_RETRIES) { + this.logger.severe("Failed to connect to database after " + MAX_RETRIES + " attempts"); throw new RuntimeException("Failed to connect to database"); } From df0905eaafce202420c9d38a0f964cc4a12f3ab8 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Mon, 20 Apr 2026 22:18:09 +0200 Subject: [PATCH 68/98] Update[AboutUsPage]: Update AboutUs page by moving the construction of the pop-up into a new class, increassing Seperation of concern --- src/main/java/edu/group5/app/App.java | 3 +- .../app/control/NavigationController.java | 35 +++--------- .../app/view/aboutuspage/AboutUsView.java | 55 +++++++++++++++++++ 3 files changed, 65 insertions(+), 28 deletions(-) create mode 100644 src/main/java/edu/group5/app/view/aboutuspage/AboutUsView.java diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index 8658fb1..3aea332 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -11,6 +11,7 @@ import edu.group5.app.model.user.UserService; import edu.group5.app.model.wrapper.DbWrapper; import edu.group5.app.model.wrapper.OrgApiWrapper; +import edu.group5.app.view.aboutuspage.AboutUsView; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.image.Image; @@ -82,7 +83,7 @@ public void init() { OrganizationService organizationService = new OrganizationService(organizationRepository, orgScraper); this.root = new BorderPane(); this.appState = new AppState(); - this.nav = new NavigationController(root, appState, userService, donationService, organizationService, getHostServices()); + this.nav = new NavigationController(root, appState, userService, donationService, organizationService, this.getHostServices()); } @Override diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index 2c47621..8bbbbb3 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -6,6 +6,7 @@ import edu.group5.app.model.user.UserService; import edu.group5.app.utils.ParameterValidator; import edu.group5.app.view.Header; +import edu.group5.app.view.aboutuspage.AboutUsView; import edu.group5.app.view.causespage.CausesPageView; import edu.group5.app.view.donationpage.DonationPageView; import edu.group5.app.view.donationpage.PaymentCompletePageView; @@ -39,7 +40,7 @@ public class NavigationController { private final LoginHeader loginHeader; private final AppState appState; - private HostServices hostServices; + private final HostServices hostServices; private final AuthController authController; private final DonationController donationController; @@ -57,8 +58,8 @@ public NavigationController(BorderPane root, AppState appState, UserService user this.root = root; this.header = new Header(this); this.loginHeader = new LoginHeader(); - this.hostServices = hostServices; this.appState = appState; + this.hostServices = hostServices; this.authController = new AuthController(appState, this, userService); this.donationController = new DonationController(appState, this, donationService); @@ -129,32 +130,12 @@ public void showDonationPage() { root.setCenter(new DonationPageView(this, donationController)); } + /** + * Displays an "About Us" dialog with information about the application and its developers. + * The dialog includes a description of the app's mission and a hyperlink to the project's GitHub repository + */ public void showAboutUsPage() { - Alert aboutUs = new Alert(Alert.AlertType.INFORMATION); - aboutUs.setTitle("About us"); - aboutUs.setHeaderText("Help Me Help - About Us"); - - Label description = new Label( - "Help Me Help is a charity donation application designed to connect donors with organizations in need. " + - "Our mission is to make it easy for people to support causes they care about and make a positive impact in the world.\n\n" + - "This application was developed by Team 5 as part of a IDATT1005 course project at NTNU spring 2026." - ); - description.setWrapText(true); - description.maxWidthProperty().bind(aboutUs.getDialogPane().widthProperty().subtract(40)); - - Hyperlink websiteLink = new Hyperlink("For more information about the project, visit our GitHub repository"); - websiteLink.setOnAction(e -> hostServices.showDocument("https://git.ntnu.no/Group-5/Help-Me-Help")); - - VBox content = new VBox(10, description, websiteLink); - content.setPrefWidth(420); - - aboutUs.getDialogPane().setContentText(null); - aboutUs.getDialogPane().setContent(content); - - // Optional: single Close button instead of OK - aboutUs.getButtonTypes().setAll(ButtonType.CLOSE); - - aboutUs.showAndWait(); + new AboutUsView(hostServices).displayAboutUs(); } /** diff --git a/src/main/java/edu/group5/app/view/aboutuspage/AboutUsView.java b/src/main/java/edu/group5/app/view/aboutuspage/AboutUsView.java new file mode 100644 index 0000000..f25a95e --- /dev/null +++ b/src/main/java/edu/group5/app/view/aboutuspage/AboutUsView.java @@ -0,0 +1,55 @@ +package edu.group5.app.view.aboutuspage; + +import edu.group5.app.utils.ParameterValidator; +import javafx.application.HostServices; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; + +/** + * A view for displaying information about the "Help Me Help" application and its creators. + * The view is presented as an informational dialog that includes a description of the app's mission + * and a hyperlink to the project's GitHub repository for more details. This page serves to provide + * users with background information about the application and its development team. + */ +public class AboutUsView { + private HostServices hostServices; + + public AboutUsView(HostServices hostServices) { + ParameterValidator.objectChecker(hostServices, "HostServices"); + this.hostServices = hostServices; + } + + /** + * Displays the "About Us" information in an alert dialog. + */ + public void displayAboutUs() { + Alert aboutUs = new Alert(Alert.AlertType.INFORMATION); + aboutUs.setTitle("About us"); + aboutUs.setHeaderText("Help Me Help - About Us"); + + Label description = new Label( + "Help Me Help is a charity donation application designed to connect donors with organizations in need. " + + "Our mission is to make it easy for people to support causes they care about and make a positive impact in the world.\n\n" + + "This application was developed by Team 5 as part of a IDATT1005 course project at NTNU spring 2026." + ); + description.setWrapText(true); + description.maxWidthProperty().bind(aboutUs.getDialogPane().widthProperty().subtract(40)); + + Hyperlink websiteLink = new Hyperlink("For more information about the project, visit our GitHub repository"); + websiteLink.setOnAction(e -> hostServices.showDocument("https://git.ntnu.no/Group-5/Help-Me-Help")); + + VBox content = new VBox(10, description, websiteLink); + content.setPrefWidth(420); + + aboutUs.getDialogPane().setContentText(null); + aboutUs.getDialogPane().setContent(content); + + // Optional: single Close button instead of OK + aboutUs.getButtonTypes().setAll(ButtonType.CLOSE); + + aboutUs.showAndWait(); + } +} From 2bf176c69c52c269e7ba59d49a554618c991d119 Mon Sep 17 00:00:00 2001 From: Fredrik Jonathan Marjoni Date: Tue, 21 Apr 2026 12:33:16 +0200 Subject: [PATCH 69/98] Update README.md Add skeletal structure for readme --- README.md | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f186dda..e88f3df 100644 --- a/README.md +++ b/README.md @@ -1 +1,151 @@ -# Help-Me-Help +# Help-Me-Help - IDATT1005 Team 5 Portofolio Project Spring 2026 :octocat: + +[//]: # (TODO: Fill inn your name and student ID) +[//]: # (TODO: Mappe-2025-Marjoni-fj) + +**STUDENT NAME** +
    +
  • Emil Fagerjord
  • +
  • Mathea Gjerde
  • +
  • Fredrik Jonathan Marjoni
  • +
  • Lucy Ciara Herud-Thomassen
  • +
    + +## Project descriptionπŸ’» + +[//]: # (TODO: Write a short description of your project/product here.) +This Java-based application, developed using Maven... + +## Project structure πŸ“ + +--- +The project follows a standard Maven layout and is organized into clearly separated packages according to responsibility-driven design (RDD). +All source files are stored under the `src` directory. + +### Main Package Structure (`src/main`) + +
    +
    +
    +[//]: # (TODO: Describe the structure of your project here. How have you used packages in your structure. Where are all sourcefiles stored. Where are all JUnit-test classes stored. etc.) + +### πŸ“¦ Package Responsibilities + +#### Models + +#### Controller + +#### View + +#### Utils + +### JUnit Tests (`src/test`) + +The JUnit tests are stored under `src/test` and mirror the main package structure. These tests cover both positive and negative test of all classes (except `App.java` and UI classes) and their methods ensuring program reliability according to the specification given in the portofolie project descriptions +
    +
    + +### Maven Layout + +The project uses the standard Maven directory structure, which ensures: + +* clean separation of source and test files +* compatibility with IDEs such as IntelliJ, VS Code, and Eclipse +* maintainability and easy future extensions (e.g., persistence or additional views) + +--- + +## Link to repositoryπŸ“š + +[//]: # (TODO: Include a link to your GitHub repository here.) +[GitHub Repository - IDATT2003 Mappevurdering 2026](https://github.com/LucyCiara/IDATT2003v26-millions) + +--- + +## How to run the projectπŸ“ + +[//]: # (TODO: Describe how to run your project here. What is the main class? What is the main method? +What is the input and output of the program? What is the expected behaviour of the program?) + +**Requirements:** + +* Java JDK 25 +* Maven +* IDE (Ideally IntelliJ or VSCode with Java Extension Pack) +**Steps:** + +1. **Clone repository** + Clone the Repository from GitHub + +2. **Open the Project:** + Open VS Code and select **File > Open Folder**, navigating to the root folder of the project (containing `pom.xml`). + +3. **Build the Project:** + Open the terminal in VS Code (`Ctrl + ~`) and run: + + ```bash + mvn clean compile + +4. **Run the Application:** + Start the program by running the main class: + + ```bash + mvn javafx:run + +5. **Input and Output** + +* Input: +* Output: + +1. **Excpected behavior:** +The program allows the user to: + +--- + +## How to run the tests πŸ§ͺ + +This project uses JUnit 5 for unit testing. +All test classes mirror the main package structure and are stored in `src/test` + +* ### Open the Project + + Open VS Code and select **File > Open Folder**, navigating to the root folder of the project (containing `pom.xml`). + +* ### Run all tests + + To execute the full test suite, run: + + ```bash + mvn clean test + +This command: + + 1. Cleans old build files + 2. Compiles the main source code + 3. Compiles the tests + 4. Runs all JUnit tests + +--- + +* ### Viewing test results + +After the tests finish, Maven creates detailed reports here: +`target/surefire-reports/` + +Each report includes: + + 1. Test class summaries + 2. Stack traces for any failures + 3. Execution times + 4. Running tests in an IDE + +[//]: # (TODO: Describe how to run the tests here.) + +## References πŸ”— + +[//]: # (TODO: Include references here, if any. For example, if you have used code from the course book, include a reference to the chapter. +Or if you have used code from a website or other source, include a link to the source.) +References are included in the project report. + +--- +""" From 0eeb8644ebbb777395c13912f11c114457a41893 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:03:29 +0200 Subject: [PATCH 70/98] update[AuthController]: updated the text of the privacy policy for better transparency --- src/main/java/edu/group5/app/control/AuthController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/group5/app/control/AuthController.java b/src/main/java/edu/group5/app/control/AuthController.java index 7c34afd..11c3288 100644 --- a/src/main/java/edu/group5/app/control/AuthController.java +++ b/src/main/java/edu/group5/app/control/AuthController.java @@ -95,8 +95,9 @@ public void handleSignUp(SignUpPageView view, String firstName, String lastName, privacyPolicy.setHeaderText("Accept Privacy Policy"); privacyPolicy.setContentText( "Your user information like:\n" + - "Name and emailβ€”as well as donations tied to your accountβ€”will be saved locally on your machine.\n" + - "By creating an account, you accept the right of our app to store this information."); + "Name and emailβ€”as well as donations tied to your accountβ€”will be saved locally on your machine.\n" + + "This information is only used to create your account, and no data will be sold to third parties.\n" + + "By creating an account, you accept the right of our app to store this information on your computer."); if (privacyPolicy.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { boolean success = userService.registerUser( From ad840a785c2d404122da84df96b7e5984c8cef21 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:45:45 +0200 Subject: [PATCH 71/98] fix[UserService]: re-implement email checker for sign-up --- src/main/java/edu/group5/app/model/user/UserService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/model/user/UserService.java b/src/main/java/edu/group5/app/model/user/UserService.java index 6bee483..e6439f1 100644 --- a/src/main/java/edu/group5/app/model/user/UserService.java +++ b/src/main/java/edu/group5/app/model/user/UserService.java @@ -51,7 +51,8 @@ public boolean registerUser(String role, String firstName, String lastName, firstName == null || firstName.trim().isEmpty() || lastName == null || lastName.trim().isEmpty() || email == null || email.trim().isEmpty() || - passwordHash == null || passwordHash.trim().isEmpty()) { + passwordHash == null || passwordHash.trim().isEmpty() || + this.getUserByEmail(email) != null) { return false; } User user; From 9aff5fa26214f80a9a8fc11293dfc2bc91737d8c Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 21 Apr 2026 13:55:34 +0200 Subject: [PATCH 72/98] Update[JUnit]: add more JUnit tests for better test coverage --- .../java/edu/group5/app/model/AppState.java | 6 +- .../edu/group5/app/model/AppStateTest.java | 69 +++++++++++++++++++ .../model/donation/DonationServiceTest.java | 29 +++++++- .../app/model/donation/DonationTest.java | 12 +++- .../OrganizationRepositoryTest.java | 24 +++++++ .../organization/OrganizationScraperTest.java | 13 ++++ 6 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 src/test/java/edu/group5/app/model/AppStateTest.java diff --git a/src/main/java/edu/group5/app/model/AppState.java b/src/main/java/edu/group5/app/model/AppState.java index bd56ba0..e17bda9 100644 --- a/src/main/java/edu/group5/app/model/AppState.java +++ b/src/main/java/edu/group5/app/model/AppState.java @@ -18,7 +18,7 @@ public class AppState { private User currentUser; private BigDecimal currentDonationAmount; private Organization currentOrganization; - private String currentDonation; + private String currentPaymentMethod; /** * Gets the current user of the application. @@ -73,7 +73,7 @@ public void setCurrentDonationAmount(BigDecimal amount) { * @return the current payment method */ public String getCurrentPaymentMethod() { - return this.currentDonation; + return this.currentPaymentMethod; } /** @@ -81,6 +81,6 @@ public String getCurrentPaymentMethod() { * @param paymentMethod the payment method to set as the current payment method */ public void setCurrentPaymentMethod(String paymentMethod){ - this.currentDonation = paymentMethod; + this.currentPaymentMethod = paymentMethod; } } diff --git a/src/test/java/edu/group5/app/model/AppStateTest.java b/src/test/java/edu/group5/app/model/AppStateTest.java new file mode 100644 index 0000000..1254033 --- /dev/null +++ b/src/test/java/edu/group5/app/model/AppStateTest.java @@ -0,0 +1,69 @@ +package edu.group5.app.model; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import edu.group5.app.model.organization.Organization; +import edu.group5.app.model.user.Customer; +import edu.group5.app.model.user.User; + +public class AppStateTest { + private User nullUser; + private BigDecimal nullDonationAmount; + private Organization nullOrganization; + private String nullPaymentMethod; + + private User currentTestUser; + private BigDecimal currentTestDonationAmount; + private Organization currentTestOrganization; + private String currentTestPaymentMethod; + + @BeforeEach + void setUp() { + nullUser = null; + nullDonationAmount = null; + nullOrganization = null; + nullPaymentMethod = null; + + currentTestUser = new Customer(1, "Bob", + "Builder", "testuser@example.com", + "password123"); + + currentTestDonationAmount = BigDecimal.ZERO; + + currentTestOrganization = new Organization(1738, "TestOrg", + true, "https://testorg.example.com", true, + "A test organization", "https://testorg.example.com/logo.png"); + + currentTestPaymentMethod = "Credit Card"; + } + + @Test + void gettersAndSetters_WorkCorrectly() { + AppState appState = new AppState(); + + // Test current user + assertEquals(nullUser, appState.getCurrentUser()); + appState.setCurrentUser(currentTestUser); + assertEquals(currentTestUser, appState.getCurrentUser()); + + // Test current donation amount + assertEquals(nullDonationAmount, appState.getCurrentDonationAmount()); + appState.setCurrentDonationAmount(currentTestDonationAmount); + assertEquals(currentTestDonationAmount, appState.getCurrentDonationAmount()); + + // Test current organization + assertEquals(nullOrganization, appState.getCurrentOrganization()); + appState.setCurrentOrganization(currentTestOrganization); + assertEquals(currentTestOrganization, appState.getCurrentOrganization()); + + // Test current payment method + assertEquals(nullPaymentMethod, appState.getCurrentPaymentMethod()); + appState.setCurrentPaymentMethod(currentTestPaymentMethod); + assertEquals(currentTestPaymentMethod, appState.getCurrentPaymentMethod()); + } +} diff --git a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java index 58bd170..3b1e5ff 100644 --- a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java +++ b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java @@ -12,7 +12,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; - +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @@ -61,6 +61,33 @@ void testConstructorThrowsIfOrganizationRepositoryIsNull() { assertEquals("OrganizationRepository can't be null", exception.getMessage()); } + @Test + void getUserDonationsReturnsEmptyMapIfNoDonations() { + assertTrue(donationService.getUserDonations(customer.getUserId()).isEmpty()); + } + + @Test + void getOrganizationDonationsReturnsMapOfDonations() { + Donation donation1 = new Donation(1, customer.getUserId(), + 101, new BigDecimal("20.00"), Timestamp.from(Instant.now()), "Card"); + Donation donation2 = new Donation(2, customer.getUserId(), + 101, new BigDecimal("30.00"), Timestamp.from(Instant.now()), "PayPal"); + donationRepository.addContent(donation1); + donationRepository.addContent(donation2); + + Map donations = donationService.getOrganizationDonations(101); + assertEquals(2, donations.size()); + assertTrue(donations.containsKey(1)); + assertTrue(donations.containsKey(2)); + assertEquals(donation1, donations.get(1)); + assertEquals(donation2, donations.get(2)); + } + + @Test + void getOrganizationDonationsReturnsEmptyMapIfNoDonations() { + assertTrue(donationService.getOrganizationDonations(1).isEmpty()); + } + @Test void donateReturnsFalseIfCustomerNull() { boolean result = donationService.donate(null, diff --git a/src/test/java/edu/group5/app/model/donation/DonationTest.java b/src/test/java/edu/group5/app/model/donation/DonationTest.java index f8f9069..1de92f9 100644 --- a/src/test/java/edu/group5/app/model/donation/DonationTest.java +++ b/src/test/java/edu/group5/app/model/donation/DonationTest.java @@ -74,11 +74,19 @@ void testIfThrowsExceptionWhenOrganizationIdIsNotPositive() { amount1, date1, paymentMethod1); } @Test - void testIfThrowsExceptionWhenAmountIsNotPositive() { + void testIfThrowsExceptionWhenAmountIsNegative() { expectedMessage = "Amount must be positive and not null"; exceptionTest(donationId1, userId1, organizationId1, - new BigDecimal("0.00"), date1, paymentMethod1); + new BigDecimal("-1.00"), date1, paymentMethod1); } + + @Test + void testIfThrowsExceptionWhenAmountIsNull() { + expectedMessage = "Amount must be positive and not null"; + exceptionTest(donationId1, userId1, organizationId1, + null, date1, paymentMethod1); + } + @Test void testIfThrowsExceptionWhenDateIsNull() { expectedMessage = "Date must not be null"; diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java index a5eab05..e6b0cf9 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java @@ -60,6 +60,30 @@ void constructor_ThrowsWhenContentIsNull() { constructorTest(null, "Input data can't be null"); } + @Test + void constructor_SkipsOrganizationWithMissingOrgNumber() { + Object[] content = new Object[] { + Map.of( + "name", "Good Org", + "status", "approved", + "url", "org.com", + "is_pre_approved", true + ), + Map.of( + "org_number", "999", + "name", "Bad Org", + "status", "approved", + "url", "org.com", + "is_pre_approved", true + ) + }; + + OrganizationRepository repo = new OrganizationRepository(content, scraper); + + assertEquals(1, repo.findByOrgNumber(999) != null ? 1 : 0); + assertNull(repo.findByOrgNumber(1)); + } + @Test void constructor_ThrowsWhenScraperIsNull() { Object[] content = new Object[] { diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java index c6c318b..8807e61 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java @@ -63,4 +63,17 @@ void fetchLogoUrl_CachesResultOnSecondCall() { // If no exception thrown, cache works assertTrue(true); } + + @Test + void fetchDescription_ReturnsCachedValue() { + OrganizationScraper scraper = new OrganizationScraper(); + + // First call - caches (but makes real request) + String result1 = scraper.fetchDescription("https://example.com"); + + // Second call - should return cached without new request + String result2 = scraper.fetchDescription("https://example.com"); + + assertEquals(result1, result2); // Same object = cached + } } From 4927acac63d746e7e2bdd8177fb762f4efa83cdc Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 21 Apr 2026 14:53:28 +0200 Subject: [PATCH 73/98] test[JUnit]: add Mockito dependency and more JUnit tests ensuring greater test coverage --- pom.xml | 6 ++ .../java/edu/group5/app/model/Repository.java | 8 -- .../organization/OrganizationScraper.java | 86 ++++++++++++------ .../organization/OrganizationScraperTest.java | 91 +++++++++++++++++-- .../app/model/user/UserServiceTest.java | 6 +- 5 files changed, 148 insertions(+), 49 deletions(-) diff --git a/pom.xml b/pom.xml index 5bbba22..8091ffe 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,12 @@ jsoup 1.17.2 + + org.mockito + mockito-inline + 5.2.0 + test + diff --git a/src/main/java/edu/group5/app/model/Repository.java b/src/main/java/edu/group5/app/model/Repository.java index 6dd425e..8b4ec61 100644 --- a/src/main/java/edu/group5/app/model/Repository.java +++ b/src/main/java/edu/group5/app/model/Repository.java @@ -23,12 +23,4 @@ protected Repository(Map content) { ParameterValidator.objectChecker(content, "content"); this.content = content; } - - /** - * Gets the content of the repository. - * @return the content of the repository - */ - public Map getContent() { - return content; - } } diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java index ad93736..beb16aa 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -40,31 +40,10 @@ public String fetchDescription(String pageUrl) { .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") .timeout(5000).get(); - Element section = doc.selectFirst("section.information"); - if (section != null) { - section.select("div.extra-info").remove(); - section.select("a.read-more").remove(); - - // Extract all

    tags and

    elements as separate paragraphs - String description = section.select("p, div").stream() - .filter(el -> el.tagName().equals("p") || el.select("p").isEmpty()) - .filter(el -> !el.hasClass("extra-info") && !el.hasClass("logo")) - .map(Element::text) - .map(text -> text.replace("Les mer", "").trim()) - .filter(text -> !text.isBlank()) - .collect(Collectors.joining("\n\n")); - - // Fallback: if no paragraphs found, get all text from section - if (description.isBlank()) { - description = section.text().trim(); - } - description = description.replace("Les mer", "").trim(); - - // Only cache and return if we found something meaningful - if (!description.isBlank()) { - descriptionCache.put(pageUrl, description); - return description; - } + String description = parseDescription(doc); + if (!description.isBlank()) { + descriptionCache.put(pageUrl, description); + return description; } } catch (Exception e) { System.out.println("Could not get description for: " + pageUrl); @@ -72,6 +51,42 @@ public String fetchDescription(String pageUrl) { return null; } + /** + * Parses the description from a Document by extracting text content + * from {@code
    }. + * + * @param doc the Document to parse + * @return the description text, or empty string if not found + */ + protected String parseDescription(Document doc) { + Element section = doc.selectFirst("section.information"); + if (section != null) { + section.select("div.extra-info").remove(); + section.select("a.read-more").remove(); + + // Extract all

    tags and

    elements as separate paragraphs + String description = section.select("p, div").stream() + .filter(el -> el.tagName().equals("p") || el.select("p").isEmpty()) + .filter(el -> !el.hasClass("extra-info") && !el.hasClass("logo")) + .map(Element::text) + .map(text -> text.replace("Les mer", "").trim()) + .filter(text -> !text.isBlank()) + .collect(Collectors.joining("\n\n")); + + // Fallback: if no paragraphs found, get all text from section + if (description.isBlank()) { + description = section.text().trim(); + } + description = description.replace("Les mer", "").trim(); + + // Only return if we found something meaningful + if (!description.isBlank()) { + return description; + } + } + return ""; + } + /** * Fetches the logo URL for the given page by scraping the {@code div.logo img} * element. Results are cached so each URL is only fetched once. @@ -88,10 +103,9 @@ public String fetchLogoUrl(String pageUrl) { Document doc = Jsoup.connect(pageUrl) .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") .timeout(5000).get(); - Element img = doc.selectFirst("div.logo img"); - if (img != null) { - String logoUrl = img.absUrl("src"); + String logoUrl = parseLogoUrl(doc); + if (!logoUrl.isBlank()) { logoCache.put(pageUrl, logoUrl); return logoUrl; } @@ -100,4 +114,20 @@ public String fetchLogoUrl(String pageUrl) { } return null; } + + /** + * Parses the logo URL from a Document by extracting the image src + * from {@code div.logo img}. + * + * @param doc the Document to parse + * @return the absolute logo URL, or empty string if not found + */ + protected String parseLogoUrl(Document doc) { + Element img = doc.selectFirst("div.logo img"); + if (img != null) { + String logoUrl = img.absUrl("src"); + return logoUrl.isBlank() ? "" : logoUrl; + } + return ""; + } } diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java index 8807e61..e78c492 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java @@ -2,8 +2,15 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.mockito.MockedStatic; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; class OrganizationScraperTest { @@ -15,23 +22,23 @@ void setUp() { } @Test - void fetchDescription_ReturnsNullWhenUrlIsNull() { + void fetchDescriptionReturnsNullWhenUrlIsNull() { assertNull(scraper.fetchDescription(null)); } @Test - void fetchDescription_ReturnsNullWhenUrlIsBlank() { + void fetchDescriptionReturnsNullWhenUrlIsBlank() { assertNull(scraper.fetchDescription("")); } @Test - void fetchDescription_ReturnsNullWhenUrlIsInvalid() { + void fetchDescriptionReturnsNullWhenUrlIsInvalid() { String result = scraper.fetchDescription("https://invalid-url-that-does-not-exist-xyz123.com"); assertNull(result); } @Test - void fetchDescription_CachesResultOnSecondCall() { + void fetchDescriptionCachesResultOnSecondCall() { // Mock URLs won't work, but cache still works with null returns scraper.fetchDescription("https://example.com"); scraper.fetchDescription("https://example.com"); @@ -40,23 +47,23 @@ void fetchDescription_CachesResultOnSecondCall() { } @Test - void fetchLogoUrl_ReturnsNullWhenUrlIsNull() { + void fetchLogoUrlReturnsNullWhenUrlIsNull() { assertNull(scraper.fetchLogoUrl(null)); } @Test - void fetchLogoUrl_ReturnsNullWhenUrlIsBlank() { + void fetchLogoUrlReturnsNullWhenUrlIsBlank() { assertNull(scraper.fetchLogoUrl("")); } @Test - void fetchLogoUrl_ReturnsNullWhenUrlIsInvalid() { + void fetchLogoUrlReturnsNullWhenUrlIsInvalid() { String result = scraper.fetchLogoUrl("https://invalid-url-that-does-not-exist-xyz123.com"); assertNull(result); } @Test - void fetchLogoUrl_CachesResultOnSecondCall() { + void fetchLogoUrlCachesResultOnSecondCall() { // Mock URLs won't work, but cache still works with null returns scraper.fetchLogoUrl("https://example.com"); scraper.fetchLogoUrl("https://example.com"); @@ -65,7 +72,7 @@ void fetchLogoUrl_CachesResultOnSecondCall() { } @Test - void fetchDescription_ReturnsCachedValue() { + void fetchDescriptionReturnsCachedValue() { OrganizationScraper scraper = new OrganizationScraper(); // First call - caches (but makes real request) @@ -76,4 +83,68 @@ void fetchDescription_ReturnsCachedValue() { assertEquals(result1, result2); // Same object = cached } -} + + @Test + void fetchDescriptionHandlesExceptionGracefully() { + String result = scraper.fetchDescription("https://invalid-domain-xyz.test"); + assertNull(result); + } + + @Test + void fetchLogoUrlHandlesExceptionGracefully() { + String result = scraper.fetchLogoUrl("https://invalid-domain-xyz.test"); + assertNull(result); + } + + @Test + void parseDescriptionExtractsParagraphs() { + String html = "

    First paragraph

    Second paragraph

    "; + Document doc = Jsoup.parse(html); + assertTrue(scraper.parseDescription(doc).contains("First paragraph")); + } + + @Test + void parseDescriptionUsesFallbackWhenNoParagraphs() { + String html = "
    Fallback text without paragraph tags
    "; + Document doc = Jsoup.parse(html); + assertEquals("Fallback text without paragraph tags", scraper.parseDescription(doc)); + } + + @Test + void parseDescriptionRemovesExtraInfoDivs() { + String html = "

    Keep this

    Remove this
    "; + Document doc = Jsoup.parse(html); + String result = scraper.parseDescription(doc); + assertTrue(result.contains("Keep this")); + assertFalse(result.contains("Remove this")); + } + + @Test + void parseDescriptionFiltersOutLesMer() { + String html = "

    Some text Les mer More text

    "; + Document doc = Jsoup.parse(html); + assertFalse(scraper.parseDescription(doc).contains("Les mer")); + } + + @Test + void parseDescriptionReturnsEmptyStringWhenNoSection() { + String html = "
    No section here
    "; + Document doc = Jsoup.parse(html); + assertEquals("", scraper.parseDescription(doc)); + } + + @Test + void parseLogoUrlExtractsImageUrl() { + String html = "
    "; + Document doc = Jsoup.parse(html); + doc.setBaseUri("https://example.com"); + assertTrue(scraper.parseLogoUrl(doc).contains("logo.png")); + } + + @Test + void parseLogoUrlReturnsEmptyWhenNoImage() { + String html = "
    "; + Document doc = Jsoup.parse(html); + assertEquals("", scraper.parseLogoUrl(doc)); + } +} \ No newline at end of file diff --git a/src/test/java/edu/group5/app/model/user/UserServiceTest.java b/src/test/java/edu/group5/app/model/user/UserServiceTest.java index 104e2f1..459b635 100644 --- a/src/test/java/edu/group5/app/model/user/UserServiceTest.java +++ b/src/test/java/edu/group5/app/model/user/UserServiceTest.java @@ -71,11 +71,11 @@ void registerUserInvalidInputsReturnFalse() { } @Test - void registerUserDuplicateEmailAllowedInCurrentCode() { + void registerUserDuplicateEmailNotAllowedInCurrentCode() { boolean result = service.registerUser("Customer", "John", "Cena", "john.cena@example.com", "$2a$10$hashed"); - assertTrue(result); - assertEquals(3, repo.getUsers().size()); + assertFalse(result); + assertEquals(2, repo.getUsers().size()); } @Test From fe3e366c72f6339b9ccc3af101dc421881cea886 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 21 Apr 2026 14:59:50 +0200 Subject: [PATCH 74/98] fix[OrganizationPageView]: fix picture quality on Organization logo --- .../group5/app/view/organizationpage/OrganizationPageView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index f591d26..7ea3b6b 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -96,7 +96,7 @@ private StackPane createImageContainer() { // Load image in background thread to avoid blocking UI new Thread(() -> { try { - Image image = new Image(org.logoUrl(), 120, 120, true, true); + Image image = new Image(org.logoUrl(), 350, 350, true, true); Platform.runLater(() -> { ImageView logo = new ImageView(image); logo.setId("logo"); From b317fbd1cc0258ded58108a4400d0e93b4ef8649 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 21 Apr 2026 15:07:09 +0200 Subject: [PATCH 75/98] fix[pom.xml]: remove Mockito dependency due to it being unused in the JUnit tests --- pom.xml | 6 ------ .../app/model/organization/OrganizationScraperTest.java | 6 +----- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index 8091ffe..5bbba22 100644 --- a/pom.xml +++ b/pom.xml @@ -73,12 +73,6 @@ jsoup 1.17.2 - - org.mockito - mockito-inline - 5.2.0 - test - diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java index e78c492..5d48428 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java @@ -4,13 +4,9 @@ import org.junit.jupiter.api.Test; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; -import org.mockito.MockedStatic; + import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; class OrganizationScraperTest { From 3748fc5ead2e20a892209745c0043d19bdebf7ab Mon Sep 17 00:00:00 2001 From: Fredrik Jonathan Marjoni Date: Tue, 21 Apr 2026 15:30:25 +0200 Subject: [PATCH 76/98] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e88f3df..af2a3dd 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ The project uses the standard Maven directory structure, which ensures: ## Link to repositoryπŸ“š [//]: # (TODO: Include a link to your GitHub repository here.) -[GitHub Repository - IDATT2003 Mappevurdering 2026](https://github.com/LucyCiara/IDATT2003v26-millions) +[GitHub Repository - IDATT1005 Project Spring 2026](https://git.ntnu.no/Group-5/Help-Me-Help) --- From 1b22059237ab6e601fd9a4d7c1e5e3655600e611 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 21 Apr 2026 15:45:31 +0200 Subject: [PATCH 77/98] Update[Readme]: Add more detailed information about the project --- README.md | 139 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 121 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index af2a3dd..4313ac2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Help-Me-Help - IDATT1005 Team 5 Portofolio Project Spring 2026 :octocat: +# Help-Me-Help - IDATT1005 Team 5 Portofolio Project Spring 2026 :octocat: [//]: # (TODO: Fill inn your name and student ID) [//]: # (TODO: Mappe-2025-Marjoni-fj) @@ -14,7 +14,16 @@ ## Project descriptionπŸ’» [//]: # (TODO: Write a short description of your project/product here.) -This Java-based application, developed using Maven... +Help-Me-Help (HmH) is a Java desktop application, developed using Maven, designed to help users donate money to legitimate charitable organizations and emergency relief initiatives. The application fetches verified organization data from Innsamlingskontrollen (IK), a non-profit foundation that verifies fundraising activities. Users can create profiles, track their donation history, and browse organizations by status (approved/pending). The application prioritizes security, data persistence, and an intuitive user experience following Don Norman's interaction design principles. + +## Key Features + +- User authentication and profile management +- Browse verified and pending organizations with descriptions and logos +- Make donations to organizations +- View donation history +- Data persistence using H2 database +- Web scraping of organization information (descriptions, logos) from external sources ## Project structure πŸ“ @@ -25,19 +34,106 @@ All source files are stored under the `src` directory. ### Main Package Structure (`src/main`)
    -
    +src/main/java/edu/group5/app/
    +β”œβ”€β”€ App.java (JavaFX Application entry point)
    +|
    +β”œβ”€β”€ control/ (Controllers - business logic)
    +β”‚ β”œβ”€β”€ AuthController.java (User login/signup handling)
    +β”‚ β”œβ”€β”€ DonationController.java (Donation processing)
    +β”‚ β”œβ”€β”€ NavigationController.java (Page navigation)
    +β”‚ └── OrganizationController.java (Organization data access)
    +|
    +β”œβ”€β”€ model/ (Business entities & repositories)
    +β”‚ β”œβ”€β”€ organization/
    +β”‚ β”‚ β”œβ”€β”€ Organization.java (Organization entity)
    +β”‚ β”‚ β”œβ”€β”€ OrganizationRepository.java (Data access for organizations)
    +β”‚ β”‚ β”œβ”€β”€ OrganizationService.java (Business logic)
    +β”‚ β”‚ └── OrganizationScraper.java (Web scraping - descriptions & logos)
    +β”‚ β”œβ”€β”€ user/
    +β”‚ β”‚ β”œβ”€β”€ User.java (User base class)
    +β”‚ β”‚ β”œβ”€β”€ Customer.java (Customer implementation)
    +β”‚ β”‚ β”œβ”€β”€ UserRepository.java (Data access for users)
    +β”‚ β”‚ └── UserService.java (User authentication & registration)
    +β”‚ β”œβ”€β”€ donation/
    +β”‚ β”‚ β”œβ”€β”€ Donation.java (Donation entity)
    +β”‚ β”‚ β”œβ”€β”€ DonationRepository.java (Data access for donations)
    +β”‚ β”‚ └── DonationService.java (Donation processing)
    +β”‚ β”œβ”€β”€ wrapper/
    +β”‚ β”‚ β”œβ”€β”€ DbWrapper.java (H2 database connection & operations)
    +β”‚ β”‚ └── OrgApiWrapper.java (Innsamlingskontrollen API client)
    +β”‚ β”œβ”€β”€ AppState.java (Global application state)
    +β”‚ β”œβ”€β”€ Repository.java (Base repository interface)
    +β”‚ └── DBRepository.java (Database repository interface)
    +|
    +β”œβ”€β”€ view/ (JavaFX UI components)
    +β”‚ β”œβ”€β”€ loginpage/
    +β”‚ β”‚ β”œβ”€β”€ LoginPageView.java
    +β”‚ β”‚ β”œβ”€β”€ SignUpPageView.java
    +β”‚ β”‚ β”œβ”€β”€ LoginHeader.java
    +β”‚ β”‚ └── loginpage.css
    +β”‚ β”œβ”€β”€ homepage/
    +β”‚ β”‚ └── HomePageView.java
    +β”‚ β”œβ”€β”€ causespage/
    +β”‚ β”‚ β”œβ”€β”€ CausesPageView.java
    +β”‚ β”‚ β”œβ”€β”€ OrganizationCard.java
    +β”‚ β”‚ └── causespage.css
    +β”‚ β”œβ”€β”€ organizationpage/
    +β”‚ β”‚ β”œβ”€β”€ OrganizationPageView.java
    +β”‚ β”‚ └── organizationpage.css
    +β”‚ β”œβ”€β”€ donationpage/
    +β”‚ β”‚ β”œβ”€β”€ DonationPageView.java
    +β”‚ β”‚ β”œβ”€β”€ PaymentCompletePageView.java
    +β”‚ β”‚ └── donationpage.css
    +β”‚ β”œβ”€β”€ userpage/
    +β”‚ β”‚ └── UserPageView.java
    +β”‚ β”œβ”€β”€ aboutuspage/
    +β”‚ β”‚ └── AboutUsView.java
    +β”‚ └── Header.java
    +|
    +└── utils/
    +└── ParameterValidator.java (Input validation utilities)
    +
    +src/main/resources/
    +β”œβ”€β”€ header/
    +β”‚   └── images/
    +β”‚       └── hmh-logo.png
    +β”œβ”€β”€ loginpage/
    +β”‚   └── loginpage.css
    +β”œβ”€β”€ homepage/
    +β”‚   └── homepage.css
    +β”œβ”€β”€ causespage/
    +β”‚   └── causespage.css
    +β”œβ”€β”€ organizationpage/
     
    + [//]: # (TODO: Describe the structure of your project here. How have you used packages in your structure. Where are all sourcefiles stored. Where are all JUnit-test classes stored. etc.) ### πŸ“¦ Package Responsibilities -#### Models +#### Models: Business logic and data entities + +- `Organization`: Represents a charity/relief organization with status, logo, description +- `User` & `Customer`: User profiles with authentication +- `Donation`: Records of user donations +- Services and Repositories implement the business logic and data access layers -#### Controller +#### Controller: Bridge between UI and business logic -#### View +- `AuthController`: Handles user login/registration with password hashing (BCrypt) +- `OrganizationController`: Manages organization data retrieval and caching +- `DonationController`: Processes donations and updates user history +- `NavigationController`: Coordinates page navigation -#### Utils +#### View: JavaFX UI components + +- Login/signup pages with form validation +- Organization browsing with filtering (approved/pending) +- Donation flow with payment confirmation +- User profile and donation history + +#### Utils: Helper functions + +- `ParameterValidator`: Validates null, empty, and positive values ### JUnit Tests (`src/test`) @@ -49,9 +145,9 @@ The JUnit tests are stored under `src/test` and mirror the main package structur The project uses the standard Maven directory structure, which ensures: -* clean separation of source and test files -* compatibility with IDEs such as IntelliJ, VS Code, and Eclipse -* maintainability and easy future extensions (e.g., persistence or additional views) +- clean separation of source and test files +- compatibility with IDEs such as IntelliJ, VS Code, and Eclipse +- maintainability and easy future extensions (e.g., persistence or additional views) --- @@ -69,9 +165,10 @@ What is the input and output of the program? What is the expected behaviour of t **Requirements:** -* Java JDK 25 -* Maven -* IDE (Ideally IntelliJ or VSCode with Java Extension Pack) +- Java JDK 25 +- Maven +- IDE (Ideally IntelliJ or VSCode with Java Extension Pack) + **Steps:** 1. **Clone repository** @@ -94,12 +191,18 @@ What is the input and output of the program? What is the expected behaviour of t 5. **Input and Output** -* Input: -* Output: +- Input: User interactions (login, organization selection, donation amount) +- Output: JavaFX UI displaying organizations, user profiles, donation confirmations 1. **Excpected behavior:** The program allows the user to: +
  • Login/signup with validation
  • +
  • Browse approved organizations from Innsamlingskontrollen API
  • +
  • View organization details (description, logo, status)
  • +
  • Complete donation workflow
  • +
  • View donation history in user profile
  • +
  • Graceful error handling with user-friendly messages
  • --- ## How to run the tests πŸ§ͺ @@ -107,11 +210,11 @@ The program allows the user to: This project uses JUnit 5 for unit testing. All test classes mirror the main package structure and are stored in `src/test` -* ### Open the Project +- ### Open the Project Open VS Code and select **File > Open Folder**, navigating to the root folder of the project (containing `pom.xml`). -* ### Run all tests +- ### Run all tests To execute the full test suite, run: @@ -127,7 +230,7 @@ This command: --- -* ### Viewing test results +- ### Viewing test results After the tests finish, Maven creates detailed reports here: `target/surefire-reports/` From c4955dea787a2947777370af829e639f9e2a42c7 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:51:22 +0200 Subject: [PATCH 78/98] update[DbWrapper]: add exception throwing for SQLException edgecase for use in testing --- src/main/java/edu/group5/app/model/wrapper/DbWrapper.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java index f357ae1..fd2a2cc 100644 --- a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -230,8 +230,10 @@ private List importDonations(int user_id, boolean all) { * @throws IllegalArgumentException This exception is thrown when data is null, its rows are not * of length 6, any of the rows are null, any of the rows are duplicates or existing rows in * the database, or any of the values in the rows can't be cast to the correct data-types. + * @throws SQLException Is thrown when an unexpected exception like trying to export a number that's + * too big happens. */ - public int exportDonations(List data) throws IllegalArgumentException { + public int exportDonations(List data) throws IllegalArgumentException, SQLException { this.fetchAllDonations(); ParameterValidator.exportChecker(data, "data", this.donations, 6); @@ -263,6 +265,7 @@ public int exportDonations(List data) throws IllegalArgumentException this.logger.info("Donations exported"); } catch (SQLException e) { this.logger.log(Level.SEVERE, "Unexpected SQL exception", e); + throw new SQLException("An unexpected SQL exception has occurred. This might be caused by inserting an item that is too large."); } finally { this.close(null, ps); } From f0430cdec860f44e89ed2feca8273fc3115aa7ae Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:23:18 +0200 Subject: [PATCH 79/98] test[DbWrapper]: add testing for SQLException --- .../model/wrapper/DbWrapperDonationsTest.java | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java b/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java index 1db7a90..f9cc8ff 100644 --- a/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java @@ -7,6 +7,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; @@ -25,12 +26,14 @@ public class DbWrapperDonationsTest { private Object[] cutoffDonation; private Object[] freakyDonation; private Object[] repeatingDonation; + private Object[] tooBigDonation; private List donations; private List donations2; private List donations3; private List repeatedDonations; private List wrongFormatDonations; private List wrongDatatypeDonations; + private List tooBigDonations; private List nullList; private static final int PRECISION = 5; @@ -40,10 +43,10 @@ public class DbWrapperDonationsTest { @BeforeEach void init() { this.db = new DbWrapper(true); - String[] firstNames = new String[] { "John", "Jane", "Cutoff", "Freaky", "Repeating" }; - String[] lastNames = new String[] { "Doe", "Doe", "Joh", "Bill", "JoeJoe" }; + String[] firstNames = new String[] { "John", "Jane", "Cutoff", "Freaky", "Repeating", "Big" }; + String[] lastNames = new String[] { "Doe", "Doe", "Joh", "Bill", "JoeJoe", "Willy" }; this.users = new ArrayList(); - for (int i = 0; i < 5; i++) { + for (int i = 0; i < firstNames.length; i++) { Object[] row = new Object[6]; row[0] = i + 1; row[1] = "Customer"; @@ -84,6 +87,13 @@ void init() { this.wrongDatatypeDonations = new ArrayList(); this.wrongDatatypeDonations.add(freakyDonation); + this.tooBigDonation = new Object[] { + 6, 6, 999999, new BigDecimal("9999999999999999999999999999999"), + new Timestamp(new Date().getTime()), "Azerbaijani technologies" + }; + this.tooBigDonations = new ArrayList(); + this.tooBigDonations.add(tooBigDonation); + Object[] nullRow = new Object[] {null, null, null, null, null, null}; this.nullList = new ArrayList(); this.nullList.add(nullRow); @@ -150,7 +160,7 @@ public void wronglyDatatypedDonationsThrowsExpectedException() { @Test public void addingSameDonationTwiceThrowsExpectedException() { assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); - assertEquals(1, this.db.exportDonations(this.donations)); + assertDoesNotThrow(() -> assertEquals(1, this.db.exportDonations(this.donations))); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { this.db.exportDonations(this.donations); }); @@ -163,7 +173,7 @@ public void addingSameDonationTwiceThrowsExpectedException() { public void addingSameDonationTwiceThrowsExpectedException2() { assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); assertTrue(this.db.importDonations((int) this.users.get(1)[0]).size() == 0); - assertEquals(2, this.db.exportDonations(this.donations2)); + assertDoesNotThrow(() -> assertEquals(2, this.db.exportDonations(this.donations2))); IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, () -> { @@ -212,8 +222,20 @@ public void addingDonationListWithNullInRowThrowsExpectedException() { @Test public void dataIsEmptyAfterExportingAndImportingEmptyList() { assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); - assertEquals(0, this.db.exportDonations(new ArrayList())); + assertDoesNotThrow(() -> assertEquals(0, this.db.exportDonations(new ArrayList()))); assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); assertTrue(this.db.disconnect()); } + + @Test + public void exportTooBigNumberThrowsSQLException() { + SQLException exception = assertThrows(SQLException.class, () -> { + this.db.exportDonations(this.tooBigDonations); + }); + assertEquals( + "An unexpected SQL exception has occurred. " + + "This might be caused by inserting an item that is too large.", + exception.getMessage() + ); + } } From d6016d5cca6ebfb7ab435b6c41ca1deb9eb9c3b4 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:41:55 +0200 Subject: [PATCH 80/98] update[DbWrapper]: add exception throwing for exportUsers in case of SQLException --- src/main/java/edu/group5/app/model/wrapper/DbWrapper.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java index fd2a2cc..3325b8c 100644 --- a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -132,8 +132,10 @@ public List importUsers() { * @throws IllegalArgumentException This exception is thrown when data is null, its rows are not * of length 6, any of the rows are null, any of the rows are duplicates or existing rows in * the database, or any of the values in the rows can't be cast to the correct data-types. + * @throws SQLException Is thrown when an unexpected exception like trying to export a number + * that's too big happens. */ - public int exportUsers(List data) throws IllegalArgumentException { + public int exportUsers(List data) throws IllegalArgumentException, SQLException { this.importUsers(); ParameterValidator.exportChecker(data, "data", this.users, 6); @@ -163,6 +165,7 @@ public int exportUsers(List data) throws IllegalArgumentException { this.logger.info("Users exported"); } catch (SQLException e) { this.logger.log(Level.SEVERE, "Unexpected SQL exception", e); + throw new SQLException("An unexpected SQL exception has occurred. This might be caused by inserting an item that is too large."); } finally { this.close(null, ps); } From 35f4d49fe2aa902f180ab8c3f61e5bd49bb09f86 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:46:12 +0200 Subject: [PATCH 81/98] test&update[DbWrapper]: add test for exporting user with too long String and handle exception throwing in DonationsTest --- .../model/wrapper/DbWrapperDonationsTest.java | 6 ++++- .../app/model/wrapper/DbWrapperUserTest.java | 27 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java b/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java index f9cc8ff..289b803 100644 --- a/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java @@ -99,7 +99,11 @@ void init() { this.nullList.add(nullRow); this.db.connect(); - this.db.exportUsers(users); + try { + this.db.exportUsers(users); + } catch (Exception e) { + // This exception won't happen + } } private static boolean donationEquals(Object[] array1, Object[] array2) { diff --git a/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java b/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java index 00107de..e8621d8 100644 --- a/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -20,12 +21,14 @@ public class DbWrapperUserTest { private Object[] cutoffJoh; private Object[] freakyBill; private Object[] repeatingJoeJoe; + private Object[] bigWilly; private List users; private List users2; private List users3; private List repeatedUsers; private List wrongFormatUsers; private List wrongDatatypeUsers; + private List tooBigUsers; private List nullList; private DbWrapper db; @@ -60,6 +63,12 @@ void init() { this.wrongDatatypeUsers = new ArrayList(); this.wrongDatatypeUsers.add(freakyBill); + this.bigWilly = new Object[] { + 6, "Customer", "Big", "Willy", "bigdwilly@waaaaaytoolargemail.com", "passssssssssssword" + }; + this.tooBigUsers = new ArrayList(); + this.tooBigUsers.add(this.bigWilly); + Object[] nullRow = new Object[] {null, null, null, null, null, null}; this.nullList = new ArrayList(); this.nullList.add(nullRow); @@ -122,7 +131,7 @@ public void wronglyDatatypedUsersThrowsExpectedException() { public void addingSameUserTwiceThrowsExpectedException() { assertTrue(this.db.connect()); assertTrue(this.db.importUsers().size() == 0); - assertEquals(1, this.db.exportUsers(this.users)); + assertDoesNotThrow(() -> assertEquals(1, this.db.exportUsers(this.users))); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { this.db.exportUsers(this.users); }); @@ -135,7 +144,7 @@ public void addingSameUserTwiceThrowsExpectedException() { public void addingSameUserTwiceThrowsExpectedException2() { assertTrue(this.db.connect()); assertTrue(this.db.importUsers().size() == 0); - assertEquals(2, this.db.exportUsers(this.users2)); + assertDoesNotThrow(() -> assertEquals(2, this.db.exportUsers(this.users2))); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { this.db.exportUsers(this.users); }); @@ -179,4 +188,18 @@ public void addingUserListWithNullInRowThrowsExpectedException() { assertTrue(this.db.disconnect()); assertEquals("One or more rows in data contains null values", exception.getMessage()); } + + @Test + public void exportTooLongNameThrowsSQLException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + SQLException exception = assertThrows(SQLException.class, () -> { + this.db.exportUsers(this.tooBigUsers); + }); + assertEquals( + "An unexpected SQL exception has occurred. " + + "This might be caused by inserting an item that is too large.", + exception.getMessage() + ); + } } From 9fbe278b908494822d40053a31474b99414adc8b Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 21 Apr 2026 21:38:03 +0200 Subject: [PATCH 82/98] feat[AuthController]: add restrictions for user input that's too long Add checks for user input that's too long and show corresponding error messages in GUI --- .../group5/app/control/AuthController.java | 106 +++++++++++++++--- 1 file changed, 88 insertions(+), 18 deletions(-) diff --git a/src/main/java/edu/group5/app/control/AuthController.java b/src/main/java/edu/group5/app/control/AuthController.java index 11c3288..347a52e 100644 --- a/src/main/java/edu/group5/app/control/AuthController.java +++ b/src/main/java/edu/group5/app/control/AuthController.java @@ -9,6 +9,10 @@ import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** @@ -74,44 +78,110 @@ public User getCurrentUser() { * @param passwordChars the user's password */ public void handleSignUp(SignUpPageView view, String firstName, String lastName, String email, char[] passwordChars) { - if (firstName == null || firstName.trim().isEmpty() || - lastName == null || lastName.trim().isEmpty() || - email == null || email.trim().isEmpty() || - passwordChars == null || passwordChars.length == 0) { + if (firstName == null || firstName.trim().isEmpty() + || lastName == null || lastName.trim().isEmpty() + || email == null || email.trim().isEmpty() + || passwordChars == null || passwordChars.length == 0) { view.showError("All fields are required"); return; } - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - - // Clears password char array after creating a hash. - String hashedPassword = encoder.encode(new String(passwordChars)); - for (int i = 0; i < passwordChars.length; i++) { - passwordChars[i] = '\u0000'; + if (firstName.length() > 32 || lastName.length() > 32 + || email.length() > 32 || passwordChars.length > 72) { + + HashMap> fields = new HashMap>(); + List fields32 = new ArrayList(); + List fields72 = new ArrayList(); + fields.put("32", fields32); + fields.put("72", fields72); + + if (firstName.length() > 32) { + fields32.add("First Name"); + } + if (lastName.length() > 32) { + fields32.add("Last Name"); + } + if (email.length() > 32) { + fields32.add("Email"); + } + if (passwordChars.length > 72) { + fields72.add("Password"); + } + + int length32 = fields.get("32").size(); + int length72 = fields.get("72").size(); + + String string32 = ""; + if (length32 > 0) { + if (length32 > 1) { + for (int i = 0; i < length32; i++) { + if (i == length32 - 1) { + string32 += String.format("and %s", fields.get("32").get(i)); + } else { + string32 += String.format("%s, ", fields.get("32").get(i)); + } + } + string32 = string32 + " must have lengths of 32 characters.\n"; + } else { + string32 = fields.get("32").getFirst() + " must have a length of 32 characters.\n"; + } + } + + String string72 = ""; + if (length72 > 0) { + if (length72 > 1) { + for (int i = 0; i < length72; i++) { + if (i == length72 - 1) { + string72 += String.format("and %s", fields.get("72").get(i)); + } else { + string72 += String.format("%s, ", fields.get("72").get(i)); + } + } + string72 = string72 + " must have lengths of 72 characters.\n"; + } else { + string72 = fields.get("72").getFirst() + + " must have a length of 72 characters.\n"; + } + } + + view.showError(string32 + string72 + "Try again."); + return; } Alert privacyPolicy = new Alert(Alert.AlertType.CONFIRMATION); privacyPolicy.setTitle("Accept Privacy Policy"); privacyPolicy.setHeaderText("Accept Privacy Policy"); privacyPolicy.setContentText( - "Your user information like:\n" + - "Name and emailβ€”as well as donations tied to your accountβ€”will be saved locally on your machine.\n" + - "This information is only used to create your account, and no data will be sold to third parties.\n" + - "By creating an account, you accept the right of our app to store this information on your computer."); + "Your user information like:\n" + + "Name and emailβ€”as well as donations tied to your accountβ€”" + + "will be saved locally on your machine.\n" + + "This information is only used to create your account," + + "and no data will be sold to third parties.\n" + + "By creating an account," + + "you accept the right of our app to store this information on your computer."); + + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + // Clears password char array after creating a hash. + String hashedPassword = encoder.encode(new String(passwordChars)); + for (int i = 0; i < passwordChars.length; i++) { + passwordChars[i] = '\u0000'; + } if (privacyPolicy.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { boolean success = userService.registerUser( "Customer", firstName, lastName, email, hashedPassword); if (success) { + User user = userService.getUserByEmail(email); appState.setCurrentUser(user); - nav.showHomePage(); - } else { - view.showError("Registration failed. Email may already be in use."); + nav.showHomePage(); + } else { + view.showError("Registration failed. Email may already be in use."); + } } } -} + /** * Handles the login of a {@link User}. From 58d80d9fd90864f89cfe8f72818d82bfe2b1d549 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 21 Apr 2026 21:39:36 +0200 Subject: [PATCH 83/98] fix[SignUpPageView]: add text wrapping to prevent longer error messages from being cut off --- src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java b/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java index bdcfb54..70f40ea 100644 --- a/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java @@ -66,6 +66,7 @@ public char[] getPassword() { public void showError(String message) { errorLabel.setText(message); + errorLabel.setWrapText(true); errorLabel.setStyle("-fx-text-fill: red;"); } @@ -87,7 +88,6 @@ private VBox getSignUpBox() { private Label getErrorLabel() { errorLabel = new Label(); - errorLabel.setPrefHeight(20); return errorLabel; } From 8d9da1f6f0845daa51f245b1b3416425f3aab42d Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 21 Apr 2026 21:57:54 +0200 Subject: [PATCH 84/98] update[readme]: update readme with more profound information --- README.md | 110 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 4313ac2..be470d5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [//]: # (TODO: Fill inn your name and student ID) [//]: # (TODO: Mappe-2025-Marjoni-fj) -**STUDENT NAME** +**TEAM 5 STUDENT NAMES**
  • Emil Fagerjord
  • Mathea Gjerde
  • @@ -14,7 +14,10 @@ ## Project descriptionπŸ’» [//]: # (TODO: Write a short description of your project/product here.) -Help-Me-Help (HmH) is a Java desktop application, developed using Maven, designed to help users donate money to legitimate charitable organizations and emergency relief initiatives. The application fetches verified organization data from Innsamlingskontrollen (IK), a non-profit foundation that verifies fundraising activities. Users can create profiles, track their donation history, and browse organizations by status (approved/pending). The application prioritizes security, data persistence, and an intuitive user experience following Don Norman's interaction design principles. +Help-Me-Help (HmH) is a Java desktop application, developed using Maven, designed to help users donate money to legitimate charitable organizations and emergency relief initiatives. +The application fetches verified organization data from Innsamlingskontrollen (IK), a non-profit foundation that verifies fundraising activities. +Users can create profiles, track their donation history, and browse organizations by status (approved/pending). +The application prioritizes security, data persistence, and an intuitive user experience following Don Norman's interaction design principles. ## Key Features @@ -35,37 +38,37 @@ All source files are stored under the `src` directory.
     src/main/java/edu/group5/app/
    -β”œβ”€β”€ App.java (JavaFX Application entry point)
    +β”œβ”€β”€ App.java                                      (JavaFX Application entry point)
     |
    -β”œβ”€β”€ control/ (Controllers - business logic)
    -β”‚ β”œβ”€β”€ AuthController.java (User login/signup handling)
    -β”‚ β”œβ”€β”€ DonationController.java (Donation processing)
    -β”‚ β”œβ”€β”€ NavigationController.java (Page navigation)
    -β”‚ └── OrganizationController.java (Organization data access)
    +β”œβ”€β”€ control/                                      (Controllers - business logic)
    +β”‚ β”œβ”€β”€ AuthController.java                         (User login/signup handling)
    +β”‚ β”œβ”€β”€ DonationController.java                     (Donation processing)
    +β”‚ β”œβ”€β”€ NavigationController.java                   (Page navigation)
    +β”‚ └── OrganizationController.java                 (Organization data access)
     |
    -β”œβ”€β”€ model/ (Business entities & repositories)
    +β”œβ”€β”€ model/                                        (Business entities & repositories)
     β”‚ β”œβ”€β”€ organization/
    -β”‚ β”‚ β”œβ”€β”€ Organization.java (Organization entity)
    -β”‚ β”‚ β”œβ”€β”€ OrganizationRepository.java (Data access for organizations)
    -β”‚ β”‚ β”œβ”€β”€ OrganizationService.java (Business logic)
    -β”‚ β”‚ └── OrganizationScraper.java (Web scraping - descriptions & logos)
    +β”‚ β”‚ β”œβ”€β”€ Organization.java                         (Organization entity)
    +β”‚ β”‚ β”œβ”€β”€ OrganizationRepository.java               (Data access for organizations)
    +β”‚ β”‚ β”œβ”€β”€ OrganizationService.java                  (Business logic)
    +β”‚ β”‚ └── OrganizationScraper.java                  (Web scraping - descriptions & logos)
     β”‚ β”œβ”€β”€ user/
    -β”‚ β”‚ β”œβ”€β”€ User.java (User base class)
    -β”‚ β”‚ β”œβ”€β”€ Customer.java (Customer implementation)
    -β”‚ β”‚ β”œβ”€β”€ UserRepository.java (Data access for users)
    -β”‚ β”‚ └── UserService.java (User authentication & registration)
    +β”‚ β”‚ β”œβ”€β”€ User.java                                 (User base class)
    +β”‚ β”‚ β”œβ”€β”€ Customer.java                             (Customer implementation)
    +β”‚ β”‚ β”œβ”€β”€ UserRepository.java                       (Data access for users)
    +β”‚ β”‚ └── UserService.java                          (User authentication & registration)
     β”‚ β”œβ”€β”€ donation/
    -β”‚ β”‚ β”œβ”€β”€ Donation.java (Donation entity)
    -β”‚ β”‚ β”œβ”€β”€ DonationRepository.java (Data access for donations)
    -β”‚ β”‚ └── DonationService.java (Donation processing)
    +β”‚ β”‚ β”œβ”€β”€ Donation.java                             (Donation entity)
    +β”‚ β”‚ β”œβ”€β”€ DonationRepository.java                   (Data access for donations)
    +β”‚ β”‚ └── DonationService.java                      (Donation processing)
     β”‚ β”œβ”€β”€ wrapper/
    -β”‚ β”‚ β”œβ”€β”€ DbWrapper.java (H2 database connection & operations)
    -β”‚ β”‚ └── OrgApiWrapper.java (Innsamlingskontrollen API client)
    -β”‚ β”œβ”€β”€ AppState.java (Global application state)
    -β”‚ β”œβ”€β”€ Repository.java (Base repository interface)
    -β”‚ └── DBRepository.java (Database repository interface)
    +β”‚ β”‚ β”œβ”€β”€ DbWrapper.java                            (H2 database connection & operations)
    +β”‚ β”‚ └── OrgApiWrapper.java                        (Innsamlingskontrollen API client)
    +β”‚ β”œβ”€β”€ AppState.java                               (Global application state)
    +β”‚ β”œβ”€β”€ Repository.java                             (Base repository interface)
    +β”‚ └── DBRepository.java                           (Database repository interface)
     |
    -β”œβ”€β”€ view/ (JavaFX UI components)
    +β”œβ”€β”€ view/                                         (JavaFX UI components)
     β”‚ β”œβ”€β”€ loginpage/
     β”‚ β”‚ β”œβ”€β”€ LoginPageView.java
     β”‚ β”‚ β”œβ”€β”€ SignUpPageView.java
    @@ -91,7 +94,7 @@ src/main/java/edu/group5/app/
     β”‚ └── Header.java
     |
     └── utils/
    -└── ParameterValidator.java (Input validation utilities)
    +└── ParameterValidator.java                       (Input validation utilities)
     
     src/main/resources/
     β”œβ”€β”€ header/
    @@ -139,6 +142,32 @@ src/main/resources/
     
     The JUnit tests are stored under `src/test` and mirror the main package structure. These tests cover both positive and negative test of all classes (except `App.java` and UI classes) and their methods ensuring program reliability according to the specification given in the portofolie project descriptions
     
    +src/test/java/edu/group5/app/
    +β”œβ”€β”€ AppTest.java                                    (Application startup tests)
    +β”œβ”€β”€ control/
    +β”‚   └── (Controller integration tests - to be added)
    +β”œβ”€β”€ model/
    +β”‚   β”œβ”€β”€ donation/
    +β”‚   β”‚   β”œβ”€β”€ DonationRepositoryTest.java            (Data access layer tests)
    +β”‚   β”‚   β”œβ”€β”€ DonationServiceTest.java               (Business logic tests)
    +β”‚   β”‚   └── DonationTest.java                      (Entity tests)
    +β”‚   β”œβ”€β”€ organization/
    +β”‚   β”‚   β”œβ”€β”€ OrganizationRepositoryTest.java        (Data access layer tests)
    +β”‚   β”‚   β”œβ”€β”€ OrganizationScraperTest.java           (Web scraping tests - 86% coverage)
    +β”‚   β”‚   β”œβ”€β”€ OrganizationServiceTest.java           (Business logic tests)
    +β”‚   β”‚   └── OrganizationTest.java                  (Entity tests)
    +β”‚   β”œβ”€β”€ user/
    +β”‚   β”‚   β”œβ”€β”€ CustomerTest.java                      (Customer entity tests)
    +β”‚   β”‚   β”œβ”€β”€ UserRepositoryTest.java                (Data access layer tests)
    +β”‚   β”‚   └── UserServiceTest.java                   (Authentication & registration tests)
    +β”‚   └── wrapper/
    +β”‚       β”œβ”€β”€ DbWrapperDonationsTest.java            (Database wrapper tests - donations)
    +β”‚       β”œβ”€β”€ DbWrapperUserTest.java                 (Database wrapper tests - users)
    +β”‚       └── OrgApiWrapperTest.java                 (API client tests)
    +β”œβ”€β”€ utils/
    +β”‚   └── ParameterValidatorTest.java                (Input validation tests)
    +└── view/
    +    └── ViewTest.java                              (UI component tests)
     
    ### Maven Layout @@ -175,10 +204,10 @@ What is the input and output of the program? What is the expected behaviour of t Clone the Repository from GitHub 2. **Open the Project:** - Open VS Code and select **File > Open Folder**, navigating to the root folder of the project (containing `pom.xml`). + Open your IDE and select **File > Open Folder**, navigating to the root folder of the project (containing `pom.xml`). 3. **Build the Project:** - Open the terminal in VS Code (`Ctrl + ~`) and run: + Open the terminal in yoru IDE (`Ctrl + ~`) and run: ```bash mvn clean compile @@ -195,14 +224,17 @@ What is the input and output of the program? What is the expected behaviour of t - Output: JavaFX UI displaying organizations, user profiles, donation confirmations 1. **Excpected behavior:** +
    + The program allows the user to: -
  • Login/signup with validation
  • -
  • Browse approved organizations from Innsamlingskontrollen API
  • -
  • View organization details (description, logo, status)
  • -
  • Complete donation workflow
  • -
  • View donation history in user profile
  • -
  • Graceful error handling with user-friendly messages
  • +- Login/signup with validation +- Browse approved organizations from Innsamlingskontrollen API +- View organization details (description, logo, status) +- Complete donation workflow +- View donation history in user profile +- Graceful error handling with user-friendly messages + --- ## How to run the tests πŸ§ͺ @@ -212,7 +244,7 @@ All test classes mirror the main package structure and are stored in `src/test` - ### Open the Project - Open VS Code and select **File > Open Folder**, navigating to the root folder of the project (containing `pom.xml`). + Open your IDE and select **File > Open Folder**, navigating to the root folder of the project (containing `pom.xml`). - ### Run all tests @@ -246,9 +278,9 @@ Each report includes: ## References πŸ”— -[//]: # (TODO: Include references here, if any. For example, if you have used code from the course book, include a reference to the chapter. -Or if you have used code from a website or other source, include a link to the source.) -References are included in the project report. +Innsamlingskontrollen API: +
    +For more references, they are included in the project report --- """ From 7ddebccb69dad14c224a80c4a6c4553e2dfca137 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 21 Apr 2026 22:09:09 +0200 Subject: [PATCH 85/98] update[*Readme]: update readme with better show of project structure and better display of references --- README.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index be470d5..9425d62 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Help-Me-Help - IDATT1005 Team 5 Portofolio Project Spring 2026 :octocat: +# Help-Me-Help - IDATT1005 Team 5 Portfolio Project Spring 2026 :octocat: [//]: # (TODO: Fill inn your name and student ID) [//]: # (TODO: Mappe-2025-Marjoni-fj) @@ -96,17 +96,20 @@ src/main/java/edu/group5/app/ └── utils/ └── ParameterValidator.java (Input validation utilities) -src/main/resources/ +src/main/resources/ (Static assets - CSS, images, etc.) β”œβ”€β”€ header/ β”‚ └── images/ -β”‚ └── hmh-logo.png +β”‚ └── hmh-logo.png (Application logo) β”œβ”€β”€ loginpage/ -β”‚ └── loginpage.css +β”‚ └── loginpage.css (Login/signup page styling) β”œβ”€β”€ homepage/ -β”‚ └── homepage.css +β”‚ └── homepage.css (Home page styling) β”œβ”€β”€ causespage/ -β”‚ └── causespage.css +β”‚ └── causespage.css (Organization browsing styling) β”œβ”€β”€ organizationpage/ +β”‚ └── organizationpage.css (Organization details styling) +└── donationpage/ + └── donationpage.css (Donation flow styling)
    [//]: # (TODO: Describe the structure of your project here. How have you used packages in your structure. Where are all sourcefiles stored. Where are all JUnit-test classes stored. etc.) @@ -207,7 +210,7 @@ What is the input and output of the program? What is the expected behaviour of t Open your IDE and select **File > Open Folder**, navigating to the root folder of the project (containing `pom.xml`). 3. **Build the Project:** - Open the terminal in yoru IDE (`Ctrl + ~`) and run: + Open the terminal in your IDE (`Ctrl + ~`) and run: ```bash mvn clean compile @@ -223,7 +226,7 @@ What is the input and output of the program? What is the expected behaviour of t - Input: User interactions (login, organization selection, donation amount) - Output: JavaFX UI displaying organizations, user profiles, donation confirmations -1. **Excpected behavior:** +1. **Expected behavior:**
    The program allows the user to: @@ -278,9 +281,10 @@ Each report includes: ## References πŸ”— -Innsamlingskontrollen API: -
    -For more references, they are included in the project report +For more references and project details, kindly refer yourself to the project report and Wiki pages + +- **GitHub Wiki**: [GitHub Wiki](https://git.ntnu.no/Group-5/Help-Me-Help/wiki) + +- **Innsamlingskontrollen API**: [Innsamlingskontrollen](https://app.innsamlingskontrollen.no/api/public/v1/all) --- -""" From f22ad07c693b102e8d54bceb7251c2c94d098074 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 21 Apr 2026 23:46:16 +0200 Subject: [PATCH 86/98] feat[DonationController]: prevent donations that are too large or with too many decimals Add error message when trying to donate an amount of money that has too many digits or decimals. --- src/main/java/edu/group5/app/control/AuthController.java | 2 ++ .../java/edu/group5/app/control/DonationController.java | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/edu/group5/app/control/AuthController.java b/src/main/java/edu/group5/app/control/AuthController.java index 347a52e..d3d4ebe 100644 --- a/src/main/java/edu/group5/app/control/AuthController.java +++ b/src/main/java/edu/group5/app/control/AuthController.java @@ -86,6 +86,7 @@ public void handleSignUp(SignUpPageView view, String firstName, String lastName, return; } + // Checks if any input is too long. if (firstName.length() > 32 || lastName.length() > 32 || email.length() > 32 || passwordChars.length > 72) { @@ -148,6 +149,7 @@ public void handleSignUp(SignUpPageView view, String firstName, String lastName, return; } + // Privacy policy pop-up. Alert privacyPolicy = new Alert(Alert.AlertType.CONFIRMATION); privacyPolicy.setTitle("Accept Privacy Policy"); privacyPolicy.setHeaderText("Accept Privacy Policy"); diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 07af6fd..3ca392d 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -178,6 +178,12 @@ private void handleDonate() { return; } + // Prevents donations that are too complex from being made + if (amount.stripTrailingZeros().precision() > 32 || amount.stripTrailingZeros().scale() > 16) { + this.showError("The number is too complex, please donate a smaller or less precise number"); + return; + } + // Create donation via service boolean success = service.donate( customer, From 64088ce2104fb59ba161ee3497b15cef399ee3fb Mon Sep 17 00:00:00 2001 From: Fredrik Jonathan Marjoni Date: Wed, 22 Apr 2026 09:11:26 +0200 Subject: [PATCH 87/98] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9425d62..5e1b8da 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ This command: --- -- ### Viewing test results +### Viewing test results After the tests finish, Maven creates detailed reports here: `target/surefire-reports/` From 88f566319b1c4b3c59275a1ee6815f45bbab8a83 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:26:27 +0200 Subject: [PATCH 88/98] docs[README]: clean up TODOs --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 5e1b8da..7d77679 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ # Help-Me-Help - IDATT1005 Team 5 Portfolio Project Spring 2026 :octocat: - -[//]: # (TODO: Fill inn your name and student ID) -[//]: # (TODO: Mappe-2025-Marjoni-fj) - **TEAM 5 STUDENT NAMES** +
  • Emil Fagerjord
  • Mathea Gjerde
  • @@ -13,7 +10,6 @@ ## Project descriptionπŸ’» -[//]: # (TODO: Write a short description of your project/product here.) Help-Me-Help (HmH) is a Java desktop application, developed using Maven, designed to help users donate money to legitimate charitable organizations and emergency relief initiatives. The application fetches verified organization data from Innsamlingskontrollen (IK), a non-profit foundation that verifies fundraising activities. Users can create profiles, track their donation history, and browse organizations by status (approved/pending). From 1d94ded1a4f1725f33180213fb0ee76619773808 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:35:22 +0200 Subject: [PATCH 89/98] chore&update[README]: update installation guide and remove TODOs --- README.md | 54 +++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 7d77679..a15b28c 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,6 @@ src/main/resources/ (Static assets - CSS, images, └── donationpage.css (Donation flow styling) -[//]: # (TODO: Describe the structure of your project here. How have you used packages in your structure. Where are all sourcefiles stored. Where are all JUnit-test classes stored. etc.) ### πŸ“¦ Package Responsibilities @@ -177,47 +176,49 @@ The project uses the standard Maven directory structure, which ensures: - compatibility with IDEs such as IntelliJ, VS Code, and Eclipse - maintainability and easy future extensions (e.g., persistence or additional views) ---- - ## Link to repositoryπŸ“š -[//]: # (TODO: Include a link to your GitHub repository here.) -[GitHub Repository - IDATT1005 Project Spring 2026](https://git.ntnu.no/Group-5/Help-Me-Help) - ---- +https://git.ntnu.no/Group-5/Help-Me-Help ## How to run the projectπŸ“ -[//]: # (TODO: Describe how to run your project here. What is the main class? What is the main method? -What is the input and output of the program? What is the expected behaviour of the program?) - **Requirements:** - Java JDK 25 - Maven -- IDE (Ideally IntelliJ or VSCode with Java Extension Pack) +- JavaFX SDK 25.0.1-- -**Steps:** +**Run With Maven:** (Windows + Mac + Linux) -1. **Clone repository** - Clone the Repository from GitHub +1. **Download and Unzip Project:** + Download project zip from the repository. -2. **Open the Project:** - Open your IDE and select **File > Open Folder**, navigating to the root folder of the project (containing `pom.xml`). +2. **Navigate to Project Folder:** + Navigate to project folder in the terminal. + + ```bash + cd path/to/project/ (linux + mac) + cd path\to\project\ (windows) + ``` -3. **Build the Project:** - Open the terminal in your IDE (`Ctrl + ~`) and run: +3. **Run the Application:** + Start the program by running the main class: + + ```bash + mvn javafx:run + ``` + ``` - ```bash - mvn clean compile - -4. **Run the Application:** - Start the program by running the main class: +**Run From JAR: (Windows) + +1. Download Project JAR: + Go to repository and download the JAR. + +2. Run the JAR in Terminal: ```bash - mvn javafx:run - -5. **Input and Output** + java --module-path "path\to\javafx\sdk" --add-modules javafx.controls -jar path\to\jar.jar + - Input: User interactions (login, organization selection, donation amount) - Output: JavaFX UI displaying organizations, user profiles, donation confirmations @@ -273,7 +274,6 @@ Each report includes: 3. Execution times 4. Running tests in an IDE -[//]: # (TODO: Describe how to run the tests here.) ## References πŸ”— From 17470d8e2b4c0e7f0a676c293423043603375a4e Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 23 Apr 2026 12:41:06 +0200 Subject: [PATCH 90/98] fix[readme]: change interface to abstract when elaborating project structure --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a15b28c..45d2bd0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Help-Me-Help - IDATT1005 Team 5 Portfolio Project Spring 2026 :octocat: + **TEAM 5 STUDENT NAMES**
    @@ -61,8 +62,8 @@ src/main/java/edu/group5/app/ β”‚ β”‚ β”œβ”€β”€ DbWrapper.java (H2 database connection & operations) β”‚ β”‚ └── OrgApiWrapper.java (Innsamlingskontrollen API client) β”‚ β”œβ”€β”€ AppState.java (Global application state) -β”‚ β”œβ”€β”€ Repository.java (Base repository interface) -β”‚ └── DBRepository.java (Database repository interface) +β”‚ β”œβ”€β”€ Repository.java (Base repository abstract) +β”‚ └── DBRepository.java (Database repository abstract) | β”œβ”€β”€ view/ (JavaFX UI components) β”‚ β”œβ”€β”€ loginpage/ @@ -108,7 +109,6 @@ src/main/resources/ (Static assets - CSS, images, └── donationpage.css (Donation flow styling) - ### πŸ“¦ Package Responsibilities #### Models: Business logic and data entities @@ -178,7 +178,7 @@ The project uses the standard Maven directory structure, which ensures: ## Link to repositoryπŸ“š -https://git.ntnu.no/Group-5/Help-Me-Help + ## How to run the projectπŸ“ @@ -207,6 +207,7 @@ https://git.ntnu.no/Group-5/Help-Me-Help ```bash mvn javafx:run ``` + ``` @@ -216,6 +217,7 @@ https://git.ntnu.no/Group-5/Help-Me-Help Go to repository and download the JAR. 2. Run the JAR in Terminal: + ```bash java --module-path "path\to\javafx\sdk" --add-modules javafx.controls -jar path\to\jar.jar @@ -274,7 +276,6 @@ Each report includes: 3. Execution times 4. Running tests in an IDE - ## References πŸ”— For more references and project details, kindly refer yourself to the project report and Wiki pages From 02d4379375b9f92cf2f7afa690f8fb8b337b1e68 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 23 Apr 2026 12:43:24 +0200 Subject: [PATCH 91/98] fix[readme]: fix markdown styling in readme --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 45d2bd0..ac54a61 100644 --- a/README.md +++ b/README.md @@ -208,10 +208,7 @@ The project uses the standard Maven directory structure, which ensures: mvn javafx:run ``` - ``` - - -**Run From JAR: (Windows) +**Run From JAR: (Windows)** 1. Download Project JAR: Go to repository and download the JAR. From c8f7beb8ca9c37bf1ea13f32cc7f69a5fb0e4311 Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Thu, 23 Apr 2026 17:10:09 +0200 Subject: [PATCH 92/98] docs: added javadoc to UserPageView --- .../edu/group5/app/view/userpage/UserPageView.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index cf6f801..aff5b32 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -23,7 +23,18 @@ import java.text.SimpleDateFormat; import java.util.*; - +/** + * A view for the user profile page. + * + *

    Displays the current user's profile information, + * including avatar, full name, email and location, along with a logout button. + * The page is divided into three sections: + * Profile information, Supported causes, and previous donations.

    + * + *

    Supported causes shows all the organizations that the user has donated to. + * Previous donations shows a list of all the user's donations, + * and includes a search bar to filter through them.

    + */ public class UserPageView extends BorderPane { private final NavigationController nav; private final AuthController authController; From a09f2d2cfb125a536786f4118d38d36528a04cae Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Thu, 23 Apr 2026 17:11:09 +0200 Subject: [PATCH 93/98] docs: fixed parameter name in javadoc in OrganizationController --- .../java/edu/group5/app/control/OrganizationController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/control/OrganizationController.java b/src/main/java/edu/group5/app/control/OrganizationController.java index 6ffe830..49201aa 100644 --- a/src/main/java/edu/group5/app/control/OrganizationController.java +++ b/src/main/java/edu/group5/app/control/OrganizationController.java @@ -33,7 +33,7 @@ public OrganizationController(AppState appState, OrganizationService service) { /** * Sets the current selected organization. - * @param organization the organization to set as current + * @param org the organization to set as current */ public void setCurrentOrganization(Organization org) { appState.setCurrentOrganization(org); From d8098885f0ecf6f9478814028596ae84f462bf04 Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Thu, 23 Apr 2026 21:21:06 +0200 Subject: [PATCH 94/98] docs: changed parameter name in javadoc --- src/main/java/edu/group5/app/model/user/UserRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/model/user/UserRepository.java b/src/main/java/edu/group5/app/model/user/UserRepository.java index 07f9ca8..ea63bb8 100644 --- a/src/main/java/edu/group5/app/model/user/UserRepository.java +++ b/src/main/java/edu/group5/app/model/user/UserRepository.java @@ -14,7 +14,7 @@ public class UserRepository extends DBRepository { * Constructs UserRepository using Hashmap, * and extends the content from DBRepository. * - * @param content the underlying map used to store users, + * @param rows the underlying map used to store users, * where the key represents the user ID */ public UserRepository(List rows) { From 2330f3ad4c47bbacbe23ccc2c39af852c3754b93 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 23 Apr 2026 22:10:07 +0200 Subject: [PATCH 95/98] chore[user]: Add JavaDoc to UserRepository --- .../edu/group5/app/model/user/UserRepository.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/model/user/UserRepository.java b/src/main/java/edu/group5/app/model/user/UserRepository.java index ea63bb8..892b11c 100644 --- a/src/main/java/edu/group5/app/model/user/UserRepository.java +++ b/src/main/java/edu/group5/app/model/user/UserRepository.java @@ -6,7 +6,11 @@ import edu.group5.app.model.DBRepository; import edu.group5.app.utils.ParameterValidator; - +/** + * Repository class for managing User entities. It provides methods to retrieve users, + * find users by their unique identifier or email address, and initializes the repository with input data. + * The repository uses a HashMap to store User objects for efficient retrieval based on their unique identifier. + */ public class UserRepository extends DBRepository { public final static String ROLE_CUSTOMER = "Customer"; @@ -69,6 +73,11 @@ public List export() { } + /** + * Retrieves a copy of the current users in the repository. + * @return a HashMap containing the current users, + * where the key is the user ID and the value is the User object + */ public HashMap getUsers() { return new HashMap<>(content); } From 75915e5614a3b92bb4a662817c139b2a979b3407 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 23 Apr 2026 22:18:07 +0200 Subject: [PATCH 96/98] chore[wrapper]: add more proper JavaDoc to the Wrapper class description --- src/main/java/edu/group5/app/model/wrapper/DbWrapper.java | 4 +++- src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java | 3 ++- src/main/java/edu/group5/app/model/wrapper/Wrapper.java | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java index 3325b8c..d0239ae 100644 --- a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -14,7 +14,9 @@ import java.util.logging.Logger; /** - * A class for wrapping the database. + * A class for wrapping the database. It provides methods for connecting and disconnecting to the database, + * importing and exporting users and donations, and handling SQL exceptions. + * The class uses a logger to log important events and exceptions that occur during database operations. */ public class DbWrapper { protected Connection connection; diff --git a/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java b/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java index 2a493e4..d2ebb1a 100644 --- a/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java @@ -10,7 +10,8 @@ import tools.jackson.databind.ObjectMapper; /** - * A Class for Wrapping an API. + * A Class for Wrapping an API. + * It provides methods for importing data from the API and accessing the imported data. */ public class OrgApiWrapper extends Wrapper { private Object[] data; diff --git a/src/main/java/edu/group5/app/model/wrapper/Wrapper.java b/src/main/java/edu/group5/app/model/wrapper/Wrapper.java index 85d8dc8..cc54c29 100644 --- a/src/main/java/edu/group5/app/model/wrapper/Wrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/Wrapper.java @@ -2,6 +2,9 @@ /** * An abstract class for all Wrappers of datasets. + * This class defines the structure for dataset wrappers, which are responsible for importing data from various sources + * and providing access to the imported data. Each wrapper must implement the importData method to handle the specific + * data import logic and the getData method to return the imported data in a suitable format. */ abstract class Wrapper { From 46addb5ba6b4377750ed66a8ccdaeb8c0ff4c2e9 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 23 Apr 2026 22:46:19 +0200 Subject: [PATCH 97/98] chore[user]: format JavaDoc to be more compact --- .../java/edu/group5/app/model/user/UserRepository.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/group5/app/model/user/UserRepository.java b/src/main/java/edu/group5/app/model/user/UserRepository.java index 892b11c..aec3e9d 100644 --- a/src/main/java/edu/group5/app/model/user/UserRepository.java +++ b/src/main/java/edu/group5/app/model/user/UserRepository.java @@ -7,9 +7,11 @@ import edu.group5.app.model.DBRepository; import edu.group5.app.utils.ParameterValidator; /** - * Repository class for managing User entities. It provides methods to retrieve users, - * find users by their unique identifier or email address, and initializes the repository with input data. - * The repository uses a HashMap to store User objects for efficient retrieval based on their unique identifier. + * Repository class for managing User entities. + * It provides methods to retrieve users, find users by their unique identifier + * or email address, and initializes the repository with input data. + * The repository uses a HashMap to store + * User objects for efficient retrieval based on their unique identifier. */ public class UserRepository extends DBRepository { public final static String ROLE_CUSTOMER = "Customer"; From ae6d4bca84a7d20a5195c9677824bc57b2e4b082 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Fri, 24 Apr 2026 00:18:38 +0200 Subject: [PATCH 98/98] chore[README]: add guide to generate and access javadoc html --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a15b28c..f4eddd7 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ https://git.ntnu.no/Group-5/Help-Me-Help **Run From JAR: (Windows) 1. Download Project JAR: - Go to repository and download the JAR. + Go to repository and download the JAR from the jar release. 2. Run the JAR in Terminal: ```bash @@ -274,6 +274,18 @@ Each report includes: 3. Execution times 4. Running tests in an IDE +## Accessing the javadocs +The javadoc can be generated and accessed using the following commands. + +1. Generate the Javadoc HTML + ```bash + mvn javadoc:javadoc + ``` + ``` + ``` + +2. Open the HTML file + open ```target/reports/apidocs/index.html``` ## References πŸ”—