diff --git a/src/main/java/edu/group5/app/control/LoginController.java b/src/main/java/edu/group5/app/control/AuthController.java similarity index 57% rename from src/main/java/edu/group5/app/control/LoginController.java rename to src/main/java/edu/group5/app/control/AuthController.java index ae9d22e..249a436 100644 --- a/src/main/java/edu/group5/app/control/LoginController.java +++ b/src/main/java/edu/group5/app/control/AuthController.java @@ -13,17 +13,48 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -public class LoginController { +/** + * 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 LoginController(AppState appState, NavigationController nav, UserService userService) { + public AuthController(AppState appState, NavigationController nav, UserService userService) { this.appState = appState; this.nav = nav; this.userService = userService; } + /** + * Handles the 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() || @@ -56,17 +87,30 @@ public void handleSignUp(SignUpPageView view, String firstName, String lastName, if (success) { User user = userService.getUserByEmail(email); - appState.setCurrentUser(user); - nav.showHomePage(); - } else { - view.showError("Registration failed. Email may already be in use."); - } + appState.setCurrentUser(user); + nav.showHomePage(); } else { - view.showError("Registration failed. Must Accept Privacy Policy to create account."); + view.showError("Registration failed. Email may already be in use."); } - } +} + /** + * Handles the login of a {@link 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"); @@ -83,10 +127,17 @@ public void handleLogin(LoginPageView view, String email, char[] passwordChars) } } + /** + * Handles the logout of a {@link User}. + * + *

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

+ */ public void handleLogout() { appState.setCurrentUser(null); appState.setCurrentOrganization(null); appState.setCurrentDonationAmount(null); + appState.setCurrentPaymentMethod(null); nav.showLoginPage(); } -} +} \ No newline at end of file diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index a579a9b..8a8ec79 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -15,6 +15,19 @@ 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: + *

+ *

+ */ public class DonationController { private final AppState appState; private final NavigationController nav; @@ -26,21 +39,45 @@ public DonationController(AppState appState, NavigationController nav, DonationS this.service = service; } + /** + * Retrieves all donations made by a specific user. + * + * @param userId the ID of the user + * @return a map of donations. + */ public Map getUserDonations(int userId) { return service.getDonationRepository().filterByUser(userId); } - public Set getUniqueOrgs() { + /** + * Returns a set of unique organization IDs that the current user + * has donated to. + * + * @return a set of organization IDs + */ + public Set getUniqueOrganizationIDs() { Map userDonations = getUserDonations(appState.getCurrentUser().getUserId()); - Set uniqueOrgs = new HashSet<>(); + Set uniqueOrganizations = new HashSet<>(); for (Donation donation : userDonations.values()) { - uniqueOrgs.add(donation.organizationId()); + uniqueOrganizations.add(donation.organizationId()); } - return uniqueOrgs; + 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(); diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index 1b794d5..7bfbc8c 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -16,6 +16,9 @@ import edu.group5.app.view.userpage.UserPageView; import javafx.scene.layout.BorderPane; +/** + * Controller responsible for navigating between views within the root node. + */ public class NavigationController { private final BorderPane root; private final Header header; @@ -23,7 +26,7 @@ public class NavigationController { private final AppState appState; - private final LoginController loginController; + private final AuthController authController; private final DonationController donationController; private final OrganizationController organizationController; @@ -34,9 +37,9 @@ public NavigationController(BorderPane root, AppState appState, UserService user this.appState = appState; - this.loginController = new LoginController(appState, this, userService); + this.authController = new AuthController(appState, this, userService); this.donationController = new DonationController(appState, this, donationService); - this.organizationController = new OrganizationController(appState, this, organizationService); + this.organizationController = new OrganizationController(organizationService); } public void showHomePage() { @@ -46,12 +49,12 @@ public void showHomePage() { public void showLoginPage() { root.setTop(loginHeader); - root.setCenter(new LoginPageView(appState, this, loginController)); + root.setCenter(new LoginPageView(appState, this, authController)); } public void showSignUpPage() { root.setTop(loginHeader); - root.setCenter(new SignUpPageView(appState, this, loginController)); + root.setCenter(new SignUpPageView(appState, this, authController)); } public void showPaymentCompletePage() { @@ -80,6 +83,6 @@ public void showAboutUsPage() { public void showUserPage() { root.setTop(header); - root.setCenter(new UserPageView(appState, this, loginController, donationController, organizationController)); + root.setCenter(new UserPageView(appState, this, authController, donationController, organizationController)); } } diff --git a/src/main/java/edu/group5/app/control/OrganizationController.java b/src/main/java/edu/group5/app/control/OrganizationController.java index 499b7c9..fb65127 100644 --- a/src/main/java/edu/group5/app/control/OrganizationController.java +++ b/src/main/java/edu/group5/app/control/OrganizationController.java @@ -1,28 +1,26 @@ package edu.group5.app.control; -import edu.group5.app.model.AppState; import edu.group5.app.model.organization.Organization; import edu.group5.app.model.organization.OrganizationService; import java.util.Map; import java.util.concurrent.CompletableFuture; +/** + * Controller responsible for organization-related operations. + */ public class OrganizationController { - private final AppState appState; - private final NavigationController nav; private final OrganizationService service; - public OrganizationController(AppState appState, NavigationController nav, OrganizationService service) { - this.appState = appState; - this.nav = nav; + public OrganizationController(OrganizationService service) { this.service = service; } - public Organization getOrgById(int orgId) { + public Organization getOrganizationById(int orgId) { return service.findByOrgNumber(orgId); } - public Map getTrustedOrgs() { + public Map getTrustedOrganizations() { return service.getTrustedOrganizations(); } diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index f6eb2b5..7db3ee9 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -65,7 +65,7 @@ private BorderPane createBody() { vBox.setMaxWidth(Double.MAX_VALUE); // Load organizations INSTANTLY from cache - allOrganizations = orgController.getTrustedOrgs(); + allOrganizations = orgController.getTrustedOrganizations(); vBox.getChildren().add(createOrganizationSection(null)); body.setContent(vBox); @@ -126,6 +126,23 @@ private GridPane createOrganizationSection(String searchTerm) { 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 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 3fb9bb7..11b3ba7 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java @@ -1,7 +1,7 @@ package edu.group5.app.view.loginpage; import edu.group5.app.control.NavigationController; -import edu.group5.app.control.LoginController; +import edu.group5.app.control.AuthController; import edu.group5.app.model.AppState; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -23,16 +23,16 @@ public class LoginPageView extends BorderPane { private final AppState appState; private final NavigationController nav; - private final LoginController loginController; + private final AuthController authController; private TextField emailField; private PasswordField passwordField; private Label errorLabel; - public LoginPageView(AppState appState, NavigationController nav, LoginController loginController) { + public LoginPageView(AppState appState, NavigationController nav, AuthController authController) { this.appState = appState; this.nav = nav; - this.loginController = loginController; + this.authController = authController; HBox content = new HBox(); content.setFillHeight(true); @@ -105,7 +105,7 @@ private Button getLoginBtn() { Button loginBtn = new Button("Log In"); loginBtn.setMaxWidth(300); loginBtn.setId("login-btn"); - loginBtn.setOnMouseClicked(e -> loginController.handleLogin( + loginBtn.setOnMouseClicked(e -> authController.handleLogin( this, getEmail(), getPassword() diff --git a/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java b/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java index 160f929..8638a1f 100644 --- a/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.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.NavigationController; -import edu.group5.app.control.LoginController; +import edu.group5.app.control.AuthController; import edu.group5.app.model.AppState; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -24,7 +24,7 @@ public class SignUpPageView extends BorderPane { private final AppState appState; private final NavigationController nav; - private final LoginController loginController; + private final AuthController authController; private TextField nameField; private TextField surnameField; @@ -32,10 +32,10 @@ public class SignUpPageView extends BorderPane { private PasswordField passwordField; private Label errorLabel; - public SignUpPageView(AppState appState, NavigationController nav, LoginController loginController) { + public SignUpPageView(AppState appState, NavigationController nav, AuthController authController) { this.appState = appState; this.nav = nav; - this.loginController = loginController; + this.authController = authController; HBox content = new HBox(); content.setFillHeight(true); @@ -138,7 +138,7 @@ private Button getSignUpBtn() { Button signUpBtn = new Button("Sign Up"); signUpBtn.setMaxWidth(300); signUpBtn.setId("login-btn"); - signUpBtn.setOnMouseClicked(e -> loginController.handleSignUp( + signUpBtn.setOnMouseClicked(e -> authController.handleSignUp( this, getFirstName(), getLastName(), diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index e3b7c8b..5e2370a 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -3,7 +3,7 @@ import edu.group5.app.control.DonationController; import edu.group5.app.control.NavigationController; import edu.group5.app.control.OrganizationController; -import edu.group5.app.control.LoginController; +import edu.group5.app.control.AuthController; import edu.group5.app.model.AppState; import edu.group5.app.model.donation.Donation; import edu.group5.app.model.organization.Organization; @@ -31,14 +31,14 @@ public class UserPageView extends BorderPane { private final AppState appState; private final NavigationController nav; - private final LoginController loginController; + private final AuthController authController; private final DonationController donationController; private final OrganizationController organizationController; - public UserPageView(AppState appState, NavigationController nav, LoginController loginController, DonationController donationController, OrganizationController organizationController) { + public UserPageView(AppState appState, NavigationController nav, AuthController authController, DonationController donationController, OrganizationController organizationController) { this.appState = appState; this.nav = nav; - this.loginController = loginController; + this.authController = authController; this.donationController = donationController; this.organizationController = organizationController; @@ -70,7 +70,7 @@ private HBox createProfileSection() { Button logoutBtn = new Button("Logout"); logoutBtn.getStyleClass().add("logout-button"); - logoutBtn.setOnAction(e -> loginController.handleLogout()); + logoutBtn.setOnAction(e -> authController.handleLogout()); VBox info = new VBox(10, name, email, location, logoutBtn); info.setAlignment(Pos.CENTER_LEFT); @@ -91,15 +91,15 @@ private VBox createCausesSection() { causesFlow.setStyle("-fx-padding: 10;"); causesFlow.getStyleClass().add("section-box"); - Set uniqueOrgs = donationController.getUniqueOrgs(); + Set uniqueOrganizations = donationController.getUniqueOrganizationIDs(); - if (uniqueOrgs.isEmpty()) { + if (uniqueOrganizations.isEmpty()) { Label noCauses = new Label("No causes supported yet"); noCauses.setStyle("-fx-text-fill: #999;"); causesFlow.getChildren().add(noCauses); } else { - for (int orgId : uniqueOrgs) { - Organization org = organizationController.getOrgById(orgId); + for (int orgId : uniqueOrganizations) { + Organization org = organizationController.getOrganizationById(orgId); if (org != null) { causesFlow.getChildren().add(createCauseChip(org)); } @@ -128,40 +128,40 @@ private VBox createDonationsSection() { 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)); User currentUser = appState.getCurrentUser(); - Map userDonations = + Map userDonations = donationController.getUserDonations(currentUser.getUserId()); // Filter donations based on search searchField.textProperty().addListener((obs, oldVal, newVal) -> { donationsBox.getChildren().clear(); - + if (userDonations.isEmpty()) { Label noDonations = new Label("No donations yet"); noDonations.setStyle("-fx-text-fill: #999;"); donationsBox.getChildren().add(noDonations); return; } - + String searchTerm = newVal.toLowerCase().trim(); boolean found = false; - + for (Donation donation : userDonations.values()) { - Organization org = organizationController.getOrgById(donation.organizationId()); + 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;"); @@ -175,24 +175,18 @@ private VBox createDonationsSection() { donationsBox.getChildren().add(noDonations); } else { for (Donation donation : userDonations.values()) { + Organization org = organizationController.getOrganizationById(donation.organizationId()); + String orgName = (org != null) ? org.name() : "Unknown Organization"; donationsBox.getChildren().add(createDonationCard(donation)); - } } + scrollPane.setContent(donationsBox); return new VBox(10, title, searchBox, scrollPane); - - } - - - private Label createCauseChip(Organization org) { - Label chip = new Label(org.name()); - chip.getStyleClass().add("cause-chip"); - return chip; } private BorderPane createDonationCard(Donation donation) { - Organization org = organizationController.getOrgById(donation.organizationId()); + Organization org = organizationController.getOrganizationById(donation.organizationId()); String orgName = (org != null) ? org.name() : "Unknown Organization"; // Use BorderPane to fix columns: LEFT | SPACE | RIGHT @@ -208,17 +202,28 @@ private BorderPane createDonationCard(Donation donation) { // 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; + } }