diff --git a/pom.xml b/pom.xml
index 75290fc..5bbba22 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,7 +55,7 @@
org.springframeworkspring-core
- 6.1.10
+ 6.2.0org.slf4j
diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java
index be397d8..b9d67da 100644
--- a/src/main/java/edu/group5/app/App.java
+++ b/src/main/java/edu/group5/app/App.java
@@ -5,6 +5,7 @@
import edu.group5.app.model.donation.DonationRepository;
import edu.group5.app.model.donation.DonationService;
import edu.group5.app.model.organization.OrganizationRepository;
+import edu.group5.app.model.organization.OrganizationScraper;
import edu.group5.app.model.organization.OrganizationService;
import edu.group5.app.model.user.UserRepository;
import edu.group5.app.model.user.UserService;
@@ -62,16 +63,16 @@ public void init() {
System.err.println("Failed to load organization data: " + e.getMessage());
}
- // Create repositories with fetched data
+ // Create scraper and repositories with fetched data
+ OrganizationScraper orgScraper = new OrganizationScraper();
this.userRepository = new UserRepository(userData);
this.donationRepository = new DonationRepository(donationData);
- OrganizationRepository organizationRepository = new OrganizationRepository(organizationData);
+ OrganizationRepository organizationRepository = new OrganizationRepository(organizationData, orgScraper);
// Create services (backend wiring)
UserService userService = new UserService(this.userRepository);
DonationService donationService = new DonationService(this.donationRepository, organizationRepository);
- OrganizationService organizationService = new OrganizationService(organizationRepository);
-
+ OrganizationService organizationService = new OrganizationService(organizationRepository, orgScraper);
this.root = new BorderPane();
this.appState = new AppState();
this.nav = new NavigationController(root, appState, userService, donationService, organizationService);
diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java
index 9118c7e..a579a9b 100644
--- a/src/main/java/edu/group5/app/control/DonationController.java
+++ b/src/main/java/edu/group5/app/control/DonationController.java
@@ -12,6 +12,9 @@
import java.util.Map;
import java.util.Set;
+import javafx.scene.control.Alert;
+import javafx.scene.control.ButtonType;
+
public class DonationController {
private final AppState appState;
private final NavigationController nav;
@@ -38,27 +41,58 @@ public Set getUniqueOrgs() {
return uniqueOrgs;
}
- public void handleDonate() {
- // Get session data from MainController
+ public void requestDonationConfirmation() {
+ // Get session data
User currentUser = appState.getCurrentUser();
Organization currentOrg = appState.getCurrentOrganization();
BigDecimal amount = appState.getCurrentDonationAmount();
String paymentMethod = appState.getCurrentPaymentMethod();
+ // Validate before showing dialog
if (currentUser == null) {
- System.err.println("Error: No user logged in");
+ showError("Error: No user logged in");
return;
}
- if (!(currentUser instanceof Customer customer)) {
- System.err.println("Error: Only customers can donate");
+ if (!(currentUser instanceof Customer)) {
+ showError("Error: Only customers can donate");
return;
}
if (currentOrg == null) {
- System.err.println("Error: No organization selected");
+ showError("Error: No organization selected");
return;
}
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
- System.err.println("Error: Invalid donation amount");
+ showError("Please select a donation amount first");
+ return;
+ }
+
+ // Show confirmation dialog
+ Alert confirmDialog = new Alert(Alert.AlertType.CONFIRMATION);
+ confirmDialog.setTitle("Confirm Donation");
+ confirmDialog.setHeaderText("Confirm Your Donation");
+ confirmDialog.setContentText(
+ "Organization: " + currentOrg.name() + "\n" +
+ "Amount: " + amount + " kr\n" +
+ "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) {
@@ -86,4 +120,12 @@ public void handleDonate() {
// Navigate to payment complete
nav.showPaymentCompletePage();
}
+
+ private void showError(String message) {
+ Alert errorAlert = new Alert(Alert.AlertType.WARNING);
+ errorAlert.setTitle("Donation Error");
+ errorAlert.setHeaderText("Cannot Process Donation");
+ errorAlert.setContentText(message);
+ errorAlert.showAndWait();
+ }
}
diff --git a/src/main/java/edu/group5/app/control/LoginController.java b/src/main/java/edu/group5/app/control/LoginController.java
index 649a1c4..ae9d22e 100644
--- a/src/main/java/edu/group5/app/control/LoginController.java
+++ b/src/main/java/edu/group5/app/control/LoginController.java
@@ -5,6 +5,12 @@
import edu.group5.app.model.user.UserService;
import edu.group5.app.view.loginpage.LoginPageView;
import edu.group5.app.view.loginpage.SignUpPageView;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonType;
+
+import java.util.Arrays;
+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class LoginController {
@@ -27,21 +33,38 @@ public void handleSignUp(SignUpPageView view, String firstName, String lastName,
return;
}
- String password = new String(passwordChars);
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
- String hashedPassword = encoder.encode(password);
- boolean success = userService.registerUser(
- "Customer", firstName, lastName, email, hashedPassword);
+ // Clears password char array after creating a hash.
+ String hashedPassword = encoder.encode(new String(passwordChars));
+ for (int i = 0; i < passwordChars.length; i++) {
+ passwordChars[0] = '0';
+ }
- if (success) {
- User user = userService.getUserByEmail(email);
+ Alert privacyPolicy = new Alert(Alert.AlertType.CONFIRMATION);
+ privacyPolicy.setTitle("Accept Privacy Policy");
+ privacyPolicy.setHeaderText("Accept Privacy Policy");
+ privacyPolicy.setContentText(
+ "Your user information like:\n" +
+ "Name and email—as well as donations tied to your account—will be saved locally on your machine.\n" +
+ "By creating an account, you accept the right of our app to store this information.");
- appState.setCurrentUser(user);
- nav.showHomePage();
+ 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.");
+ }
} else {
- view.showError("Registration failed. Email may already be in use.");
+ view.showError("Registration failed. Must Accept Privacy Policy to create account.");
}
+
}
public void handleLogin(LoginPageView view, String email, char[] passwordChars) {
diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java
index cc0a6b1..61155b7 100644
--- a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java
+++ b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java
@@ -11,25 +11,33 @@
* Repository class for managing Organization entities. It provides methods to retrieve trusted organizations,
* find organizations by their organization number or name, and initializes the repository with input data.
* The repository uses a HashMap to store Organization objects for efficient retrieval based on their organization number.
- * Handles the business logic associated with organizations
+ * Delegates web scraping to OrganizationScraper for separation of concerns.
*/
public class OrganizationRepository extends Repository {
private final HashMap grandMap;
+ private final OrganizationScraper scraper;
/**
- * Initializes the repository with the given input data, c
- * onverting it into Organization objects and storing them in a map for efficient retrieval.
- * The input is expected to be an array of objects, where each object contains
+ * Initializes the repository with the given input data and scraper.
+ * Converts input into Organization objects and stores them in a map for efficient retrieval.
+ * The input is expected to be an array of objects, where each object contains
* the necessary information to create an Organization.
+ *
* @param input the input data used to populate the repository, must not be null
- * @throws IllegalArgumentException if the input is null
+ * @param scraper the OrganizationScraper to use for fetching web data, must not be null
+ * @throws IllegalArgumentException if input or scraper is null
*/
- public OrganizationRepository(Object[] input) {
- super(new HashMap<>());
- grandMap = new HashMap<>();
+ public OrganizationRepository(Object[] input, OrganizationScraper scraper) {
+ super(new HashMap<>());
+ this.grandMap = new HashMap<>();
if (input == null) {
throw new IllegalArgumentException("The input cannot be null");
}
+ if (scraper == null) {
+ throw new IllegalArgumentException("The scraper cannot be null");
+ }
+ this.scraper = scraper;
+
ObjectMapper mapper = new ObjectMapper();
for (Object obj : input) {
@@ -42,7 +50,8 @@ public OrganizationRepository(Object[] input) {
boolean trusted = "approved".equalsIgnoreCase((String) contentMap.get("status"));
String websiteURL = (String) contentMap.get("url");
boolean isPreApproved = Boolean.TRUE.equals(contentMap.get("is_pre_approved"));
- String description = "Information about " + name;
+ String description = scraper.fetchDescription(websiteURL);
+ description = description != null ? description : "Information about " + name;
Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description, null);
grandMap.put(org.orgNumber(), org);
diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java
new file mode 100644
index 0000000..6a8c230
--- /dev/null
+++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java
@@ -0,0 +1,115 @@
+package edu.group5.app.model.organization;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.TextNode;
+import org.jsoup.select.Elements;
+
+import java.util.stream.Collectors;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Handles web scraping of organization information from Innsamlingskontrollen.
+ * Responsible for fetching logos and descriptions from organization pages.
+ * All results are cached to avoid redundant network requests.
+ */
+public class OrganizationScraper {
+ private final Map logoCache = new HashMap<>();
+ private final Map descriptionCache = new HashMap<>();
+
+ /**
+ * Fetches the description for the given URL by scraping all text content
+ * inside {@code }. Results are cached.
+ *
+ *
Strategy:
+ *
+ *
Tries to get all <p> tags (skipping the first one) and concatenates them
+ *
If no paragraphs found, gets all text content from the section
+ *
Returns null if section not found or is empty
+ *
+ *
+ * @param pageUrl the URL for the organization's page; may be null or blank
+ * @return the description text, or null if not found or pageUrl is invalid
+ */
+ public String fetchDescription(String pageUrl) {
+ if (pageUrl == null || pageUrl.isBlank()) {
+ return null;
+ }
+
+ if (descriptionCache.containsKey(pageUrl)) {
+ return descriptionCache.get(pageUrl);
+ }
+
+ try {
+ Document doc = Jsoup.connect(pageUrl)
+ .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
+ .timeout(5000).get();
+
+ Element section = doc.selectFirst("section.information");
+ if (section != null) {
+ section.select("div.extra-info").remove();
+ section.select("a.read-more").remove();
+
+ // Extract all
tags and
elements as separate paragraphs
+ String description = section.select("p, div").stream()
+ .filter(el -> el.tagName().equals("p") || el.select("p").isEmpty())
+ .filter(el -> !el.hasClass("extra-info") && !el.hasClass("logo"))
+ .map(Element::text)
+ .map(text -> text.replace("Les mer", "").trim())
+ .filter(text -> !text.isBlank())
+ .collect(Collectors.joining("\n\n"));
+
+ // Fallback: if no paragraphs found, get all text from section
+ if (description.isBlank()) {
+ description = section.text().trim();
+ }
+ description = description.replace("Les mer", "").trim();
+
+ // Only cache and return if we found something meaningful
+ if (!description.isBlank()) {
+ descriptionCache.put(pageUrl, description);
+ return description;
+ }
+ }
+ } catch (Exception e) {
+ System.out.println("Could not get description for: " + pageUrl);
+ }
+ return null;
+ }
+
+ /**
+ * Fetches the logo URL for the given page by scraping the {@code div.logo img}
+ * element. Results are cached so each URL is only fetched once.
+ *
+ * @param pageUrl the URL for the organization's page; may be null or blank
+ * @return the absolute logo URL, or null if not found or pageUrl is invalid
+ */
+ public String fetchLogoUrl(String pageUrl) {
+ if (pageUrl == null || pageUrl.isBlank()) {
+ return null;
+ }
+
+ if (logoCache.containsKey(pageUrl)) {
+ return logoCache.get(pageUrl);
+ }
+
+ try {
+ Document doc = Jsoup.connect(pageUrl)
+ .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
+ .timeout(5000).get();
+ Element img = doc.selectFirst("div.logo img");
+
+ if (img != null) {
+ String logoUrl = img.absUrl("src");
+ logoCache.put(pageUrl, logoUrl);
+ return logoUrl;
+ }
+ } catch (Exception e) {
+ System.out.println("Could not get logo for: " + pageUrl);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java
index 9785040..e19c273 100644
--- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java
+++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java
@@ -1,10 +1,9 @@
package edu.group5.app.model.organization;
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
+import java.util.stream.Collectors;
-import java.util.HashMap;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -13,26 +12,29 @@
* It interacts with the OrganizationRepository to retrieve organization information
* and contains business logic associated with organization management.
*
- *
It provides fetching logo URLs by web scraping each organization's page on
- * Innsamlingskontrollen.
+ *
It provides fetching logo URLs by delegating to OrganizationScraper.
*
* Fetched logo URLs are cached to avoid redundant network requests.
*/
public class OrganizationService {
private OrganizationRepository organizationRepository;
-
- private final Map logoCache = new HashMap<>();
+ private OrganizationScraper scraper;
/**
- * Constructs an OrganizationService with the given OrganizationRepository.
+ * Constructs an OrganizationService with the given OrganizationRepository and scraper.
* @param organizationRepository the OrganizationRepository to use for managing organization data; must not be null
- * @throws IllegalArgumentException if organizationRepository is null
+ * @param scraper the OrganizationScraper to use for fetching web data; must not be null
+ * @throws IllegalArgumentException if organizationRepository or scraper is null
*/
- public OrganizationService(OrganizationRepository organizationRepository) {
+ public OrganizationService(OrganizationRepository organizationRepository, OrganizationScraper scraper) {
if (organizationRepository == null) {
throw new IllegalArgumentException("OrganizationRepository cannot be null");
}
+ if (scraper == null) {
+ throw new IllegalArgumentException("OrganizationScraper cannot be null");
+ }
this.organizationRepository = organizationRepository;
+ this.scraper = scraper;
}
/**
@@ -48,7 +50,10 @@ public OrganizationRepository getOrganizationRepository() {
* @return a map of trusted organizations by organization number
*/
public Map getTrustedOrganizations() {
- return organizationRepository.getTrustedOrganizations();
+ return organizationRepository.getTrustedOrganizations().values().stream()
+ .sorted(Comparator.comparing(Organization::name))
+ .collect(Collectors.toMap(Organization::orgNumber,
+ org -> org, (e1, e2) -> e1, LinkedHashMap::new));
}
/**
@@ -80,59 +85,30 @@ public Organization findByOrgName(String name) {
* @return the absolute logo URL, or null if not found or pageUrl is invalid
*/
- public String fetchLogoUrl(String pageUrl) {
- if (pageUrl == null || pageUrl.isBlank()) {
- return null;
- }
-
- if (logoCache.containsKey(pageUrl)) {
- return logoCache.get(pageUrl);
- }
-
- try {
- Document doc = Jsoup.connect(pageUrl).get();
- Element img = doc.selectFirst("div.logo img");
-
- if (img != null) {
- String logoUrl = img.absUrl("src");
- logoCache.put(pageUrl, logoUrl);
- return logoUrl;
- }
- } catch (Exception e) {
- System.out.println("Could not get logo for: " + pageUrl);
- }
- return null;
- }
-
/**
- * Fetches all trusted organizations with their logo URLs.
+ * Fetches all trusted organizations with their logo URLs and descriptions.
*
*
- * For each trusted organization, attempts to get its logo using
- * {@link #fetchLogoUrl(String)}. Creates a new Organization
- * object including the logo URL.
+ * For each trusted organization, attempts to get its logo using the scraper.
+ * Creates a new Organization object including the logo URL (description is
+ * already fetched during repository initialization).
*
* @return a map of trusted organizations keyed by organization number, with logos included
*/
public Map getTrustedOrganizationsWithLogos() {
Map original = getTrustedOrganizations();
- Map trustedOrgsWithLogos = new HashMap<>();
-
- for (Organization org : original.values()) {
- String logoUrl = fetchLogoUrl(org.websiteUrl());
-
- Organization newOrg = new Organization(
- org.orgNumber(),
- org.name(),
- org.trusted(),
- org.websiteUrl(),
- org.isPreApproved(),
- org.description(),
- logoUrl
- );
- trustedOrgsWithLogos.put(newOrg.orgNumber(), newOrg);
- }
- return trustedOrgsWithLogos;
+ return original.values().parallelStream()
+ .map(org -> new Organization(
+ org.orgNumber(),
+ org.name(),
+ org.trusted(),
+ org.websiteUrl(),
+ org.isPreApproved(),
+ org.description(),
+ scraper.fetchLogoUrl(org.websiteUrl())
+ ))
+ .sorted(Comparator.comparing(Organization::name))
+ .collect(Collectors.toMap(Organization::orgNumber, org -> org, (e1, e2) -> e1, LinkedHashMap::new));
}
/**
diff --git a/src/main/java/edu/group5/app/model/user/UserService.java b/src/main/java/edu/group5/app/model/user/UserService.java
index 628c785..b237eae 100644
--- a/src/main/java/edu/group5/app/model/user/UserService.java
+++ b/src/main/java/edu/group5/app/model/user/UserService.java
@@ -1,16 +1,21 @@
package edu.group5.app.model.user;
/**
- * Service class for managing user-related operations, such as registration and login.
- * It interacts with the UserRepository to perform these operations and contains the business logic
- * associated with user management, including validation of input data and handling of user authentication.
+ * Service class for managing user-related operations, such as registration and
+ * login.
+ * It interacts with the UserRepository to perform these operations and contains
+ * the business logic
+ * associated with user management, including validation of input data and
+ * handling of user authentication.
*/
public class UserService {
private UserRepository userRepository;
/**
* Constructs a UserService with the given UserRepository.
- * @param userRepository the UserRepository to use for managing user data; must not be null
+ *
+ * @param userRepository the UserRepository to use for managing user data; must
+ * not be null
* @throws IllegalArgumentException if userRepository is null
*/
public UserService(UserRepository userRepository) {
@@ -22,7 +27,9 @@ public UserService(UserRepository userRepository) {
/**
* Getter for the UserRepository used by this service.
- * This method allows access to the user repository for managing user data and performing operations such as registration and login.
+ * This method allows access to the user repository for managing user data and
+ * performing operations such as registration and login.
+ *
* @return the UserRepository instance used by this service
*/
public UserRepository getUserRepository() {
@@ -30,31 +37,39 @@ public UserRepository getUserRepository() {
}
/**
- * Registers a new user with the given information. Validates the input data and creates a new User object
- * based on the specified role. Currently supports registration for customers only.
- * @param role the role of the user (e.g., "Customer"); must not be null or empty
- * @param firstName the first name of the user; must not be null or empty
- * @param lastName the last name of the user; must not be null or empty
- * @param email the email address of the user; must not be null or empty
- * @param passwordHash the hashed password of the user; must not be null or empty
- * @return true if the user was successfully registered, false if any input is invalid or
- * if the role is not supported
- * @throws IllegalArgumentException if any of the input parameters are null or empty
- * or if the role is not supported
+ * Registers a new user with the given information. Validates the input data and
+ * creates a new User object
+ * based on the specified role. Currently supports registration for customers
+ * only.
+ *
+ * @param role the role of the user (e.g., "Customer"); must not be null
+ * or empty
+ * @param firstName the first name of the user; must not be null or empty
+ * @param lastName the last name of the user; must not be null or empty
+ * @param email the email address of the user; must not be null or empty
+ * @param passwordHash the hashed password of the user; must not be null or
+ * empty
+ * @return true if the user was successfully registered, false if any input is
+ * invalid or
+ * if the role is not supported
+ * @throws IllegalArgumentException if any of the input parameters are null or
+ * empty
+ * or if the role is not supported
*/
public boolean registerUser(String role, String firstName, String lastName,
- String email, String passwordHash) {
+ String email, String passwordHash) {
if (role == null || role.trim().isEmpty() ||
firstName == null || firstName.trim().isEmpty() ||
lastName == null || lastName.trim().isEmpty() ||
email == null || email.trim().isEmpty() ||
- passwordHash == null || passwordHash.trim().isEmpty()) {
+ passwordHash == null || passwordHash.trim().isEmpty() ||
+ this.getUserByEmail(email) != null) {
return false;
}
User user;
if (role.equalsIgnoreCase("Customer")) {
user = new Customer(userRepository.getNextUserId(), firstName, lastName, email, passwordHash);
- } else { /* TODO when you switch to a real DB, replace getNextUserId with DB auto-increment/identity and ignore manual ID generation in service*/
+ } else {
return false;
}
this.userRepository.addContent(user);
@@ -63,15 +78,19 @@ public boolean registerUser(String role, String firstName, String lastName,
/**
* Authenticates a user based on the provided email and password.
- * @param email the email address of the user attempting to log in; must not be null or empty
- * @param password the plaintext password of the user attempting to log in; must not be null or empty
+ *
+ * @param email the email address of the user attempting to log in; must not
+ * be null or empty
+ * @param password the plaintext password of the user attempting to log in; must
+ * not be null or empty
* @return the authenticated User object if the login is successful
- * (i.e., the user exists and the password is correct), null otherwise
- * @throws IllegalArgumentException if email is null or empty, or if password is null or empty
+ * (i.e., the user exists and the password is correct), null otherwise
+ * @throws IllegalArgumentException if email is null or empty, or if password is
+ * null or empty
*/
public User login(String email, char[] password) {
if (email == null || email.trim().isEmpty() || password == null || password.length == 0) {
- return null;
+ return null;
}
User user = this.userRepository.findUserByEmail(email);
if (user != null && user.verifyPassword(password)) {
@@ -82,6 +101,7 @@ public User login(String email, char[] password) {
/**
* Retrieves a user by email address.
+ *
* @param email the email address of the user to find; must not be null or empty
* @return the User object if found, null otherwise
*/
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..1a44e46
--- /dev/null
+++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java
@@ -0,0 +1,71 @@
+package edu.group5.app.utils;
+
+import java.math.BigDecimal;
+/**
+ * ParameterValidator is a utility class that provides static methods for validating various types of parameters.
+ * It includes methods for checking strings, integers, objects, and BigDecimal values to ensure they meet specific
+ * criteria such as not being null, not being blank, or being positive.
+ *
+ */
+public final class ParameterValidator {
+
+ /**
+ * Validates that a string parameter is not null and not blank.
+ * @param stringArg the string parameter to validate
+ * @param variableName the name of the variable being validated, used in exception messages
+ * @throws IllegalArgumentException if the string is null or blank
+ */
+ public static final void stringChecker(String stringArg, String variableName) throws IllegalArgumentException {
+ nullCheck(stringArg, variableName);
+ if (stringArg.isBlank()) {
+ throw new IllegalArgumentException(String.format("%s can't be blank", variableName));
+ }
+ }
+
+ /**
+ * Validates that an integer parameter is not null and is a positive integer.
+ * @param intArg the integer parameter to validate
+ * @param variableName the name of the variable being validated, used in exception messages
+ * @throws IllegalArgumentException if the integer is null or not a positive integer
+ */
+ public static final void intChecker(int intArg, String variableName) throws IllegalArgumentException {
+ 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));
+ }
+ }
+}
diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java
index fbf438a..f6eb2b5 100644
--- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java
+++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java
@@ -10,8 +10,10 @@
import javafx.scene.layout.*;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
+import java.util.Comparator;
/**
* A view for the causes page.
@@ -47,7 +49,10 @@ public CausesPageView(AppState appState, NavigationController nav, OrganizationC
setCenter(createBody());
}
- private ScrollPane createBody() {
+ private BorderPane createBody() {
+ BorderPane bodyRoot = new BorderPane();
+ bodyRoot.setTop(createSearchSection());
+
ScrollPane body = new ScrollPane();
body.setId("body");
body.setFitToWidth(true);
@@ -58,12 +63,40 @@ private ScrollPane createBody() {
vBox.setStyle("-fx-padding: 10;");
vBox.setSpacing(10);
vBox.setMaxWidth(Double.MAX_VALUE);
- vBox.getChildren().addAll(
- createSearchSection(),
- createOrganizationSection(null)
- );
+
+ // Load organizations INSTANTLY from cache
+ allOrganizations = orgController.getTrustedOrgs();
+
+ vBox.getChildren().add(createOrganizationSection(null));
body.setContent(vBox);
- return body;
+ 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 = appState.getCurrentOrganization();
+ if (currentOrg != null && orgs.containsKey(currentOrg.orgNumber())) {
+ appState.setCurrentOrganization(orgs.get(currentOrg.orgNumber()));
+ }
+ });
+ });
+
+ return bodyRoot;
}
private HBox createSearchSection() {
@@ -93,25 +126,8 @@ private GridPane createOrganizationSection(String searchTerm) {
organizationGrid = grid;
}
- if (allOrganizations == null) {
- allOrganizations = orgController.getTrustedOrgs();
-
- //Show loading text while organizations and logos are fetched
- grid.add(new javafx.scene.control.Label("Loading..."), 0, 0);
-
- //Fetch trusted organizations with logos asynchronously (runs in background)
- orgController.getOrganizationsWithLogosAsync()
- .thenAccept(orgs -> {
- this.allOrganizations = orgs;
-
- // Update UI when data is ready
- Platform.runLater(() -> updateOrganizationGrid(""));
- });
- return grid;
- }
-
Map organizations = new HashMap<>();
- if (searchTerm != null) {
+ if (searchTerm != null && !searchTerm.isEmpty()) {
// Filter organizations by search term
organizations = filterOrganizations(searchTerm);
} else {
@@ -162,9 +178,12 @@ private Map filterOrganizations(String searchTerm) {
String lowerSearchTerm = searchTerm.toLowerCase();
return allOrganizations.values().stream()
.filter(org -> org.name().toLowerCase().contains(lowerSearchTerm))
+ .sorted(Comparator.comparing(Organization::name))
.collect(Collectors.toMap(
Organization::orgNumber,
- org -> org
+ org -> org,
+ (e1, e2) -> e1,
+ LinkedHashMap::new
));
}
diff --git a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java
index e42ebd5..2ca30bb 100644
--- a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java
+++ b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java
@@ -24,6 +24,8 @@ public class OrganizationCard extends VBox {
private final AppState appState;
private final Organization organization;
private final NavigationController nav;
+ private StackPane imageContainer;
+ private String currentLogoUrl;
public OrganizationCard(AppState appstate, NavigationController nav, Organization org, String img) {
this.appState = appstate;
@@ -32,14 +34,15 @@ public OrganizationCard(AppState appstate, NavigationController nav, Organizatio
setId("mainContainer");
getStylesheets().add(getClass().getResource("/browsepage/browse_org.css").toExternalForm());
+ imageContainer = createImageContainer(img);
getChildren().addAll(
- imageContainer(img),
+ imageContainer,
orgName(org.name()),
checkMarkContainer()
);
setOnMouseClicked(e -> {
- appstate.setCurrentOrganization(organization);
+ appstate.setCurrentOrganization(getOrganizationWithCurrentLogo());
nav.showOrganizationPage();
});
@@ -48,7 +51,41 @@ public OrganizationCard(AppState appstate, NavigationController nav, Organizatio
setAlignment(Pos.CENTER);
}
- private StackPane imageContainer(String img) {
+ public Organization getOrganization() {
+ return organization;
+ }
+
+ public void updateLogo(String logoUrl) {
+ this.currentLogoUrl = logoUrl;
+ if (imageContainer == null) return;
+ imageContainer.getChildren().clear();
+ if (logoUrl != null && !logoUrl.isBlank()) {
+ ImageView logo = new ImageView(new Image(logoUrl, true));
+ logo.setId("logo");
+ logo.setSmooth(true);
+ logo.setPreserveRatio(true);
+ logo.setFitHeight(80);
+ logo.setFitWidth(80);
+ imageContainer.getChildren().add(logo);
+ }
+ }
+
+ private Organization getOrganizationWithCurrentLogo() {
+ if (currentLogoUrl == null) {
+ return organization;
+ }
+ return new Organization(
+ organization.orgNumber(),
+ organization.name(),
+ organization.trusted(),
+ organization.websiteUrl(),
+ organization.isPreApproved(),
+ organization.description(),
+ currentLogoUrl
+ );
+ }
+
+ private StackPane createImageContainer(String img) {
StackPane imageContainer = new StackPane();
imageContainer.setId("imageContainer");
diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java
index f9681fc..9444421 100644
--- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java
+++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java
@@ -5,8 +5,8 @@
import edu.group5.app.model.AppState;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
-import javafx.scene.control.Button;
import javafx.scene.control.TextField;
+import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.TilePane;
@@ -16,7 +16,7 @@
import javafx.scene.Node;
import java.math.BigDecimal;
-import java.util.*;
+import java.util.Objects;
/**
* A view for the Donation Page.
@@ -31,13 +31,9 @@ public class DonationPageView extends BorderPane {
private final NavigationController nav;
private final DonationController donationController;
- private final List allDonationElements = new ArrayList<>();
- private final List allPaymentElements = new ArrayList<>();
- private final Map