diff --git a/README.md b/README.md index f186dda..15bdbf7 100644 --- a/README.md +++ b/README.md @@ -1 +1,297 @@ -# Help-Me-Help +# Help-Me-Help - IDATT1005 Team 5 Portfolio Project Spring 2026 :octocat: + +**TEAM 5 STUDENT NAMES** + +
+
  • Emil Fagerjord
  • +
  • Mathea Gjerde
  • +
  • Fredrik Jonathan Marjoni
  • +
  • Lucy Ciara Herud-Thomassen
  • +
    + +## Project descriptionπŸ’» + +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 πŸ“ + +--- +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`) + +
    +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 abstract)
    +β”‚ └── DBRepository.java                           (Database repository abstract)
    +|
    +β”œβ”€β”€ 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/                               (Static assets - CSS, images, etc.)
    +β”œβ”€β”€ header/
    +β”‚   └── images/
    +β”‚       └── hmh-logo.png                          (Application logo)
    +β”œβ”€β”€ loginpage/
    +β”‚   └── loginpage.css                             (Login/signup page styling)
    +β”œβ”€β”€ homepage/
    +β”‚   └── homepage.css                              (Home page styling)
    +β”œβ”€β”€ causespage/
    +β”‚   └── causespage.css                            (Organization browsing styling)
    +β”œβ”€β”€ organizationpage/
    +β”‚   └── organizationpage.css                      (Organization details styling)
    +└── donationpage/
    +    └── donationpage.css                          (Donation flow styling)
    +
    + +### πŸ“¦ Package Responsibilities + +#### 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: Bridge between UI and business logic + +- `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 + +#### 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`) + +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 + +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πŸ“š + + + +## How to run the projectπŸ“ + +**Requirements:** + +- Java JDK 25 +- Maven +- JavaFX SDK 25.0.1-- + +**Run With Maven:** (Windows + Mac + Linux) + +1. **Download and Unzip Project:** + Download project zip from the repository. + +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. **Run the Application:** + Start the program by running the main class: + + ```bash + mvn javafx:run + ``` + +**Run From JAR: (Windows)** + +1. Download Project JAR: + Go to repository and download the JAR from the jar release. + +2. Run the JAR in Terminal: + + ```bash + 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 + +1. **Expected 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 πŸ§ͺ + +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 your IDE 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 + +## 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 πŸ”— + +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) + +--- diff --git a/pom.xml b/pom.xml index f9a8b88..5bbba22 100644 --- a/pom.xml +++ b/pom.xml @@ -55,7 +55,7 @@ org.springframework spring-core - 6.1.10 + 6.2.0 org.slf4j @@ -68,6 +68,11 @@ 2.2.224 runtime + + org.jsoup + jsoup + 1.17.2 + diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index d657fb8..3aea332 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -1,19 +1,21 @@ 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.donation.Donation; +import edu.group5.app.control.NavigationController; +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; +import edu.group5.app.model.organization.OrganizationScraper; import edu.group5.app.model.organization.OrganizationService; -import edu.group5.app.model.user.User; 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 edu.group5.app.view.aboutuspage.AboutUsView; 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; @@ -28,9 +30,13 @@ public class App extends Application { DbWrapper dbWrapper; UserRepository userRepository; DonationRepository donationRepository; + + BorderPane root; + AppState appState; + NavigationController nav; + private Logger logger; - private MainController controller; - private Scene scene; + static int MAX_RETRIES = 3; @Override public void init() { @@ -40,8 +46,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 @@ -59,28 +71,29 @@ 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); - - this.controller = new MainController(userService, donationService, organizationService); - - this.scene = controller.getMainView().getScene(); + OrganizationService organizationService = new OrganizationService(organizationRepository, orgScraper); + this.root = new BorderPane(); + this.appState = new AppState(); + this.nav = new NavigationController(root, appState, userService, donationService, organizationService, this.getHostServices()); } @Override public void start(Stage stage) { - this.controller.showLoginPage(); + this.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(this.scene); + stage.setScene(scene); stage.show(); } diff --git a/src/main/java/edu/group5/app/control/AuthController.java b/src/main/java/edu/group5/app/control/AuthController.java new file mode 100644 index 0000000..d3d4ebe --- /dev/null +++ b/src/main/java/edu/group5/app/control/AuthController.java @@ -0,0 +1,233 @@ +package edu.group5.app.control; + +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; +import javafx.scene.control.ButtonType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +/** + * Controller responsible for authentication-related operations. + * + *

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

    + *

    + */ +public class AuthController { + private final AppState appState; + private final NavigationController nav; + 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; + } + + + /** + * 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}. + * + * + * + *

    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 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) { + view.showError("All fields are required"); + return; + } + + // Checks if any input is too long. + 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; + } + + // Privacy policy pop-up. + 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."); + + 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."); + } + } + } + + + /** + * Handles the login of a {@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) { + 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"); + } + } + + /** + * 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(); + } +} \ No newline at end of file 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 9f86271..0000000 --- a/src/main/java/edu/group5/app/control/BrowseCardController.java +++ /dev/null @@ -1,16 +0,0 @@ -package edu.group5.app.control; - -import edu.group5.app.model.organization.Organization; - -public class BrowseCardController { - private final MainController controller; - - public BrowseCardController(MainController mainController) { - this.controller = mainController; - } - - public void handleCardClick(Organization organization) { - controller.setCurrentOrganization(organization); - 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/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java new file mode 100644 index 0000000..3ca392d --- /dev/null +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -0,0 +1,222 @@ +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 edu.group5.app.utils.ParameterValidator; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; + +/** + * 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; + private final NavigationController nav; + 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; + } + + /** + * 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. + * + * @param userId the ID of the user + * @return a map of donations. + */ + public Map getUserDonations(int userId) { + ParameterValidator.intChecker(userId, "User ID"); + return service.getUserDonations(userId); + } + + /** + * Returns a set of unique organization IDs that the current user + * has donated to. + * + * @return a set of organization IDs + */ + public Set getUniqueOrganizationIDs() { + 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()) { + uniqueOrganizations.add(donation.organizationId()); + } + + return uniqueOrganizations; + } + + /** + * 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 requestDonationConfirmation() { + // Get session data + User currentUser = appState.getCurrentUser(); + Organization currentOrg = appState.getCurrentOrganization(); + BigDecimal amount = appState.getCurrentDonationAmount(); + String paymentMethod = appState.getCurrentPaymentMethod(); + + // Validate before showing dialog + if (currentUser == null) { + showError("Error: No user logged in"); + return; + } + if (!(currentUser instanceof Customer)) { + showError("Error: Only customers can donate"); + return; + } + if (currentOrg == null) { + showError("Error: No organization selected"); + return; + } + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + 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" + + "Payment Method: " + paymentMethod + "\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(); + String paymentMethod = appState.getCurrentPaymentMethod(); + + if (!(currentUser instanceof Customer customer)) { + System.err.println("Error: Only customers can donate"); + return; + } + if (paymentMethod == null) { + System.out.println("Error: Invalid payment method"); + 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, + currentOrg.orgNumber(), + amount, + paymentMethod + ); + + if (success) { + 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"); + errorAlert.setContentText(message); + errorAlert.showAndWait(); + } +} 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 b28f191..0000000 --- a/src/main/java/edu/group5/app/control/LoginPageController.java +++ /dev/null @@ -1,44 +0,0 @@ -package edu.group5.app.control; - -import edu.group5.app.model.user.User; -import edu.group5.app.model.user.UserService; -import edu.group5.app.view.loginpage.LoginPageView; - -public class LoginPageController { - private final MainController controller; - private final UserService userService; - private LoginPageView view; - - public LoginPageController(MainController controller, UserService userService) { - this.controller = controller; - this.userService = userService; - } - - public void setView(LoginPageView view) { - this.view = view; - } - - public void handleLoginBtn() { - String email = view.getEmail(); - char[] passwordChars = view.getPassword(); - - 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) { - controller.setCurrentUser(user); - controller.showHomePage(); - } else { - view.showError("Invalid email or password"); - } - } - - 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 deleted file mode 100644 index 2b75487..0000000 --- a/src/main/java/edu/group5/app/control/MainController.java +++ /dev/null @@ -1,121 +0,0 @@ -package edu.group5.app.control; - -import edu.group5.app.control.donationpage.DonationPageController; -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.MainView; -import edu.group5.app.view.donationpage.DonationPageView; - -import java.math.BigDecimal; - -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 UserService userService; - private final DonationService donationService; - private final OrganizationService organizationService; - private User currentUser; - private Organization currentOrganization; - private BigDecimal currentDonationAmount; - - public MainController(UserService userService, DonationService donationService, - OrganizationService organizationService) { - this.userService = userService; - this.donationService = donationService; - this.organizationService = organizationService; - - this.view = new MainView(this, userService); - 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); - } - - public UserService getUserService() { - return userService; - } - - public DonationService getDonationService() { - return donationService; - } - - public OrganizationService getOrganizationService() { - return organizationService; - } - - public void setCurrentUser(User user) { - this.currentUser = user; - } - - public User getCurrentUser() { - return this.currentUser; - } - - public void setCurrentOrganization(Organization organization) { - this.currentOrganization = organization; - } - - public Organization getCurrentOrganization() { - return this.currentOrganization; - } - - public void setCurrentDonationAmount(BigDecimal amount) { - this.currentDonationAmount = amount; - } - - public BigDecimal getCurrentDonationAmount() { - return this.currentDonationAmount; - } - - public void logout() { - currentUser = null; - currentOrganization = null; - currentDonationAmount = null; - showLoginPage(); - } - - public MainView getMainView() { - return view; - } - - public void showHomePage() { - view.showHomePage(homePageController, headerController); - } - - public void showLoginPage() { - view.showLoginPage(); - } - public void showSignInPage() { - view.showSignInPage(); - } - public void showPaymentCompletePage() { - view.showPaymentCompletePage(); - } - public void showBrowsePage() { - view.showBrowsePage(browsePageController, browseCardController, headerController); - } - - public void showOrganizationPage() { - view.showOrganizationPage(organizationPageController, headerController); - } - - public void showDonationPage() { - view.showDonationPage(); - } - - public void showAboutUsPage() {} - - public void showUserPage() { - view.showUserPage(); - } -} diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java new file mode 100644 index 0000000..8bbbbb3 --- /dev/null +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -0,0 +1,150 @@ +package edu.group5.app.control; + +import edu.group5.app.model.AppState; +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.aboutuspage.AboutUsView; +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.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. + * 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; + private final Header header; + private final LoginHeader loginHeader; + + private final AppState appState; + private final 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, 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.appState = appState; + this.hostServices = hostServices; + + this.authController = new AuthController(appState, this, userService); + this.donationController = new DonationController(appState, this, donationService); + this.organizationController = new OrganizationController(appState, 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(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(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(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(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(this, organizationController, 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(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() { + new AboutUsView(hostServices).displayAboutUs(); + } + + /** + * 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(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 new file mode 100644 index 0000000..49201aa --- /dev/null +++ b/src/main/java/edu/group5/app/control/OrganizationController.java @@ -0,0 +1,62 @@ +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; + +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 AppState appState; + private final 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 org 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); + } + + public Map getTrustedOrganizations() { + return service.getTrustedOrganizations(); + } + + public CompletableFuture> getOrganizationsWithLogosAsync() { + return service.getTrustedOrganizationsWithLogosAsync(); + } +} 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/SignInPageController.java b/src/main/java/edu/group5/app/control/SignInPageController.java deleted file mode 100644 index 2d8d874..0000000 --- a/src/main/java/edu/group5/app/control/SignInPageController.java +++ /dev/null @@ -1,58 +0,0 @@ -package edu.group5.app.control; -import edu.group5.app.model.user.UserService; -import edu.group5.app.view.loginpage.SignInPageView; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; - -import edu.group5.app.model.user.User; - -public class SignInPageController { - private final MainController controller; - private final UserService userService; - private SignInPageView view; - - public SignInPageController(MainController controller, UserService userService) { - this.controller = controller; - this.userService = userService; - } - - public void setView(SignInPageView view) { - this.view = view; - } - - public void handleSignInBtn() { - String firstName = view.getFirstName(); - String lastName = view.getLastName(); - String email = view.getEmail(); - char[] passwordChars = view.getPassword(); - - 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); - - controller.setCurrentUser(user); - controller.showHomePage(); - } else { - view.showError("Registration failed. Email may already be in use."); - } - } - - 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 5fed3e2..0000000 --- a/src/main/java/edu/group5/app/control/donationpage/DonationPageController.java +++ /dev/null @@ -1,60 +0,0 @@ -package edu.group5.app.control.donationpage; - -import edu.group5.app.control.MainController; -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; - -public class DonationPageController { - private final MainController controller; - - public DonationPageController(MainController controller) { - this.controller = controller; - } - public void handleDonationBtn() { - // Get session data from MainController - User currentUser = controller.getCurrentUser(); - Organization currentOrg = controller.getCurrentOrganization(); - BigDecimal amount = controller.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 = controller.getDonationService().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 - controller.setCurrentDonationAmount(null); - - // Navigate to payment complete - 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/model/AppState.java b/src/main/java/edu/group5/app/model/AppState.java new file mode 100644 index 0000000..e17bda9 --- /dev/null +++ b/src/main/java/edu/group5/app/model/AppState.java @@ -0,0 +1,86 @@ +package edu.group5.app.model; + +import edu.group5.app.model.organization.Organization; +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 currentPaymentMethod; + + /** + * 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.currentPaymentMethod; + } + + /** + * Sets the current payment method. + * @param paymentMethod the payment method to set as the current payment method + */ + public void setCurrentPaymentMethod(String paymentMethod){ + this.currentPaymentMethod = paymentMethod; + } +} diff --git a/src/main/java/edu/group5/app/model/DBRepository.java b/src/main/java/edu/group5/app/model/DBRepository.java index 3cd1fa9..d36d0e2 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,12 +25,24 @@ 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<>(); } + /** + * 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 032f307..8b4ec61 100644 --- a/src/main/java/edu/group5/app/model/Repository.java +++ b/src/main/java/edu/group5/app/model/Repository.java @@ -2,8 +2,15 @@ import java.util.Map; +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; @@ -13,14 +20,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; } - - /** - * Gets the content of the repo - * @return content of the repo - */ - public Map getContent() { - return 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..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,11 +1,12 @@ 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; 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,32 +28,28 @@ 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; } /** - * 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/Organization.java b/src/main/java/edu/group5/app/model/organization/Organization.java index 42844e1..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,12 +2,15 @@ import java.util.Objects; +import edu.group5.app.utils.ParameterValidator; + /** * Represents an organization. * *

    * 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 +18,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 +27,8 @@ public record Organization( boolean trusted, String websiteUrl, boolean isPreApproved, - String description) { + String description, + String logoUrl) { /** * Creates a new organization. * @@ -35,21 +40,21 @@ 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) { - if (orgNumber < 0) { - throw new IllegalArgumentException("orgNumber cannot be negative"); - } + String description, String logoUrl) { + ParameterValidator.intChecker(orgNumber, "Organization number"); this.orgNumber = orgNumber; this.name = Objects.requireNonNull(name, "name cannot be null"); this.trusted = trusted; 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"); 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..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; @@ -11,39 +12,49 @@ * 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<>(); - if (input == null) { - throw new IllegalArgumentException("The input cannot be null"); - } + public OrganizationRepository(Object[] input, OrganizationScraper scraper) { + super(new HashMap<>()); + this.grandMap = new HashMap<>(); + ParameterValidator.objectChecker(input, "Input data"); + ParameterValidator.objectChecker(scraper, "Scraper"); + this.scraper = scraper; + ObjectMapper mapper = new ObjectMapper(); for (Object obj : input) { 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"); 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; - Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description); + 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); } @@ -92,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); } @@ -105,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 new file mode 100644 index 0000000..beb16aa --- /dev/null +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -0,0 +1,133 @@ +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.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 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 ConcurrentHashMap<>(); + private final Map descriptionCache = new ConcurrentHashMap<>(); + + /** + * 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 && 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(); + + 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); + } + 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. + * + * @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 && 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(); + + String logoUrl = parseLogoUrl(doc); + if (!logoUrl.isBlank()) { + logoCache.put(pageUrl, logoUrl); + return logoUrl; + } + } catch (Exception e) { + System.out.println("Could not get logo for: " + 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/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java index c5979f5..df4d04e 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -1,33 +1,38 @@ package edu.group5.app.model.organization; +import java.util.stream.Collectors; + +import edu.group5.app.utils.ParameterValidator; + +import java.util.Comparator; +import java.util.LinkedHashMap; 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 delegating to OrganizationScraper.

    + * + * Fetched logo URLs are cached to avoid redundant network requests. */ public class OrganizationService { private OrganizationRepository organizationRepository; + 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) { - if (organizationRepository == null) { - throw new IllegalArgumentException("OrganizationRepository cannot be null"); - } + public OrganizationService(OrganizationRepository organizationRepository, OrganizationScraper scraper) { + ParameterValidator.objectChecker(organizationRepository, "OrganizationRepository"); + ParameterValidator.objectChecker(scraper, "Scraper"); this.organizationRepository = organizationRepository; - } - - /** - * Getter for the OrganizationRepository used by this service. - * @return the OrganizationRepository instance used by this service - */ - public OrganizationRepository getOrganizationRepository() { - return this.organizationRepository; + this.scraper = scraper; } /** @@ -35,7 +40,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)); } /** @@ -53,6 +61,56 @@ 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); } + + /** + * 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 + */ + + /** + * Fetches all trusted organizations with their logo URLs and descriptions. + * + *

    + * 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 + */ + public Map getTrustedOrganizationsWithLogos() { + Map original = getTrustedOrganizations(); + return original.values().parallelStream() + .map(org -> new Organization( + org.orgNumber(), + org.name(), + org.trusted(), + org.websiteUrl(), + org.isPreApproved(), + org.description(), + scraper.fetchLogoUrl(org.websiteUrl()) + )) + .sorted(Comparator.comparing(Organization::name)) + .collect(Collectors.toMap(Organization::orgNumber, org -> org, (e1, e2) -> e1, LinkedHashMap::new)); + } + + /** + * 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); + } } 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..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,8 @@ package edu.group5.app.model.user; + 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 +30,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..aec3e9d 100644 --- a/src/main/java/edu/group5/app/model/user/UserRepository.java +++ b/src/main/java/edu/group5/app/model/user/UserRepository.java @@ -5,7 +5,14 @@ import java.util.Map; 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"; @@ -13,14 +20,12 @@ 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) { 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"); @@ -70,6 +75,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); } @@ -83,9 +93,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 +112,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 +128,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 +144,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 628c785..e6439f1 100644 --- a/src/main/java/edu/group5/app/model/user/UserService.java +++ b/src/main/java/edu/group5/app/model/user/UserService.java @@ -1,60 +1,64 @@ package edu.group5.app.model.user; +import edu.group5.app.utils.ParameterValidator; + /** - * 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) { - if (userRepository == null) { - throw new IllegalArgumentException("UserRepository cannot be null"); - } + ParameterValidator.objectChecker(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 - * 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 +67,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 +90,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 */ 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 57% 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..d0239ae 100644 --- a/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -1,5 +1,6 @@ -package edu.group5.app.control.wrapper; +package edu.group5.app.model.wrapper; +import edu.group5.app.utils.ParameterValidator; import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; @@ -8,13 +9,15 @@ 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; +/** + * 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; 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,8 +62,14 @@ public boolean connect() { } } + /** + * Disconnects the database connection, logging failures. + * + * @return True if successful, false if not. + */ public boolean disconnect() { - try{ this.connection.close(); } catch (Exception e) {}; + // 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(); } catch (Exception e) { @@ -59,16 +78,29 @@ 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; - try{ + try { ps = this.connection.prepareStatement("SELECT * FROM users"); results = ps.executeQuery(); List data = new ArrayList(); @@ -93,37 +125,34 @@ 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. + * @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, SQLException { this.importUsers(); - - 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.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, 6); PreparedStatement ps = null; 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]); @@ -138,16 +167,28 @@ public int exportUsers(List data) { 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); } 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 +196,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,34 +226,34 @@ 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. + * @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, SQLException { 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, 6); PreparedStatement ps = null; 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++) { @@ -229,6 +270,7 @@ public int exportDonations(List data) { 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); } 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 88% 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..d2ebb1a 100644 --- a/src/main/java/edu/group5/app/control/wrapper/OrgApiWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java @@ -1,5 +1,6 @@ -package edu.group5.app.control.wrapper; +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; @@ -9,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; @@ -24,11 +26,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(); 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 69% 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..cc54c29 100644 --- a/src/main/java/edu/group5/app/control/wrapper/Wrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/Wrapper.java @@ -1,7 +1,10 @@ -package edu.group5.app.control.wrapper; +package edu.group5.app.model.wrapper; /** * 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 { 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..28a8dc0 --- /dev/null +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -0,0 +1,127 @@ +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 + * 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.trim().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 { + 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)); + } + } + + /** + * 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, int rowLength + ) throws IllegalArgumentException { + 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 %d", dataName, rowLength) + ); + } + 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"); + } + } + } + } +} 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/main/java/edu/group5/app/view/Header.java b/src/main/java/edu/group5/app/view/Header.java index 1edc5e4..6bc6133 100644 --- a/src/main/java/edu/group5/app/view/Header.java +++ b/src/main/java/edu/group5/app/view/Header.java @@ -1,16 +1,24 @@ package edu.group5.app.view; -import edu.group5.app.control.HeaderController; +import edu.group5.app.control.NavigationController; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.image.Image; 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 HeaderController controller; + private final NavigationController controller; - public Header(HeaderController controller) { + public Header(NavigationController controller) { this.controller = controller; getStylesheets().add(getClass().getResource("/header/header.css").toExternalForm()); setId("header"); @@ -24,7 +32,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 +52,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 +71,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/MainView.java b/src/main/java/edu/group5/app/view/MainView.java deleted file mode 100644 index e35935f..0000000 --- a/src/main/java/edu/group5/app/view/MainView.java +++ /dev/null @@ -1,86 +0,0 @@ -package edu.group5.app.view; - -import edu.group5.app.control.*; -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.User; -import edu.group5.app.model.user.UserService; -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.LoginPageView; -import edu.group5.app.view.loginpage.SignInPageView; -import edu.group5.app.view.userpage.UserPageView; -import edu.group5.app.view.organizationpage.OrganizationPageView; -import javafx.scene.Scene; -import javafx.scene.layout.BorderPane; - -public class MainView { - private final MainController mainController; - 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 MainView(MainController mainController, UserService userService) { - this.mainController = mainController; - this.headerController = new HeaderController(mainController); - this.homePageController = new HomePageController(mainController); - this.loginPageController = new LoginPageController(mainController, userService); - this.signInPageController = new SignInPageController(mainController, userService); - this.donationPageController = new DonationPageController(mainController); - this.paymentCompleteController = new PaymentCompleteController(mainController); - this.root = new BorderPane(); - this.scene = new Scene(root, 1280, 720); - } - - public Scene getScene() { - return this.scene; - } - - public Scene createView() { - root.setCenter(new LoginPageView(loginPageController)); - return new Scene(root, 1280, 720); - } - - public void showHomePage(HomePageController homePageController, HeaderController headerController) { - root.setCenter(new HomePageView(homePageController, headerController)); - } - - public void showLoginPage() { - root.setCenter(new LoginPageView(loginPageController)); - } - - public void showBrowsePage(BrowsePageController browsePageController, BrowseCardController browseCardController, HeaderController headerController) { - root.setCenter(new BrowsePageView(getScene(), browsePageController, browseCardController, headerController, mainController)); - } - - public void showOrganizationPage(OrganizationPageController organizationController, HeaderController headerController) { - root.setCenter(new OrganizationPageView(organizationController, headerController, mainController)); - } - - public void showSignInPage() { - root.setCenter(new SignInPageView(signInPageController)); - } - public void showDonationPage() { - root.setCenter(new DonationPageView(donationPageController, headerController, mainController)); - } - public void showPaymentCompletePage() { - root.setCenter(new PaymentCompletePageView(paymentCompleteController)); - } - - public void showAboutUsPage() {} - - public void showUserPage() { - User currentUser = mainController.getCurrentUser(); - if (currentUser != null) { - root.setCenter(new UserPageView(headerController, mainController)); - } - } -} 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(); + } +} diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java deleted file mode 100644 index ad4be8a..0000000 --- a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java +++ /dev/null @@ -1,79 +0,0 @@ -package edu.group5.app.view.browsepage; - -import edu.group5.app.control.BrowseCardController; -import edu.group5.app.model.organization.Organization; -import javafx.geometry.Pos; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; -import javafx.scene.text.Text; - -public class BrowseCard extends VBox { - private final BrowseCardController controller; - private final Organization organization; - - public BrowseCard(BrowseCardController browseCardController, Organization org, String img) { - this.controller = browseCardController; - this.organization = org; - setId("mainContainer"); - getStylesheets().add(getClass().getResource("/browsepage/browse_org.css").toExternalForm()); - - getChildren().addAll( - imageContainer(img), - orgName(org.name()), - checkMarkContainer() - ); - - setOnMouseClicked(e -> { - controller.handleCardClick(organization); - }); - - setSpacing(10); - setFillWidth(true); - setAlignment(Pos.CENTER); - } - - 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); - - imageContainer.getChildren().add(logo); - return imageContainer; - } - - private Text orgName(String text) { - Text orgName = new Text(text); - orgName.setId("orgName"); - orgName.setWrappingWidth(150); - return orgName; - } - - private StackPane checkMarkContainer() { - StackPane checkMarkContainer = new StackPane(); - checkMarkContainer.setId("checkMarkContainer"); - checkMarkContainer.setAlignment(Pos.CENTER_LEFT); - - ImageView verifiedCheck = new ImageView( - new Image(getClass().getResource("/verified_check.png").toExternalForm()) - ); - - verifiedCheck.setPreserveRatio(true); - verifiedCheck.setSmooth(true); - - checkMarkContainer.getChildren().add(verifiedCheck); - return checkMarkContainer; - } -} diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java deleted file mode 100644 index a2d92c8..0000000 --- a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java +++ /dev/null @@ -1,145 +0,0 @@ -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.control.MainController; -import edu.group5.app.model.organization.Organization; -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.*; - -import java.util.Map; -import java.util.stream.Collectors; - -public class BrowsePageView extends BorderPane { - private final Scene scene; - private final BrowsePageController controller; - private final BrowseCardController orgController; - private final MainController mainController; - private GridPane organizationGrid; - private Map allOrganizations; - - public BrowsePageView(Scene mainScene, BrowsePageController browsePageController, BrowseCardController browseCardController, HeaderController headerController, MainController mainController) { - this.scene = mainScene; - this.controller = browsePageController; - this.orgController = browseCardController; - this.mainController = mainController; - getStylesheets().add(getClass().getResource("/browsepage/browsepage.css").toExternalForm()); - Header headerView = new Header(headerController); - setTop(headerView); - setCenter(createBody()); - } - - private ScrollPane createBody() { - ScrollPane body = new ScrollPane(); - body.setId("body"); - body.setFitToWidth(true); - body.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - body.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); - - VBox vBox = new VBox(); - vBox.setStyle("-fx-padding: 10;"); - vBox.setSpacing(10); - vBox.setMaxWidth(Double.MAX_VALUE); - vBox.getChildren().addAll( - createSearchSection(), - createOrganizationSection(null) - ); - body.setContent(vBox); - return body; - } - - private HBox createSearchSection() { - HBox searchSection = new HBox(); - TextField searchField = new TextField(); - searchField.setPromptText("Search organizations..."); - - // Add listener for search text changes - searchField.textProperty().addListener((obs, oldVal, newVal) -> { - updateOrganizationGrid(newVal.trim()); - }); - - searchSection.getChildren().add(searchField); - return searchSection; - } - - private GridPane createOrganizationSection(String searchTerm) { - GridPane grid = new GridPane(); - grid.setId("card-grid"); - grid.setHgap(20); - grid.setVgap(20); - grid.setStyle("-fx-padding: 0;"); - grid.setMaxWidth(Double.MAX_VALUE); - - if (allOrganizations == null) { - allOrganizations = mainController.getOrganizationService().getTrustedOrganizations(); - } - - // Filter organizations by search term - Map organizations = filterOrganizations(searchTerm); - - int column = 0; - int row = 0; - - for (Organization org : organizations.values()) { - String defaultImg = "/browsepage/images/children_of_shambala.png"; - BrowseCard card = new BrowseCard(orgController, org, defaultImg); - - grid.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); - grid.getColumnConstraints().add(col); - } - - // Store reference for later updates - if (organizationGrid == null) { - organizationGrid = grid; - } - - return grid; - } - - private Map filterOrganizations(String searchTerm) { - // If no search term, return all organizations - if (searchTerm == null || searchTerm.isEmpty()) { - return allOrganizations; - } - - String lowerSearchTerm = searchTerm.toLowerCase(); - return allOrganizations.values().stream() - .filter(org -> org.name().toLowerCase().contains(lowerSearchTerm)) - .collect(Collectors.toMap( - Organization::orgNumber, - org -> org - )); - } - - private void updateOrganizationGrid(String searchTerm) { - if (organizationGrid == null) { - return; - } - - 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()); - } -} diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java new file mode 100644 index 0000000..0bdddbc --- /dev/null +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -0,0 +1,246 @@ +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 javafx.application.Platform; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +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; + +/** + * 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 NavigationController nav; + private final OrganizationController orgController; + + private GridPane organizationGrid; + private Map allOrganizations; + private Map cardCache = new HashMap<>(); + + public CausesPageView(NavigationController nav, OrganizationController orgController) { + ParameterValidator.objectChecker(nav, "NavigationController"); + ParameterValidator.objectChecker(orgController, "OrganizationController"); + + this.nav = nav; + this.orgController = orgController; + + getStylesheets().add(getClass().getResource("/browsepage/browsepage.css").toExternalForm()); + setCenter(createBody()); + } + + private BorderPane createBody() { + BorderPane bodyRoot = new BorderPane(); + bodyRoot.setTop(createSearchSection()); + + ScrollPane body = new ScrollPane(); + body.setId("body"); + body.setFitToWidth(true); + body.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + body.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); + + VBox vBox = new VBox(); + vBox.setStyle("-fx-padding: 10;"); + vBox.setSpacing(10); + vBox.setMaxWidth(Double.MAX_VALUE); + + // Load organizations INSTANTLY from cache + allOrganizations = orgController.getTrustedOrganizations(); + + 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<>(); + 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 = orgController.getCurrentOrganization(); + if (currentOrg != null && orgs.containsKey(currentOrg.orgNumber())) { + orgController.setCurrentOrganization(orgs.get(currentOrg.orgNumber())); + } + }); + }); + + return bodyRoot; + } + + private HBox createSearchSection() { + HBox searchSection = new HBox(); + TextField searchField = new TextField(); + searchField.setPromptText("Search organizations..."); + + // Add listener for search text changes + searchField.textProperty().addListener((obs, oldVal, newVal) -> { + updateOrganizationGrid(newVal.trim()); + }); + + searchSection.getChildren().add(searchField); + return searchSection; + } + + private GridPane createOrganizationSection(String searchTerm) { + GridPane grid = new GridPane(); + grid.setId("card-grid"); + grid.setHgap(20); + grid.setVgap(20); + grid.setStyle("-fx-padding: 0;"); + grid.setMaxWidth(Double.MAX_VALUE); + + // Store reference for later updates + if (organizationGrid == null) { + organizationGrid = grid; + } + + if (allOrganizations == null) { + allOrganizations = orgController.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) + 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 && !searchTerm.isEmpty()) { + // Filter organizations by search term + organizations = filterOrganizations(searchTerm); + } else { + organizations = allOrganizations; + } + + int column = 0; + int row = 0; + + for (Organization org : organizations.values()) { + //Adds default text if organization does not have any + String img = (org.logoUrl() != null && !org.logoUrl().isBlank()) + ? org.logoUrl() + : null; + + OrganizationCard card = new OrganizationCard(nav, orgController, org, img); + + grid.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); + grid.getColumnConstraints().add(col); + } + + // Store reference for later updates + if (organizationGrid == null) { + organizationGrid = grid; + } + + return grid; + } + + private Map filterOrganizations(String searchTerm) { + // If no search term, return all organizations + if (searchTerm == null || searchTerm.isEmpty()) { + return allOrganizations; + } + + 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, + (e1, e2) -> e1, + LinkedHashMap::new + )); + } + + + private void updateOrganizationGrid(String searchTerm) { + 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(); + + 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 new file mode 100644 index 0000000..483d0e7 --- /dev/null +++ b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java @@ -0,0 +1,143 @@ +package edu.group5.app.view.causespage; + +import edu.group5.app.control.NavigationController; +import edu.group5.app.control.OrganizationController; +import edu.group5.app.model.organization.Organization; +import javafx.geometry.Pos; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; +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 Organization organization; + private final NavigationController nav; + private final OrganizationController organizationController; + private StackPane imageContainer; + private String currentLogoUrl; + + 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()); + + imageContainer = createImageContainer(img); + getChildren().addAll( + imageContainer, + orgName(org.name()), + checkMarkContainer() + ); + + setOnMouseClicked(e -> { + organizationController.setCurrentOrganization(getOrganizationWithCurrentLogo()); + nav.showOrganizationPage(); + }); + + setSpacing(10); + setFillWidth(true); + setAlignment(Pos.CENTER); + } + + 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, 80, 80, true, true, 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"); + + imageContainer.setPrefHeight(80); + imageContainer.setPrefWidth(80); + imageContainer.setMaxWidth(Double.MAX_VALUE); + + + if (img != null && !img.isBlank()) { + ImageView logo = new ImageView(new Image(img, 80, 80, true, true, 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); + } + + return imageContainer; + } + + private Text orgName(String text) { + Text orgName = new Text(text); + orgName.setId("orgName"); + orgName.setWrappingWidth(150); + return orgName; + } + + private StackPane checkMarkContainer() { + StackPane checkMarkContainer = new StackPane(); + checkMarkContainer.setId("checkMarkContainer"); + checkMarkContainer.setAlignment(Pos.CENTER_LEFT); + + ImageView verifiedCheck = new ImageView( + new Image(getClass().getResource("/verified_check.png").toExternalForm()) + ); + + verifiedCheck.setPreserveRatio(true); + verifiedCheck.setSmooth(true); + + checkMarkContainer.getChildren().add(verifiedCheck); + return checkMarkContainer; + } +} 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 6c5e5e6..ed0288c 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -1,13 +1,12 @@ package edu.group5.app.view.donationpage; -import edu.group5.app.control.HeaderController; -import edu.group5.app.control.MainController; -import edu.group5.app.control.donationpage.DonationPageController; -import edu.group5.app.view.Header; +import edu.group5.app.control.DonationController; +import edu.group5.app.control.NavigationController; +import edu.group5.app.utils.ParameterValidator; 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; @@ -17,30 +16,56 @@ 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.Objects; +/** + * 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 DonationPageController controller; - private final MainController mainController; - private final List allDonationElements = new ArrayList<>(); - private final Map elementAmounts = new HashMap<>(); + private final NavigationController nav; + private final DonationController donationController; + + private Node currentlySelected = null; + private TextField customAmountField; + private Node selectedPaymentMethod = null; + private Button donateBtn; - public DonationPageView(DonationPageController donationPageController, HeaderController headerController, MainController mainController) { - this.controller = donationPageController; - this.mainController = mainController; - getStylesheets().add(getClass().getResource("/donationpage/donation.css").toExternalForm()); + public DonationPageView(NavigationController nav, DonationController donationController) { + ParameterValidator.objectChecker(nav, "NavigationController"); + ParameterValidator.objectChecker(donationController, "DonationController"); - Header headerView = new Header(headerController); - setTop(headerView); + this.nav = nav; + this.donationController = donationController; + + 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(); + } + }); 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); @@ -75,14 +100,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 +116,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); @@ -102,51 +127,119 @@ private VBox createCustomButton() { box.setAlignment(Pos.CENTER); 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(); }); - allDonationElements.add(box); + amountField.setOnMouseClicked(e -> + { + selectDonation(box); + amountField.requestFocus(); + }); + + // 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 = new Button("Donate"); donateBtn.getStyleClass().add("donate-button"); - donateBtn.setOnAction(e -> controller.handleDonationBtn()); + donateBtn.setDisable(true); + 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"); + public HBox createPaymentMethodSection() { + Button appleBtn = new Button("Apple Pay"); + 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)); } - element.getStyleClass().add("donation-button-selected"); + 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"); + } + currentlySelected = element; + currentlySelected.getStyleClass().add("donation-button-selected"); - // Extract and store the amount - extractAndStoreAmount(element); + BigDecimal amount = (BigDecimal) element.getUserData(); + updateDonationAmount(amount); + updateDonationButtonState(); } - private void extractAndStoreAmount(Node element) { - BigDecimal amount = elementAmounts.get(element); - if (amount != null) { - mainController.setCurrentDonationAmount(amount); - } else { - System.err.println("Error: No amount found for selected element"); + private void selectPaymentMethod(Node element) { + if (selectedPaymentMethod != null) { + selectedPaymentMethod.getStyleClass().remove("payment-method-selected"); } + selectedPaymentMethod = element; + selectedPaymentMethod.getStyleClass().add("payment-method-selected"); + + String paymentMethod = (String) element.getUserData(); + donationController.setPaymentMethod(paymentMethod); + updateDonationButtonState(); + } + + private void clearSelection() { + if (currentlySelected != null) { + currentlySelected.getStyleClass().remove("donation-button-selected"); + currentlySelected = null; + updateDonationAmount(null); + } + + if (selectedPaymentMethod != null) { + selectedPaymentMethod.getStyleClass().remove("payment-method-selected"); + selectedPaymentMethod = null; + } + + if (customAmountField != null) { + customAmountField.clear(); + } + + updateDonationButtonState(); + } + + private void updateDonationAmount(BigDecimal amount) { + donationController.setDonationAmount(amount); } private BigDecimal parseAmount(String amountStr) { @@ -157,4 +250,8 @@ private BigDecimal parseAmount(String amountStr) { } } + private void updateDonationButtonState() { + donateBtn.setDisable(currentlySelected == null || selectedPaymentMethod == null); + } + } \ No newline at end of file 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..e20cfee 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.NavigationController; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -9,14 +9,20 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; -import java.awt.*; 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 PaymentCompleteController controller; + private final NavigationController nav; - public PaymentCompletePageView(PaymentCompleteController paymentCompleteController) { - this.controller = paymentCompleteController; + public PaymentCompletePageView(NavigationController nav) { + this.nav = nav; getStylesheets().add(getClass().getResource("/donationpage/paymentcomplete.css").toExternalForm()); VBox content = new VBox(20); @@ -40,7 +46,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 5510daa..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,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.NavigationController; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.ScrollPane; @@ -11,14 +9,21 @@ 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 HomePageController controller; + private final NavigationController nav; + + public HomePageView(NavigationController nav) { + this.nav = nav; - public HomePageView(HomePageController homePageController, HeaderController headerController) { - this.controller = homePageController; getStylesheets().add(getClass().getResource("/homepage/homepage.css").toExternalForm()); - Header headerView = new Header(headerController); - setTop(headerView); setCenter(createBody()); } @@ -46,10 +51,12 @@ private VBox createIntroductionSection() { h2.setId("h2"); Button donateToACauseBtn = new Button("Donate to a cause"); - donateToACauseBtn.setOnAction(e -> controller.handleDonateToACauseBtn()); + donateToACauseBtn.setId("donate-to-cause-btn"); + donateToACauseBtn.setOnAction(e -> nav.showCausesPage()); Button aboutUsBtn = new Button("About us"); - aboutUsBtn.setOnAction(e -> controller.handleAboutUsBtn()); + aboutUsBtn.setId("about-us-btn"); + 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/LoginHeader.java b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java index decd5a1..b5f4296 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java @@ -1,12 +1,15 @@ 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; 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() { @@ -19,7 +22,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()) 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 96d83d7..927a8af 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,7 @@ package edu.group5.app.view.loginpage; - -import edu.group5.app.control.HeaderController; -import edu.group5.app.control.LoginPageController; -import javafx.geometry.Insets; +import edu.group5.app.control.NavigationController; +import edu.group5.app.control.AuthController; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -13,17 +11,25 @@ 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 LoginPageController controller; + private final NavigationController nav; + private final AuthController authController; + private TextField emailField; private PasswordField passwordField; private Label errorLabel; - public LoginPageView(LoginPageController loginPageController) { - this.controller = loginPageController; - this.controller.setView(this); - LoginHeader loginHeaderView = new LoginHeader(); - setTop(loginHeaderView); + public LoginPageView(NavigationController nav, AuthController authController) { + this.nav = nav; + this.authController = authController; HBox content = new HBox(); content.setFillHeight(true); @@ -58,6 +64,7 @@ private VBox getOuterSection() { outerSection.getChildren().addAll(getLoginBox(), getRegisterBtn()); return outerSection; } + private VBox getLoginBox() { VBox loginSection = new VBox(12); loginSection.setAlignment(Pos.CENTER); @@ -81,6 +88,7 @@ private VBox getEmailBox() { emailBox.getChildren().addAll(new Label("Email"), emailField); return emailBox; } + private VBox getPasswordBox() { VBox passwordBox = new VBox(); passwordBox.setMaxWidth(300); @@ -89,20 +97,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 -> authController.handleLogin( + this, + getEmail(), + getPassword() + )); return loginBtn; } + 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 -> controller.handleRegisterBtn()); + registerBtn.setOnMouseClicked(e -> nav.showSignUpPage()); 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/SignUpPageView.java similarity index 65% 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 6ee0e9b..70f40ea 100644 --- a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java @@ -1,7 +1,7 @@ package edu.group5.app.view.loginpage; -import edu.group5.app.control.HeaderController; -import edu.group5.app.control.SignInPageController; +import edu.group5.app.control.NavigationController; +import edu.group5.app.control.AuthController; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -11,19 +11,28 @@ import java.util.Objects; +/** + * 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 NavigationController nav; + private final AuthController authController; -public class SignInPageView extends BorderPane { - private final SignInPageController controller; private TextField nameField; private TextField surnameField; private TextField emailField; private PasswordField passwordField; private Label errorLabel; - public SignInPageView(SignInPageController signInPageController) { - this.controller = signInPageController; - this.controller.setView(this); - setTop(new LoginHeader()); + public SignUpPageView(NavigationController nav, AuthController authController) { + this.nav = nav; + this.authController = authController; HBox content = new HBox(); content.setFillHeight(true); @@ -32,14 +41,13 @@ public SignInPageView(SignInPageController signInPageController) { 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); } - public String getFirstName() { return nameField.getText(); } @@ -58,6 +66,7 @@ public char[] getPassword() { public void showError(String message) { errorLabel.setText(message); + errorLabel.setWrapText(true); errorLabel.setStyle("-fx-text-fill: red;"); } @@ -65,20 +74,20 @@ 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() { errorLabel = new Label(); - errorLabel.setPrefHeight(20); return errorLabel; } @@ -102,6 +111,7 @@ private HBox getNameRow() { return nameRow; } + private VBox getEmailBox() { VBox emailBox = new VBox(); emailBox.setMaxWidth(300); @@ -111,6 +121,7 @@ private VBox getEmailBox() { emailBox.getChildren().addAll(new Label("Email"), emailField); return emailBox; } + private VBox getPasswordBox() { VBox passwordBox = new VBox(); passwordBox.setMaxWidth(300); @@ -119,20 +130,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()); - return signInBtn; + + private Button getSignUpBtn() { + Button signUpBtn = new Button("Sign Up"); + signUpBtn.setMaxWidth(300); + signUpBtn.setId("login-btn"); + signUpBtn.setOnMouseClicked(e -> authController.handleSignUp( + this, + getFirstName(), + getLastName(), + getEmail(), + getPassword() + )); + return signUpBtn; } + 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 4ec7c91..7ea3b6b 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -1,10 +1,11 @@ package edu.group5.app.view.organizationpage; -import edu.group5.app.control.HeaderController; -import edu.group5.app.control.MainController; -import edu.group5.app.control.OrganizationPageController; +import edu.group5.app.control.DonationController; +import edu.group5.app.control.NavigationController; +import edu.group5.app.control.OrganizationController; import edu.group5.app.model.organization.Organization; -import edu.group5.app.view.Header; +import javafx.application.Platform; +import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -17,33 +18,60 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; +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 OrganizationPageController controller; - private final MainController mainController; - - public OrganizationPageView(OrganizationPageController controller, HeaderController headerController, MainController mainController) { - this.controller = controller; - this.mainController = mainController; - getStylesheets().add(getClass().getResource("/organizationpage/organizationpage.css").toExternalForm()); - Header headerView = new Header(headerController); - setTop(headerView); - setCenter(createBody()); + private final NavigationController nav; + private final OrganizationController organizationController; + private final DonationController donationController; + + 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()); + + 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(); @@ -60,42 +88,89 @@ private StackPane createImageContainer() { imageContainer.setId("imageContainer"); imageContainer.setPrefHeight(120); imageContainer.setPrefWidth(120); - 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"; + imageContainer.setMaxWidth(120); + + Organization org = organizationController.getCurrentOrganization(); + + if (org != null && org.logoUrl() != null && !org.logoUrl().isBlank()) { + // Load image in background thread to avoid blocking UI + new Thread(() -> { + try { + Image image = new Image(org.logoUrl(), 350, 350, 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 { + Text text = new Text("No image"); + text.setStyle("-fx-font-size: 10;"); + imageContainer.getChildren().add(text); + } - ImageView logo = new ImageView( - new Image(getClass().getResource(imagePath).toExternalForm()) - ); - - logo.setId("logo"); - logo.setSmooth(true); - logo.setPreserveRatio(true); - - imageContainer.getChildren().add(logo); return imageContainer; } private VBox createOrgInfoSection() { - Organization org = mainController.getCurrentOrganization(); + Organization org = organizationController.getCurrentOrganization(); VBox orgInfoSection = new VBox(); 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"); - - orgNameAndDescription.getChildren().addAll(orgName, description); + VBox descriptionBox = new VBox(); + descriptionBox.setSpacing(15); + descriptionBox.setId("description-container"); + descriptionBox.setMaxWidth(750); + + if (org != null && org.description() != null) { + 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); + } + } + } + + 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); + descriptionScroll.setPrefWidth(750); + descriptionScroll.setMinWidth(750); + + orgNameAndDescription.getChildren().addAll(orgName, descriptionScroll); 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 9ab73ee..aff5b32 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -1,40 +1,55 @@ package edu.group5.app.view.userpage; -import edu.group5.app.control.HeaderController; -import edu.group5.app.control.MainController; +import edu.group5.app.control.DonationController; +import edu.group5.app.control.NavigationController; +import edu.group5.app.control.OrganizationController; +import edu.group5.app.control.AuthController; import edu.group5.app.model.donation.Donation; import edu.group5.app.model.organization.Organization; import edu.group5.app.model.user.User; -import edu.group5.app.view.Header; import javafx.geometry.Insets; import javafx.geometry.Pos; 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; +import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.text.Text; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; - - +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 User currentUser; - private final MainController mainController; - - public UserPageView(HeaderController headerController, MainController mainController) { - this.mainController = mainController; - this.currentUser = mainController.getCurrentUser(); + private final NavigationController nav; + private final AuthController authController; + private final DonationController donationController; + private final OrganizationController organizationController; + + public UserPageView(NavigationController nav, AuthController authController, + DonationController donationController, OrganizationController organizationController) { + this.nav = nav; + this.authController = authController; + this.donationController = donationController; + this.organizationController = organizationController; 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()); @@ -42,12 +57,16 @@ public UserPageView(HeaderController headerController, MainController mainContro } 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 = authController.getCurrentUser(); + Text name = new Text(currentUser.getFirstName() + " " + currentUser.getLastName()); name.setId("profile-name"); @@ -59,7 +78,7 @@ private HBox createProfileSection() { Button logoutBtn = new Button("Logout"); logoutBtn.getStyleClass().add("logout-button"); - logoutBtn.setOnAction(e -> mainController.logout()); + logoutBtn.setOnAction(e -> authController.handleLogout()); VBox info = new VBox(10, name, email, location, logoutBtn); info.setAlignment(Pos.CENTER_LEFT); @@ -68,50 +87,95 @@ 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"); - VBox causesBox = new VBox(10); - causesBox.getStyleClass().add("section-box"); - causesBox.setPadding(new Insets(10)); + 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"); - HashMap userDonations = mainController.getDonationService() - .getDonationRepository().filterByUser(currentUser.getUserId()); - - Set uniqueOrgs = new HashSet<>(); - for (Donation donation : userDonations.values()) { - uniqueOrgs.add(donation.organizationId()); - } + 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); + causesFlow.getChildren().add(noCauses); } else { - for (int orgId : uniqueOrgs) { - Organization org = mainController.getOrganizationService().findByOrgNumber(orgId); + for (int orgId : uniqueOrganizations) { + Organization org = organizationController.getOrganizationById(orgId); if (org != null) { - Label causeLabel = new Label("β€’ " + org.name()); - causesBox.getChildren().add(causeLabel); + causesFlow.getChildren().add(createCauseChip(org)); } } } + scrollPane.setPrefHeight(275); + scrollPane.setContent(causesFlow); - return new VBox(10, title, causesBox); + 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"); + 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(false); + scrollPane.setPrefWidth(650); + scrollPane.setMaxWidth(650); + scrollPane.setPrefHeight(400); + 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)); - HashMap userDonations = mainController.getDonationService() - .getDonationRepository().filterByUser(currentUser.getUserId()); + User currentUser = authController.getCurrentUser(); + 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.getOrganizationById(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"); @@ -119,18 +183,55 @@ private VBox createDonationsSection() { donationsBox.getChildren().add(noDonations); } else { for (Donation donation : userDonations.values()) { - Organization org = mainController.getOrganizationService() - .findByOrgNumber(donation.organizationId()); + Organization org = organizationController.getOrganizationById(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)); } } - return new VBox(10, title, donationsBox); + scrollPane.setContent(donationsBox); + return new VBox(10, title, searchBox, scrollPane); } + private BorderPane createDonationCard(Donation donation) { + Organization org = organizationController.getOrganizationById(donation.organizationId()); + String orgName = (org != null) ? org.name() : "Unknown Organization"; + + // 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)); + + // 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"); + 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); + + return card; + } + + private FlowPane createCauseChip(Organization org) { + FlowPane chip = new FlowPane(); + chip.getStyleClass().add("cause-chip"); + chip.setPadding(new Insets(8, 12, 8, 12)); + + Label label = new Label(org.name()); + chip.getChildren().add(label); + + return chip; + } } diff --git a/src/main/resources/donationpage/donation.css b/src/main/resources/donationpage/donation.css index 32433df..bc777b6 100644 --- a/src/main/resources/donationpage/donation.css +++ b/src/main/resources/donationpage/donation.css @@ -1,53 +1,149 @@ .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-background-color: #111; + -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-border-color: #111; + -fx-prompt-text-fill: #ccc; } + .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-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; +} + +.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; +} +.payment-method-button { + -fx-background-color: #111; -fx-text-fill: white; - -fx-font-size: 22px; + -fx-font-size: 16px; -fx-font-weight: bold; - -fx-background-radius: 8; + -fx-pref-width: 180px; + -fx-pref-height: 45px; + -fx-background-radius: 6; + -fx-border-radius: 6; -fx-cursor: hand; - -fx-padding: 0 40 0 40; } -.donate-button:hover { - -fx-background-color: #c02020; +.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; +} +.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/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 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 deleted file mode 100644 index 4ab0276..0000000 --- a/src/main/resources/loginpage/signin.css +++ /dev/null @@ -1,25 +0,0 @@ -#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%; -} -#login-btn { - -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; -} -#login-box { - -fx-border-color: #ccc; - -fx-border-radius: 8px; - -fx-border-width: 1px; - -fx-padding: 24px; - -fx-max-width: 340px; -} \ No newline at end of file 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/signup.css b/src/main/resources/loginpage/signup.css new file mode 100644 index 0000000..fcd8751 --- /dev/null +++ b/src/main/resources/loginpage/signup.css @@ -0,0 +1,35 @@ +#image-section { + -fx-background-image: url("/loginpage/signup-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; +} + +#register-btn { + -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; +} + +#login-btn:hover { + -fx-cursor: hand; +} + +#register-btn:hover { + -fx-cursor: hand; +} \ No newline at end of file diff --git a/src/main/resources/organizationpage/organizationpage.css b/src/main/resources/organizationpage/organizationpage.css index a7276b5..20a9ad8 100644 --- a/src/main/resources/organizationpage/organizationpage.css +++ b/src/main/resources/organizationpage/organizationpage.css @@ -1,31 +1,81 @@ #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-container { + -fx-padding: 30; + -fx-spacing: 22; + -fx-max-width: 750; + -fx-background-color: #f8f9fa; + -fx-border-radius: 8; } -#description { - -fx-font-size: 10pt; +#description-paragraph { + -fx-font-size: 18; + -fx-text-fill: #333; + -fx-font-family: "Segoe UI", Arial, sans-serif; + -fx-line-spacing: 13; + -fx-text-alignment: Left; + -fx-wrap-text: true; + -fx-padding: 10 0 10 0; } #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; +} + +#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; +} +.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/userpage/userpage.css b/src/main/resources/userpage/userpage.css index 8401a77..8d68841 100644 --- a/src/main/resources/userpage/userpage.css +++ b/src/main/resources/userpage/userpage.css @@ -1,31 +1,86 @@ #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: 150px; + -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 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/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 80d37bf..3b1e5ff 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; @@ -11,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.*; @@ -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<>()); @@ -47,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 @@ -55,17 +58,34 @@ 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 + void getUserDonationsReturnsEmptyMapIfNoDonations() { + assertTrue(donationService.getUserDonations(customer.getUserId()).isEmpty()); } @Test - void testGetDonationRepository() { - assertEquals(donationRepository, donationService.getDonationRepository()); + 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 testGetOrganizationRepository() { - assertEquals(organizationRepository, donationService.getOrganizationRepository()); + void getOrganizationDonationsReturnsEmptyMapIfNoDonations() { + assertTrue(donationService.getOrganizationDonations(1).isEmpty()); } @Test 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 7a5ece5..e6b0cf9 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java @@ -1,137 +1,181 @@ -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; - - @BeforeEach - void setUp() { - 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); - } - - private void constructorTest(Object[] input, String expectedMessage) { - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> new OrganizationRepository(input) - ); - assertEquals(expectedMessage, exception.getMessage()); - } - @Test - void constructor_ThrowsWhenContentIsNull() { - constructorTest(null, "The input cannot be null"); - } - - @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"), - 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"), - 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"), - 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]); - 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_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[] { + 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/OrganizationScraperTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java new file mode 100644 index 0000000..5d48428 --- /dev/null +++ b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java @@ -0,0 +1,146 @@ +package edu.group5.app.model.organization; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + + +import static org.junit.jupiter.api.Assertions.*; + +class OrganizationScraperTest { + + private OrganizationScraper scraper; + + @BeforeEach + void setUp() { + scraper = new OrganizationScraper(); + } + + @Test + void fetchDescriptionReturnsNullWhenUrlIsNull() { + assertNull(scraper.fetchDescription(null)); + } + + @Test + void fetchDescriptionReturnsNullWhenUrlIsBlank() { + assertNull(scraper.fetchDescription("")); + } + + @Test + void fetchDescriptionReturnsNullWhenUrlIsInvalid() { + String result = scraper.fetchDescription("https://invalid-url-that-does-not-exist-xyz123.com"); + assertNull(result); + } + + @Test + void fetchDescriptionCachesResultOnSecondCall() { + // 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 fetchLogoUrlReturnsNullWhenUrlIsNull() { + assertNull(scraper.fetchLogoUrl(null)); + } + + @Test + void fetchLogoUrlReturnsNullWhenUrlIsBlank() { + assertNull(scraper.fetchLogoUrl("")); + } + + @Test + void fetchLogoUrlReturnsNullWhenUrlIsInvalid() { + String result = scraper.fetchLogoUrl("https://invalid-url-that-does-not-exist-xyz123.com"); + assertNull(result); + } + + @Test + void fetchLogoUrlCachesResultOnSecondCall() { + // 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); + } + + @Test + void fetchDescriptionReturnsCachedValue() { + 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 + } + + @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/organization/OrganizationServiceTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java index e34aba7..3765bf5 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,20 +25,45 @@ 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)); - assertEquals("OrganizationRepository cannot be null", ex.getMessage()); + () -> new OrganizationService(null, scraper)); + assertEquals("OrganizationRepository can't be null", ex.getMessage()); } @Test - void testGetOrganizationRepository() { - assertEquals(repo, service.getOrganizationRepository()); + void constructor_throwsIfScraperIsNull() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new OrganizationService(repo, null)); + assertEquals("Scraper can't be null", ex.getMessage()); + } + + @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 @@ -68,3 +96,4 @@ void testFindByOrgName() { assertEquals("Misjonsalliansen", org.name()); } } + 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 )); } 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 acec10b..459b635 100644 --- a/src/test/java/edu/group5/app/model/user/UserServiceTest.java +++ b/src/test/java/edu/group5/app/model/user/UserServiceTest.java @@ -31,12 +31,7 @@ void setUp() { void constructorthrowsIfNull() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new UserService(null)); - assertEquals("UserRepository cannot be null", ex.getMessage()); - } - - @Test - void testGetUserRepository() { - assertEquals(repo, service.getUserRepository()); + assertEquals("UserRepository can't be null", ex.getMessage()); } @Test @@ -76,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 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 79% 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..289b803 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; @@ -7,15 +7,18 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +/** + * A test class for interactions with the donations table using the DbWrapper. + */ public class DbWrapperDonationsTest { private Object[] johnDonation; private List users; @@ -23,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; @@ -38,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"; @@ -52,13 +57,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); @@ -80,12 +87,23 @@ 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); 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) { @@ -102,7 +120,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()); }); } @@ -141,7 +164,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); }); @@ -154,10 +177,13 @@ 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)); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - this.db.exportDonations(this.donations); - }); + assertDoesNotThrow(() -> assertEquals(2, this.db.exportDonations(this.donations2))); + 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()); @@ -200,8 +226,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() + ); + } } 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 84% 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..e8621d8 100644 --- a/src/test/java/edu/group5/app/control/wrapper/DbWrapperUserTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java @@ -1,39 +1,34 @@ -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; -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 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; 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; @@ -53,8 +48,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); @@ -67,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); @@ -129,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); }); @@ -142,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); }); @@ -187,5 +189,17 @@ public void addingUserListWithNullInRowThrowsExpectedException() { 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() + ); + } } 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 98% 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..0165456 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; 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()); + } +} 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 { - -}