diff --git a/pom.xml b/pom.xml index 7a55551..f9a8b88 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,12 @@ slf4j-simple 2.0.9 + + com.h2database + h2 + 2.2.224 + runtime + diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index 887fe4d..d657fb8 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -1,10 +1,100 @@ 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.model.donation.DonationRepository; +import edu.group5.app.model.donation.DonationService; +import edu.group5.app.model.organization.OrganizationRepository; +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 javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.stage.Stage; + +import java.util.List; + +import java.util.logging.Logger; + /** - * Hello world! + * Main entry point for the Help-Me-Help charity donation application. + * Handles database connection, data loading, and application setup. */ -public class App { - public static void main(String[] args) throws InterruptedException { - System.out.println("Hello World!"); +public class App extends Application { + DbWrapper dbWrapper; + UserRepository userRepository; + DonationRepository donationRepository; + private Logger logger; + private MainController controller; + private Scene scene; + + @Override + public void init() { + this.logger = Logger.getLogger(App.class.getName()); + this.logger.info("Application starting"); + + this.dbWrapper = new DbWrapper(false); + OrgApiWrapper orgApiWrapper = new OrgApiWrapper("https://app.innsamlingskontrollen.no/api/public/v1/all"); + + while (!dbWrapper.connect()) { + this.logger.warning("Failed to connect to database"); + } + + // Load data from database + List userData = dbWrapper.importUsers(); + List donationData = dbWrapper.fetchAllDonations(); + dbWrapper.disconnect(); + + // Load organizations from API + Object[] organizationData = new Object[0]; + try { + if (orgApiWrapper.importData()) { + organizationData = orgApiWrapper.getData(); + } + } catch (InterruptedException e) { + System.err.println("Failed to load organization data: " + e.getMessage()); + } + + // Create repositories with fetched data + this.userRepository = new UserRepository(userData); + this.donationRepository = new DonationRepository(donationData); + OrganizationRepository organizationRepository = new OrganizationRepository(organizationData); + + // 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(); + } + + @Override + public void start(Stage stage) { + this.controller.showLoginPage(); + + stage.getIcons().add(new Image(getClass().getResource("/header/images/hmh-logo.png").toExternalForm())); + stage.setTitle("Help-Me-Help"); + stage.setScene(this.scene); + stage.show(); + } + + @Override + public void stop() throws Exception { + super.stop(); + this.logger.info("Application stopping"); + this.dbWrapper.connect(); + this.dbWrapper.exportUsers(this.userRepository.export()); + this.dbWrapper.exportDonations(this.donationRepository.export()); + this.dbWrapper.disconnect(); + } + + public static void main(String[] args) { + launch(args); } } diff --git a/src/main/java/edu/group5/app/control/BrowseCardController.java b/src/main/java/edu/group5/app/control/BrowseCardController.java new file mode 100644 index 0000000..9f86271 --- /dev/null +++ b/src/main/java/edu/group5/app/control/BrowseCardController.java @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..0922e68 --- /dev/null +++ b/src/main/java/edu/group5/app/control/BrowsePageController.java @@ -0,0 +1,9 @@ +package edu.group5.app.control; + +public class BrowsePageController { + private final MainController controller; + + public BrowsePageController(MainController mainController) { + this.controller = mainController; + } +} diff --git a/src/main/java/edu/group5/app/control/HeaderController.java b/src/main/java/edu/group5/app/control/HeaderController.java new file mode 100644 index 0000000..0a1e424 --- /dev/null +++ b/src/main/java/edu/group5/app/control/HeaderController.java @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..10d3fbf --- /dev/null +++ b/src/main/java/edu/group5/app/control/HomePageController.java @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..b28f191 --- /dev/null +++ b/src/main/java/edu/group5/app/control/LoginPageController.java @@ -0,0 +1,44 @@ +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 new file mode 100644 index 0000000..2b75487 --- /dev/null +++ b/src/main/java/edu/group5/app/control/MainController.java @@ -0,0 +1,121 @@ +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/OrganizationPageController.java b/src/main/java/edu/group5/app/control/OrganizationPageController.java new file mode 100644 index 0000000..dd8f1bf --- /dev/null +++ b/src/main/java/edu/group5/app/control/OrganizationPageController.java @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..2d8d874 --- /dev/null +++ b/src/main/java/edu/group5/app/control/SignInPageController.java @@ -0,0 +1,58 @@ +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 new file mode 100644 index 0000000..5fed3e2 --- /dev/null +++ b/src/main/java/edu/group5/app/control/donationpage/DonationPageController.java @@ -0,0 +1,60 @@ +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 new file mode 100644 index 0000000..d1bc3a5 --- /dev/null +++ b/src/main/java/edu/group5/app/control/donationpage/PaymentCompleteController.java @@ -0,0 +1,15 @@ +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/control/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java new file mode 100644 index 0000000..7e3adc0 --- /dev/null +++ b/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java @@ -0,0 +1,238 @@ +package edu.group5.app.control.wrapper; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +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; + +public class DbWrapper { + protected Connection connection; + private static final String CONNECTION_TYPE = "jdbc:h2:"; + private static final String DB_SCRIPT = "INIT=RUNSCRIPT FROM 'classpath:"; + private String connectionString; + private List users; + private List donations; + private Logger logger = Logger.getLogger(DbWrapper.class.getName()); + + public DbWrapper(boolean test) { + if (test) { + this.connectionString = CONNECTION_TYPE + "mem:test;" + DB_SCRIPT + "test_init.sql'"; + } else { + this.connectionString = CONNECTION_TYPE + "file:./help-me-help;" + DB_SCRIPT + "init.sql'"; + } + this.logger.info("connectionString constructed"); + } + + public boolean connect() { + try { + this.connection = DriverManager.getConnection(this.connectionString); + if (this.connection.isValid(0)) { + this.logger.info("Database connected"); + return true; + } else { + this.logger.warning("Failed to connect to database"); + return false; + } + } catch (SQLException e) { + this.logger.log(Level.SEVERE, "Failed to connect to database due to exception", e); + return false; + } + } + + public boolean disconnect() { + try{ this.connection.close(); } catch (Exception e) {}; + try { + return this.connection.isClosed(); + } catch (Exception e) { + this.logger.log(Level.WARNING, "Failed to check if connection is closed due to exception", e); + return false; + } + } + + private void close(ResultSet results, PreparedStatement ps) { + try { results.close(); } catch (Exception e) {} + try { ps.close(); } catch (Exception e) {} + this.logger.info("results and ps closed"); + } + + public List importUsers() { + PreparedStatement ps = null; + ResultSet results = null; + try{ + ps = this.connection.prepareStatement("SELECT * FROM users"); + results = ps.executeQuery(); + List data = new ArrayList(); + while (results.next()) { + data.add( + new Object[] { + results.getInt("user_id"), + results.getString("role"), + results.getString("first_name"), + results.getString("last_name"), + results.getString("email"), + results.getString("password_hash") + }); + } + this.users = data; + this.logger.info("Users imported"); + } catch (SQLException e) { + this.logger.log(Level.SEVERE, "Unexpected SQL exception", e); + } finally { + this.close(results, ps); + } + return this.users; + } + + public int exportUsers(List data) { + 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"); + } + + + 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 (?, ?, ?, ?, ?, ?)"); + for (Object[] row : data) { + try { + ps.setInt(1, (int) row[0]); + for (int i = 1; i < row.length; i++) { + ps.setString(i + 1, (String) row[i]); + } + } catch (Exception e) { + throw new IllegalArgumentException("One or more rows in data contains a wrong datatype"); + } + rowsAffected += ps.executeUpdate(); + } + this.logger.info("Users exported"); + } catch (SQLException e) { + this.logger.log(Level.SEVERE, "Unexpected SQL exception", e); + } finally { + this.close(null, ps); + } + return rowsAffected; + } + + public List fetchAllDonations() { + return this.importDonations(0, true); + } + + public List importDonations(int user_id) { + return this.importDonations(user_id, false); + } + + private List importDonations(int user_id, boolean all) { + PreparedStatement ps = null; + ResultSet results = null; + try{ + if (all) { + ps = this.connection.prepareStatement("SELECT * FROM donations"); + } else { + ps = this.connection.prepareStatement("SELECT * FROM donations WHERE user_id = ?"); + ps.setInt(1, user_id); + } + results = ps.executeQuery(); + List data = new ArrayList(); + while (results.next()) { + data.add( + new Object[] { + results.getInt(1), + results.getInt(2), + results.getInt(3), + results.getBigDecimal(4), + results.getTimestamp(5), + results.getString(6) + }); + } + this.donations = data; + this.logger.info("Donations imported"); + } catch (SQLException e) { + this.logger.log(Level.SEVERE, "Unexpected SQL exception", e); + } finally { + this.close(results, ps); + } + return this.donations; + } + + public int exportDonations(List data) { + 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"); + } + + 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 = ?), ?, ?, ?, ?)"); + for (Object[] row : data) { + try { + for (int i = 0; i < 3; i++) { + ps.setInt(i + 1, (int) row[i]); + } + ps.setBigDecimal(4, (BigDecimal) row[3]); + ps.setTimestamp(5, (Timestamp) row[4]); + ps.setString(6, (String) row[5]); + } catch (Exception e) { + throw new IllegalArgumentException("One or more rows in data contains a wrong datatype"); + } + rowsAffected += ps.executeUpdate(); + } + this.logger.info("Donations exported"); + } catch (SQLException e) { + this.logger.log(Level.SEVERE, "Unexpected SQL exception", e); + } finally { + this.close(null, ps); + } + return rowsAffected; + } + +} diff --git a/src/main/java/edu/group5/app/control/OrgApiWrapper.java b/src/main/java/edu/group5/app/control/wrapper/OrgApiWrapper.java similarity index 85% rename from src/main/java/edu/group5/app/control/OrgApiWrapper.java rename to src/main/java/edu/group5/app/control/wrapper/OrgApiWrapper.java index 24c82ee..39ac283 100644 --- a/src/main/java/edu/group5/app/control/OrgApiWrapper.java +++ b/src/main/java/edu/group5/app/control/wrapper/OrgApiWrapper.java @@ -1,4 +1,4 @@ -package edu.group5.app.control; +package edu.group5.app.control.wrapper; import java.io.IOException; import java.net.URI; @@ -17,7 +17,8 @@ public class OrgApiWrapper extends Wrapper { private HttpRequest request; /** - * The constructor, which takes a url String and constructs a URI and HttpRequest object from it. + * The constructor, which takes a url String and constructs a URI and + * HttpRequest object from it. * If the url is invalid, it will throw a fitting exception. * * @param urlString A string of the URL that's being connected to. @@ -44,11 +45,13 @@ public OrgApiWrapper(String urlString) { /** * A method for importing data from the wrapped API. * - * @return Returns a boolean, which indicates if the import was successful. Will be False if, for - * example, there is no internet connection. + * @return Returns a boolean, which indicates if the import was successful. Will + * be False if, for + * example, there is no internet connection. * - * @throws InterruptedException This exception is thrown whenever the program is interrupted, like - * by ctrl + c. + * @throws InterruptedException This exception is thrown whenever the program is + * interrupted, like + * by ctrl + c. */ @Override public boolean importData() throws InterruptedException { @@ -67,7 +70,8 @@ public boolean importData() throws InterruptedException { /** * A method for accessing the imported data. * - * @return Returns an array with HashMaps, which is how data is structured in the API. + * @return Returns an array with HashMaps, which is how data is structured in + * the API. */ @Override public Object[] getData() { diff --git a/src/main/java/edu/group5/app/control/Wrapper.java b/src/main/java/edu/group5/app/control/wrapper/Wrapper.java similarity index 66% rename from src/main/java/edu/group5/app/control/Wrapper.java rename to src/main/java/edu/group5/app/control/wrapper/Wrapper.java index ea55179..992b7a9 100644 --- a/src/main/java/edu/group5/app/control/Wrapper.java +++ b/src/main/java/edu/group5/app/control/wrapper/Wrapper.java @@ -1,4 +1,4 @@ -package edu.group5.app.control; +package edu.group5.app.control.wrapper; /** * An abstract class for all Wrappers of datasets. @@ -9,13 +9,16 @@ protected Wrapper() { } /** - * An abstract method for importing data from the dataset that child methods wrap. + * An abstract method for importing data from the dataset that child methods + * wrap. * - * @return Returns a boolean, which indicates if the import was successful. Will be False if, for - * example, there is no internet connection. + * @return Returns a boolean, which indicates if the import was successful. Will + * be False if, for + * example, there is no internet connection. * - * @throws InterruptedException This exception is thrown whenever the program is interrupted, like - * by ctrl + c. + * @throws InterruptedException This exception is thrown whenever the program is + * interrupted, like + * by ctrl + c. */ public abstract boolean importData() throws InterruptedException; diff --git a/src/main/java/edu/group5/app/model/DBRepository.java b/src/main/java/edu/group5/app/model/DBRepository.java index f3363b7..3cd1fa9 100644 --- a/src/main/java/edu/group5/app/model/DBRepository.java +++ b/src/main/java/edu/group5/app/model/DBRepository.java @@ -1,18 +1,21 @@ package edu.group5.app.model; + import java.util.HashMap; import java.util.Map; import java.util.List; + /** * Abstract base class for repositories that store their data * in a database-related structure. * *

- * Extends {@link Repository} and specifies that the content - * is stored as a {@link Map}. + * Extends {@link Repository} and specifies that the content + * is stored as a {@link Map}. *

*/ public abstract class DBRepository extends Repository { protected final Map contentLock; + /** * Constructs a DBRepository with the given content. * @@ -23,19 +26,18 @@ protected DBRepository(Map content) { this.contentLock = new HashMap<>(); } - protected void updateContentLock() { - synchronized (contentLock) { - contentLock.clear(); - contentLock.putAll(this.content); - } - } + protected abstract void updateContentLock(); public abstract boolean addContent(V value); /** - * Exports the repository content as a list of Object arrays, where each array represents a row of data. - * This method is intended for converting the repository content into a format suitable for database storage or export. - * @return a List of Object arrays representing the repository content for database export + * Exports the repository content as a list of Object arrays, where each array + * represents a row of data. + * This method is intended for converting the repository content into a format + * suitable for database storage or export. + * + * @return a List of Object arrays representing the repository content for + * database export */ public abstract List export(); } 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 6181460..a55ea74 100644 --- a/src/main/java/edu/group5/app/model/donation/DonationRepository.java +++ b/src/main/java/edu/group5/app/model/donation/DonationRepository.java @@ -15,7 +15,7 @@ * Repository class for Donation. * *

- * Extends {@link DBRepository} and manages Donation entities. + * Extends {@link DBRepository} and manages Donation entities. *

*/ public class DonationRepository extends DBRepository { @@ -23,10 +23,12 @@ public class DonationRepository extends DBRepository { /** * Constructs DonationRepository from a list of Object[] rows from the database. - * @param rows List of Object[] representing donations from the DB. - * Each row must have 6 elements: - * [donationId, userId, organizationId, amount, date, paymentMethod] - * @throws IllegalArgumentException if the input list is null or any row is invalid + * + * @param rows List of Object[] representing donations from the DB. + * Each row must have 6 elements: + * [donationId, userId, organizationId, amount, date, paymentMethod] + * @throws IllegalArgumentException if the input list is null or any row is + * invalid */ public DonationRepository(List rows) { super(new HashMap<>()); @@ -35,12 +37,12 @@ public DonationRepository(List rows) { } this.content = new HashMap<>(); for (Object[] row : rows) { - if (row == null || row.length != 6 ) { + if (row == null || row.length != 6) { throw new IllegalArgumentException("Each row must contain exactly 6 elements"); } int donationId = (int) row[0]; int customerId = (int) row[1]; - int organizationId = (int) row[2]; + int organizationId = (int) row[2]; BigDecimal amount = (BigDecimal) row[3]; Timestamp date = (Timestamp) row[4]; String paymentMethod = (String) row[5]; @@ -48,23 +50,39 @@ public DonationRepository(List rows) { Donation donation = new Donation(donationId, customerId, organizationId, amount, date, paymentMethod); this.content.put(donationId, donation); } - super.updateContentLock(); + this.updateContentLock(); + } + + @Override + protected void updateContentLock() { + synchronized (contentLock) { + this.contentLock.clear(); + this.contentLock.putAll(this.content); + } } @Override public List export() { - return content.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .map(entry -> { Donation donation = entry.getValue(); - return new Object[] { - donation.donationId(), donation.userId(), - donation.organizationId(), donation.amount(), - donation.date(), donation.paymentMethod()};}) - .toList(); + Map output = new HashMap<>(this.content); + for (int i : super.contentLock.keySet()) { + output.remove(i); + } + this.updateContentLock(); + return output.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(entry -> { + Donation donation = entry.getValue(); + return new Object[] { + donation.donationId(), donation.userId(), + donation.organizationId(), donation.amount(), + donation.date(), donation.paymentMethod() }; + }) + .toList(); } /** * Retrieves a donation by its ID. + * * @param donationId the ID of the donation to retrieve * @return the Donation object with the specified ID, or null if not found * @throws IllegalArgumentException if the donationId is not positive @@ -77,10 +95,12 @@ public Donation getDonationById(int donationId) { } /** - * Generates the next donation ID based on the current maximum ID in the repository. + * Generates the next donation ID based on the current maximum ID in the + * repository. + * * @return the next donation ID to be used for a new donation */ - public int getNextDonationId() { + public int getNextDonationId() { return content.keySet().stream().max(Integer::compareTo).orElse(0) + 1; } /* TODO change this when data database is introduced */ @@ -91,22 +111,22 @@ public Map getAllDonations() { /** * Adds a new donation to the repository *

- * The donation is stored using its {@code donationId} as the key. - * If a donation with the same ID already exists, the donation - * will not be added. + * The donation is stored using its {@code donationId} as the key. + * If a donation with the same ID already exists, the donation + * will not be added. *

* * @param donation the donation to add * @return {@code true} if the donation was successfully added, and - * {@code false} if a donation with the same ID already exists + * {@code false} if a donation with the same ID already exists */ @Override public boolean addContent(Donation donation) { if (donation == null) { throw new IllegalArgumentException("Donation cannot be null"); } - if (content.containsKey(donation.donationId())){ - return false; + if (content.containsKey(donation.donationId())) { + return false; } this.content.put(donation.donationId(), donation); return true; @@ -116,12 +136,12 @@ public boolean addContent(Donation donation) { * Returns all donations sorted by date (ascending). * *

- * The returned map preserves the sorted order. + * The returned map preserves the sorted order. *

* * @return a new {@link HashMap} containing the donations sorted by date */ - public HashMap sortByDate(){ + public HashMap sortByDate() { return content.entrySet().stream() .sorted(Map.Entry.comparingByValue( Comparator.comparing(Donation::date))) @@ -136,7 +156,7 @@ public HashMap sortByDate(){ * Returns all donations sorted by amount (ascending). * *

- * The returned map preserves the sorted order from lowest to highest amount. + * The returned map preserves the sorted order from lowest to highest amount. *

* * @return a new {@link HashMap} containing the donations sorted by amount. @@ -149,12 +169,12 @@ public HashMap sortByAmount() { Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, - LinkedHashMap::new - )); + LinkedHashMap::new)); } /** * Returns all donations associated with a specific organization. + * * @param orgNumber the organization ID to filter by * @return a map containing all donations that belong to the given organization * @throws IllegalArgumentException if the orgNumber is not positive @@ -165,17 +185,17 @@ public HashMap filterByOrganization(int orgNumber) { } return content.entrySet() .stream() - .filter(entry -> entry.getValue().organizationId() == orgNumber) + .filter(entry -> entry.getValue().organizationId() == orgNumber) .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, - LinkedHashMap::new - )); + LinkedHashMap::new)); } /** * Returns all donations made by a specific user. + * * @param userId the user ID to filter by * @return a map containing all donations that belong to the given user * @throws IllegalArgumentException if the userId is not positive @@ -190,7 +210,6 @@ public HashMap filterByUser(int userId) { Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, - LinkedHashMap::new - )); + LinkedHashMap::new)); } } diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java new file mode 100644 index 0000000..c5979f5 --- /dev/null +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -0,0 +1,58 @@ +package edu.group5.app.model.organization; + +import java.util.Map; + +/** + * Service class for managing organization-related operations. + * It interacts with the OrganizationRepository to retrieve organization information + * and contains business logic associated with organization management. + */ +public class OrganizationService { + private OrganizationRepository organizationRepository; + + /** + * Constructs an OrganizationService with the given OrganizationRepository. + * @param organizationRepository the OrganizationRepository to use for managing organization data; must not be null + * @throws IllegalArgumentException if organizationRepository is null + */ + public OrganizationService(OrganizationRepository organizationRepository) { + if (organizationRepository == null) { + throw new IllegalArgumentException("OrganizationRepository cannot be null"); + } + 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; + } + + /** + * Retrieves all trusted organizations. + * @return a map of trusted organizations by organization number + */ + public Map getTrustedOrganizations() { + return organizationRepository.getTrustedOrganizations(); + } + + /** + * Finds an organization by its organization number. + * @param orgNumber the organization number to find + * @return the Organization if found, null otherwise + */ + public Organization findByOrgNumber(int orgNumber) { + return organizationRepository.findByOrgNumber(orgNumber); + } + + /** + * Finds an organization by its name. + * @param name the name of the organization + * @return the Organization if found, null otherwise + */ + public Organization findByOrgName(String name) { + return organizationRepository.findByOrgName(name); + } +} 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 692126f..193f433 100644 --- a/src/main/java/edu/group5/app/model/user/UserRepository.java +++ b/src/main/java/edu/group5/app/model/user/UserRepository.java @@ -6,11 +6,13 @@ import edu.group5.app.model.DBRepository; -public class UserRepository extends DBRepository{ +public class UserRepository extends DBRepository { public final static String ROLE_CUSTOMER = "Customer"; + /** * Constructs UserRepository using Hashmap, * and extends the content from DBRepository. + * * @param content the underlying map used to store users, * where the key represents the user ID */ @@ -20,7 +22,7 @@ public UserRepository(List rows) { throw new IllegalArgumentException("The list of rows cannot be null"); } for (Object[] row : rows) { - if (row == null || row.length != 6 ) { + if (row == null || row.length != 6) { throw new IllegalArgumentException("Each row must contain exactly 6 elements"); } int userId = (int) row[0]; @@ -38,28 +40,46 @@ public UserRepository(List rows) { } this.content.put(userId, user); } - super.updateContentLock(); + this.updateContentLock(); } + @Override + protected void updateContentLock() { + synchronized (contentLock) { + this.contentLock.clear(); + this.contentLock.putAll(this.content); + } + } @Override public List export() { - return content.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .map(entry -> { User user = entry.getValue(); - return new Object[]{user.getUserId(), user.getRole(), - user.getFirstName(), user.getLastName(), - user.getEmail(), user.getPasswordHash()};}) + Map output = new HashMap<>(this.content); + for (int i : contentLock.keySet()) { + output.remove(i); + } + this.updateContentLock(); + return output.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(entry -> { + User user = entry.getValue(); + return new Object[] { user.getUserId(), user.getRole(), + user.getFirstName(), user.getLastName(), + user.getEmail(), user.getPasswordHash() }; + }) .toList(); + } public HashMap getUsers() { return new HashMap<>(content); } + /** * Retrieves a user by their unique identifier. + * * @param userId the unique identifier of the user to retrieve - * @return the user with the specified ID, or {@code null} if no such user exists + * @return the user with the specified ID, or {@code null} if no such user + * exists * @throws IllegalArgumentException if the userId is not positive */ public User getUserById(int userId) { @@ -72,12 +92,13 @@ public User getUserById(int userId) { /** * Generates the next user ID based on repository size. * Uses size+1 and then moves forward if that ID is already taken. + * * @return the next available user ID * @throws IllegalStateException if no available user ID can be found */ - public int getNextUserId() { + public int getNextUserId() { if (content.isEmpty()) { - return 1; + return 1; } int maxKey = content.keySet().stream().max(Integer::compareTo).orElseThrow( () -> new IllegalStateException("No keys found")); @@ -88,21 +109,21 @@ public int getNextUserId() { /** * Adds a new user to the repository *

- * The user is stored using its {@code userId} as the key. - * If a user with the same ID already exists, the user - * will not be added. + * The user is stored using its {@code userId} as the key. + * If a user with the same ID already exists, the user + * will not be added. *

* * @param user the user to add * @return {@code true} if the user was successfully added, and - * {@code false} if a user with the same ID already exists + * {@code false} if a user with the same ID already exists */ @Override public boolean addContent(User user) { if (user == null) { throw new IllegalArgumentException("User cannot be null"); } - if (content.containsKey(user.getUserId())){ + if (content.containsKey(user.getUserId())) { return false; } this.content.put(user.getUserId(), user); @@ -111,8 +132,10 @@ public boolean addContent(User user) { /** * Finds a user by their email address. + * * @param email the email address of the user to find - * @return the user with the specified email address, or {@code null} if no such user exists + * @return the user with the specified email address, or {@code null} if no such + * user exists */ public User findUserByEmail(String email) { if (email == null || email.trim().isEmpty()) { @@ -122,5 +145,5 @@ public User findUserByEmail(String email) { .filter(user -> user.getEmail().equals(email)) .findFirst() .orElse(null); - } + } } 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 5df32ee..628c785 100644 --- a/src/main/java/edu/group5/app/model/user/UserService.java +++ b/src/main/java/edu/group5/app/model/user/UserService.java @@ -21,7 +21,7 @@ public UserService(UserRepository userRepository) { } /** - * Getter for the UserRepository used by this service. + * 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 */ @@ -65,15 +65,30 @@ 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 - * @return true if the login is successful - * (i.e., the user exists and the password is correct), false otherwise + * @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 */ - public boolean login(String email, char[] password) { + public User login(String email, char[] password) { if (email == null || email.trim().isEmpty() || password == null || password.length == 0) { - return false; + return null; } User user = this.userRepository.findUserByEmail(email); - return user != null && user.verifyPassword(password); + if (user != null && user.verifyPassword(password)) { + return user; + } + return null; + } + + /** + * 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 + */ + public User getUserByEmail(String email) { + if (email == null || email.trim().isEmpty()) { + return null; + } + return this.userRepository.findUserByEmail(email); } } diff --git a/src/main/java/edu/group5/app/view/Header.java b/src/main/java/edu/group5/app/view/Header.java new file mode 100644 index 0000000..1edc5e4 --- /dev/null +++ b/src/main/java/edu/group5/app/view/Header.java @@ -0,0 +1,78 @@ +package edu.group5.app.view; + +import edu.group5.app.control.HeaderController; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; + +public class Header extends BorderPane { + private final HeaderController controller; + + public Header(HeaderController controller) { + this.controller = controller; + getStylesheets().add(getClass().getResource("/header/header.css").toExternalForm()); + setId("header"); + + setLeft(getLogoSection()); + setCenter(getNavBar()); + setRight(getProfileSection()); + } + + private StackPane getLogoSection() { + StackPane logoSection = new StackPane(); + logoSection.setId("logo-section"); + logoSection.setAlignment(Pos.CENTER); + logoSection.setOnMouseClicked(e -> controller.handleHomeBtn()); + logoSection.setStyle("-fx-cursor: hand;"); + + ImageView logo = new ImageView( + new Image(getClass().getResource("/header/images/hmh-logo.png").toExternalForm()) + ); + logo.setFitHeight(60); + logo.setPreserveRatio(true); + + logoSection.getChildren().add(logo); + return logoSection; + } + + private HBox getNavBar() { + HBox navbar = new HBox(); + navbar.setId("navbar"); + navbar.setAlignment(Pos.CENTER); + navbar.setSpacing(10); + + Button home = new Button("Home"); + home.setOnAction(e -> controller.handleHomeBtn()); + home.setStyle("-fx-cursor: hand;"); + + Button causes = new Button("Causes"); + causes.setOnAction(e -> controller.handleCausesBtn()); + causes.setStyle("-fx-cursor: hand;"); + + Button about = new Button("About us"); + about.setOnAction(e -> controller.handleAboutBtn()); + about.setStyle("-fx-cursor: hand;"); + + navbar.getChildren().addAll(home, causes, about); + return navbar; + } + + private StackPane getProfileSection() { + StackPane profileSection = new StackPane(); + profileSection.setId("profile-section"); + profileSection.setAlignment(Pos.CENTER); + profileSection.setOnMouseClicked(e -> controller.handleProfileBtn()); + profileSection.setStyle("-fx-cursor: hand;"); + + ImageView avatar = new ImageView( + new Image(getClass().getResource("/header/images/avatar.png").toExternalForm()) + ); + avatar.setFitHeight(60); + avatar.setPreserveRatio(true); + + profileSection.getChildren().add(avatar); + return profileSection; + } +} diff --git a/src/main/java/edu/group5/app/view/MainView.java b/src/main/java/edu/group5/app/view/MainView.java new file mode 100644 index 0000000..e35935f --- /dev/null +++ b/src/main/java/edu/group5/app/view/MainView.java @@ -0,0 +1,86 @@ +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/View.java b/src/main/java/edu/group5/app/view/View.java deleted file mode 100644 index 2c36e4f..0000000 --- a/src/main/java/edu/group5/app/view/View.java +++ /dev/null @@ -1,5 +0,0 @@ -package edu.group5.app.view; - -public class View { - -} diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java new file mode 100644 index 0000000..ad4be8a --- /dev/null +++ b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java @@ -0,0 +1,79 @@ +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 new file mode 100644 index 0000000..a2d92c8 --- /dev/null +++ b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java @@ -0,0 +1,145 @@ +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/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java new file mode 100644 index 0000000..6c5e5e6 --- /dev/null +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -0,0 +1,160 @@ +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 javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.TilePane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextAlignment; +import javafx.scene.Node; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DonationPageView extends BorderPane { + private final DonationPageController controller; + private final MainController mainController; + private final List allDonationElements = new ArrayList<>(); + private final Map elementAmounts = new HashMap<>(); + + public DonationPageView(DonationPageController donationPageController, HeaderController headerController, MainController mainController) { + this.controller = donationPageController; + this.mainController = mainController; + getStylesheets().add(getClass().getResource("/donationpage/donation.css").toExternalForm()); + + Header headerView = new Header(headerController); + setTop(headerView); + + VBox content = new VBox(); + content.getChildren().addAll(createDonationGrid(), createDonateSection()); + setCenter(content); + + } + private TilePane createDonationGrid(){ + TilePane body = new TilePane(); + body.setAlignment(Pos.CENTER); + body.setPrefColumns(3); + body.setPrefRows(2); + body.setPrefTileWidth(300); + body.setPrefTileHeight(150); + body.setHgap(20); + body.setVgap(20); + body.setPadding(new Insets(40, 40, 20, 40)); + + String[][] labels = { + {"Tiny", "50 kr"}, + {"Small", "100 kr"}, + {"Medium", "250 kr"}, + {"Big", "500 kr"}, + {"Grandiose", "1000 kr"}, + }; + for (String[] label : labels) { + body.getChildren().add(createDonationButton(label[0], label[1])); + } + body.getChildren().add(createCustomButton()); + + return body; + } + + public Button createDonationButton(String title, String amount) { + Button button = new Button(title + "\n" + amount); + button.setWrapText(true); + button.setTextAlignment(TextAlignment.CENTER); + button.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + button.getStyleClass().add("donation-button"); + + BigDecimal parsedAmount = parseAmount(amount); + elementAmounts.put(button, parsedAmount); + + button.setOnAction(e -> { + selectDonationElement(button); + }); + allDonationElements.add(button); + return button; + } + private VBox createCustomButton() { + Text titleText = new Text("Custom Donation"); + titleText.getStyleClass().add("donation-title"); + + HBox amountRow = new HBox(4); + amountRow.setAlignment(Pos.CENTER); + + Text krText = new Text("kr"); + krText.getStyleClass().add("donation-amount"); + + TextField amountField = new TextField(); + amountField.getStyleClass().add("donation-input"); + + amountRow.getChildren().addAll(amountField, krText); + + VBox box = new VBox(6, titleText, amountRow); + 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()); + } + + }); + + allDonationElements.add(box); + return box; + } + private HBox createDonateSection() { + Button donateBtn = new Button("Donate"); + donateBtn.getStyleClass().add("donate-button"); + donateBtn.setOnAction(e -> controller.handleDonationBtn()); + + HBox section = new HBox(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"); + } + + element.getStyleClass().add("donation-button-selected"); + + // Extract and store the amount + extractAndStoreAmount(element); + } + + 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 BigDecimal parseAmount(String amountStr) { + try { + return new BigDecimal(amountStr.replace("kr", "").trim()); + } catch (NumberFormatException e) { + return BigDecimal.ZERO; + } + } + +} \ 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 new file mode 100644 index 0000000..d563fef --- /dev/null +++ b/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java @@ -0,0 +1,48 @@ +package edu.group5.app.view.donationpage; + +import edu.group5.app.control.donationpage.PaymentCompleteController; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; + +import java.awt.*; +import java.util.Objects; + +public class PaymentCompletePageView extends BorderPane { + private final PaymentCompleteController controller; + + public PaymentCompletePageView(PaymentCompleteController paymentCompleteController) { + this.controller = paymentCompleteController; + getStylesheets().add(getClass().getResource("/donationpage/paymentcomplete.css").toExternalForm()); + + VBox content = new VBox(20); + content.setAlignment(Pos.CENTER); + content.setPadding(new Insets(40)); + content.getChildren().addAll(getImageSection(), getHomeBtn()); + setCenter(content); + } + public VBox getImageSection() { + Image image = new Image(Objects.requireNonNull(getClass().getResourceAsStream("/donationpage/Payment Complete.png"))); + ImageView imageView = new ImageView(image); + imageView.setPreserveRatio(true); + + imageView.fitWidthProperty().bind(widthProperty()); + imageView.setFitHeight(500); + + VBox imageSection = new VBox(imageView); + imageSection.setAlignment(Pos.CENTER); + return imageSection; + } + + public Button getHomeBtn() { + Button home = new Button("Home"); + home.setOnAction(e -> controller.handleHomeBtn()); + 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 new file mode 100644 index 0000000..5510daa --- /dev/null +++ b/src/main/java/edu/group5/app/view/homepage/HomePageView.java @@ -0,0 +1,67 @@ +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 javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +public class HomePageView extends BorderPane { + private final HomePageController controller; + + 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()); + } + + private ScrollPane createBody() { + ScrollPane body = new ScrollPane(); + body.setFitToWidth(true); + VBox vBox = new VBox(); + vBox.getChildren().addAll( + createIntroductionSection(), + createCharityImageSection() + ); + body.setContent(vBox); + return body; + } + + private VBox createIntroductionSection() { + VBox introductionSection = new VBox(); + introductionSection.setId("introduction-section"); + introductionSection.setAlignment(Pos.CENTER); + introductionSection.setSpacing(10); + + Text h1 = new Text("MAKE A DIFFERENCE TODAY"); + h1.setId("h1"); + Text h2 = new Text("SUPPORT THOSE IN NEED AROUND THE WORLD"); + h2.setId("h2"); + + Button donateToACauseBtn = new Button("Donate to a cause"); + donateToACauseBtn.setOnAction(e -> controller.handleDonateToACauseBtn()); + + Button aboutUsBtn = new Button("About us"); + aboutUsBtn.setOnAction(e -> controller.handleAboutUsBtn()); + + introductionSection.getChildren().addAll(h1, h2, donateToACauseBtn, aboutUsBtn); + return introductionSection; + } + + private StackPane createCharityImageSection() { + StackPane charityImageSection = new StackPane(); + charityImageSection.setId("charity-image-section"); + charityImageSection.setPrefHeight(300); + StackPane image = new StackPane(); + image.setId("charity-image"); + charityImageSection.getChildren().add(image); + return charityImageSection; + } +} diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java new file mode 100644 index 0000000..decd5a1 --- /dev/null +++ b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java @@ -0,0 +1,33 @@ +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; + +public class LoginHeader extends BorderPane { + + public LoginHeader() { + getStylesheets().add(getClass().getResource("/header/header.css").toExternalForm()); + setId("header"); + + setCenter(getLogoSection()); + } + 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()) + ); + logo.setFitHeight(60); + logo.setPreserveRatio(true); + + logoSection.getChildren().add(logo); + return logoSection; + } +} diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java new file mode 100644 index 0000000..96d83d7 --- /dev/null +++ b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java @@ -0,0 +1,114 @@ +package edu.group5.app.view.loginpage; + + +import edu.group5.app.control.HeaderController; +import edu.group5.app.control.LoginPageController; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.*; + +import java.util.Objects; + +public class LoginPageView extends BorderPane { + private final LoginPageController controller; + 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); + + HBox content = new HBox(); + content.setFillHeight(true); + HBox.setHgrow(content, Priority.ALWAYS); + + content.getChildren().addAll(getOuterSection(), getImageSection()); + + String css = Objects.requireNonNull( + getClass().getResource("/loginpage/login.css")).toExternalForm(); + content.getStylesheets().add(css); + + setCenter(content); + } + + public String getEmail() { + return emailField.getText(); + } + + public char[] getPassword() { + return passwordField.getText().toCharArray(); + } + + public void showError(String message) { + errorLabel.setText(message); + errorLabel.setStyle("-fx-text-fill: red;"); + } + + private VBox getOuterSection() { + VBox outerSection = new VBox(12); + outerSection.setAlignment(Pos.CENTER); + HBox.setHgrow(outerSection, Priority.ALWAYS); + outerSection.getChildren().addAll(getLoginBox(), getRegisterBtn()); + return outerSection; + } + private VBox getLoginBox() { + VBox loginSection = new VBox(12); + loginSection.setAlignment(Pos.CENTER); + loginSection.setId("login-box"); + loginSection.getChildren().addAll(getErrorLabel(), getEmailBox(), getPasswordBox(), getLoginBtn()); + return loginSection; + } + + private Label getErrorLabel() { + errorLabel = new Label(); + errorLabel.setPrefHeight(20); + return errorLabel; + } + + private VBox getEmailBox() { + VBox emailBox = new VBox(); + emailBox.setMaxWidth(300); + emailField = new TextField(); + emailField.setPromptText("aurafarmer@gmail.com"); + emailField.setMaxWidth(300); + emailBox.getChildren().addAll(new Label("Email"), emailField); + return emailBox; + } + private VBox getPasswordBox() { + VBox passwordBox = new VBox(); + passwordBox.setMaxWidth(300); + passwordField = new PasswordField(); + passwordField.setMaxWidth(300); + 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()); + return loginBtn; + } + public Button getRegisterBtn() { + Button registerBtn = new Button("Don't have an account? Sign In"); + registerBtn.setMaxWidth(300); + registerBtn.setOnMouseClicked(e -> controller.handleRegisterBtn()); + registerBtn.setId("register-btn"); + return registerBtn; + } + private StackPane getImageSection() { + StackPane imageSection = new StackPane(); + imageSection.setId("image-section"); + HBox.setHgrow(imageSection, Priority.ALWAYS); + return imageSection; + } + + +} diff --git a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java new file mode 100644 index 0000000..6ee0e9b --- /dev/null +++ b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java @@ -0,0 +1,143 @@ +package edu.group5.app.view.loginpage; + +import edu.group5.app.control.HeaderController; +import edu.group5.app.control.SignInPageController; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.*; + +import java.util.Objects; + + +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()); + + HBox content = new HBox(); + content.setFillHeight(true); + HBox.setHgrow(content, Priority.ALWAYS); + + content.getChildren().addAll(getOuterSection(), getImageSection()); + + String css = Objects.requireNonNull( + getClass().getResource("/loginpage/signin.css")).toExternalForm(); + content.getStylesheets().add(css); + + setCenter(content); + + } + + + public String getFirstName() { + return nameField.getText(); + } + + public String getLastName() { + return surnameField.getText(); + } + + public String getEmail() { + return emailField.getText(); + } + + public char[] getPassword() { + return passwordField.getText().toCharArray(); + } + + public void showError(String message) { + errorLabel.setText(message); + errorLabel.setStyle("-fx-text-fill: red;"); + } + + private VBox getOuterSection() { + VBox outerSection = new VBox(12); + outerSection.setAlignment(Pos.CENTER); + HBox.setHgrow(outerSection, Priority.ALWAYS); + outerSection.getChildren().addAll(getSignInBox(), 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 Label getErrorLabel() { + errorLabel = new Label(); + errorLabel.setPrefHeight(20); + return errorLabel; + } + + private HBox getNameRow() { + HBox nameRow = new HBox(12); + nameRow.setMaxWidth(300); + + VBox nameBox = new VBox(); + nameField = new TextField(); + nameField.setPromptText("Jinwoo"); + HBox.setHgrow(nameBox, Priority.ALWAYS); + nameBox.getChildren().addAll(new Label("First name"), nameField); + + VBox surnameBox = new VBox(); + surnameField = new TextField(); + surnameField.setPromptText("Son"); + HBox.setHgrow(surnameBox, Priority.ALWAYS); + surnameBox.getChildren().addAll(new Label("Last Name"), surnameField); + + nameRow.getChildren().addAll(nameBox, surnameBox); + return nameRow; + + } + private VBox getEmailBox() { + VBox emailBox = new VBox(); + emailBox.setMaxWidth(300); + emailField = new TextField(); + emailField.setPromptText("aurafarmer@gmail.com"); + emailField.setMaxWidth(300); + emailBox.getChildren().addAll(new Label("Email"), emailField); + return emailBox; + } + private VBox getPasswordBox() { + VBox passwordBox = new VBox(); + passwordBox.setMaxWidth(300); + passwordField = new PasswordField(); + passwordField.setMaxWidth(300); + 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; + } + public Button getBackToLoginBtn() { + Button backBtn = new Button("Already have an account? Log in"); + backBtn.setMaxWidth(300); + backBtn.setOnMouseClicked(e -> controller.handleLoginBtn()); + backBtn.setId("register-btn"); + return backBtn; + } + private StackPane getImageSection() { + StackPane imageSection = new StackPane(); + imageSection.setId("image-section"); + HBox.setHgrow(imageSection, Priority.ALWAYS); + return imageSection; + } + +} diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java new file mode 100644 index 0000000..4ec7c91 --- /dev/null +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -0,0 +1,103 @@ +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.model.organization.Organization; +import edu.group5.app.view.Header; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +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 ScrollPane createBody() { + ScrollPane body = new ScrollPane(); + body.setFitToWidth(true); + body.setFitToHeight(true) + ; + VBox vBox = new VBox(); + vBox.setId("main-container"); + + vBox.getChildren().addAll( + createOrgSection() + ); + body.setContent(vBox); + return body; + } + + private HBox createOrgSection() { + HBox orgSection = new HBox(); + orgSection.setId("org-section"); + orgSection.setAlignment(Pos.CENTER); + orgSection.setSpacing(40); + + orgSection.getChildren().addAll(createImageContainer(), createOrgInfoSection()); + return orgSection; + } + + private StackPane createImageContainer() { + StackPane imageContainer = new StackPane(); + 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"; + + 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(); + + VBox orgInfoSection = new VBox(); + orgInfoSection.setSpacing(50); + + VBox orgNameAndDescription = new VBox(); + + 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); + + Button donateBtn = new Button("Donate"); + donateBtn.setId("donate-button"); + donateBtn.setOnAction(e -> controller.handleDonateClick()); + + 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 new file mode 100644 index 0000000..9ab73ee --- /dev/null +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -0,0 +1,136 @@ +package edu.group5.app.view.userpage; + +import edu.group5.app.control.HeaderController; +import edu.group5.app.control.MainController; +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.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +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; + + +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(); + + 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()); + setCenter(content); + } + + private HBox createProfileSection() { + ImageView avatar = new ImageView(new Image(getClass().getResourceAsStream("/userpage/account_circle.png"))); + avatar.setFitWidth(150); + avatar.setFitHeight(150); + avatar.setPreserveRatio(true); + avatar.setId("avatar"); + + Text name = new Text(currentUser.getFirstName() + " " + currentUser.getLastName()); + name.setId("profile-name"); + + Label email = new Label(currentUser.getEmail()); + email.getStyleClass().add("profile-info"); + + Label location = new Label("Trondheim, Norway"); + location.getStyleClass().add("profile-info"); + + Button logoutBtn = new Button("Logout"); + logoutBtn.getStyleClass().add("logout-button"); + logoutBtn.setOnAction(e -> mainController.logout()); + + VBox info = new VBox(10, name, email, location, logoutBtn); + info.setAlignment(Pos.CENTER_LEFT); + + HBox profile = new HBox(40, avatar, info); + 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)); + + HashMap userDonations = mainController.getDonationService() + .getDonationRepository().filterByUser(currentUser.getUserId()); + + Set uniqueOrgs = new HashSet<>(); + for (Donation donation : userDonations.values()) { + uniqueOrgs.add(donation.organizationId()); + } + + if (uniqueOrgs.isEmpty()) { + Label noCauses = new Label("No causes supported yet"); + noCauses.setStyle("-fx-text-fill: #999;"); + causesBox.getChildren().add(noCauses); + } else { + for (int orgId : uniqueOrgs) { + Organization org = mainController.getOrganizationService().findByOrgNumber(orgId); + if (org != null) { + Label causeLabel = new Label("• " + org.name()); + causesBox.getChildren().add(causeLabel); + } + } + } + + return new VBox(10, title, causesBox); + } + + private VBox createDonationsSection() { + Text title = new Text("PREVIOUS DONATIONS"); + title.getStyleClass().add("section-title"); + + VBox donationsBox = new VBox(10); + donationsBox.getStyleClass().add("section-box"); + donationsBox.setPadding(new Insets(10)); + + HashMap userDonations = mainController.getDonationService() + .getDonationRepository().filterByUser(currentUser.getUserId()); + + if (userDonations.isEmpty()) { + Label noDonations = new Label("No donations yet"); + noDonations.setStyle("-fx-text-fill: #999;"); + donationsBox.getChildren().add(noDonations); + } else { + for (Donation donation : userDonations.values()) { + Organization org = mainController.getOrganizationService() + .findByOrgNumber(donation.organizationId()); + String orgName = (org != null) ? org.name() : "Unknown Organization"; + + Label donationLabel = new Label( + orgName + " • " + donation.amount() + " kr" + " • " + donation.date() + ); + donationsBox.getChildren().add(donationLabel); + } + } + + return new VBox(10, title, donationsBox); + } + +} diff --git a/src/main/resources/browsepage/browse_org.css b/src/main/resources/browsepage/browse_org.css new file mode 100644 index 0000000..9a50c71 --- /dev/null +++ b/src/main/resources/browsepage/browse_org.css @@ -0,0 +1,25 @@ +#mainContainer { + -fx-border-color: black; + -fx-border-width: 1px; + -fx-border-radius: 1em; + -fx-padding: 5px; + -fx-background-color: white; + -fx-background-radius: 1em; +} + +#mainContainer:hover { + -fx-cursor: hand; +} + +#imageContainer {} + + +#logo {} + +#orgName { + -fx-font-size: x-large; + -fx-font-weight: bold; + -fx-text-alignment: center; +} + +#checkMarkContainer {} \ No newline at end of file diff --git a/src/main/resources/browsepage/browsepage.css b/src/main/resources/browsepage/browsepage.css new file mode 100644 index 0000000..1dd8643 --- /dev/null +++ b/src/main/resources/browsepage/browsepage.css @@ -0,0 +1,6 @@ +#body { + -fx-padding: 20px; +} + +#card-grid { +} \ No newline at end of file diff --git a/src/main/resources/browsepage/images/children_of_shambala.png b/src/main/resources/browsepage/images/children_of_shambala.png new file mode 100644 index 0000000..3b1a7f7 Binary files /dev/null and b/src/main/resources/browsepage/images/children_of_shambala.png differ diff --git a/src/main/resources/browsepage/images/kfum_kfum_global.png b/src/main/resources/browsepage/images/kfum_kfum_global.png new file mode 100644 index 0000000..bd5365a Binary files /dev/null and b/src/main/resources/browsepage/images/kfum_kfum_global.png differ diff --git a/src/main/resources/donationpage/Payment Complete.png b/src/main/resources/donationpage/Payment Complete.png new file mode 100644 index 0000000..e2b3d6a Binary files /dev/null and b/src/main/resources/donationpage/Payment Complete.png differ diff --git a/src/main/resources/donationpage/donation.css b/src/main/resources/donationpage/donation.css new file mode 100644 index 0000000..32433df --- /dev/null +++ b/src/main/resources/donationpage/donation.css @@ -0,0 +1,53 @@ +.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; +} + +.donation-button:hover { + -fx-border-color: #f0f0f0; +} + +.donation-button-selected { + -fx-background-color: #111; + -fx-text-fill: white; + -fx-border-color: #111; +} +.donation-title { + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-fill: #111; +} +.donation-amount { + -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; +} +.donation-input:focused { + -fx-border-color: transparent transparent #4a90d9 transparent; +} +.donate-button { + -fx-pref-height: 55px; + -fx-background-color: #e03030; + -fx-text-fill: white; + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-padding: 0 40 0 40; +} +.donate-button:hover { + -fx-background-color: #c02020; +} + diff --git a/src/main/resources/donationpage/paymentcomplete.css b/src/main/resources/donationpage/paymentcomplete.css new file mode 100644 index 0000000..0e1e786 --- /dev/null +++ b/src/main/resources/donationpage/paymentcomplete.css @@ -0,0 +1,14 @@ +#home-button { + -fx-background-color: #4caf50; + -fx-text-fill: white; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-pref-width: 200px; + -fx-pref-height: 50px; + -fx-background-radius: 8; + -fx-cursor: hand; +} + +#home-button:hover { + -fx-background-color: #388e3c; +} \ No newline at end of file diff --git a/src/main/resources/header/header.css b/src/main/resources/header/header.css new file mode 100644 index 0000000..28fcc59 --- /dev/null +++ b/src/main/resources/header/header.css @@ -0,0 +1,19 @@ +#header { + -fx-background-color: #A6CDF2; + -fx-padding: 10px; +} + +#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/header/images/avatar.png b/src/main/resources/header/images/avatar.png new file mode 100644 index 0000000..e46b74d Binary files /dev/null and b/src/main/resources/header/images/avatar.png differ diff --git a/src/main/resources/header/images/hmh-logo.png b/src/main/resources/header/images/hmh-logo.png new file mode 100644 index 0000000..78be8d9 Binary files /dev/null and b/src/main/resources/header/images/hmh-logo.png differ diff --git a/src/main/resources/homepage/homepage.css b/src/main/resources/homepage/homepage.css new file mode 100644 index 0000000..90b090c --- /dev/null +++ b/src/main/resources/homepage/homepage.css @@ -0,0 +1,27 @@ +#introduction-section { + -fx-border-color: black; + -fx-border-width: 2px; + -fx-padding: 20px 0; +} + +#h1 { + -fx-text-fill: black; + -fx-font-size: 50pt; + -fx-font-weight: bold; +} + +#h2 { + -fx-fill: #757575; + -fx-font-size: 25pt; +} + +#charity-image-section { + -fx-border-color: black; +} + +#charity-image { + -fx-background-image: url("/homepage/images/charityimage.jpg"); + -fx-background-position: center 55%; + -fx-background-size: 100% auto; + -fx-background-repeat: no-repeat; +} \ No newline at end of file diff --git a/src/main/resources/homepage/images/charityimage.jpg b/src/main/resources/homepage/images/charityimage.jpg new file mode 100644 index 0000000..9072612 Binary files /dev/null and b/src/main/resources/homepage/images/charityimage.jpg differ diff --git a/src/main/resources/init.sql b/src/main/resources/init.sql new file mode 100644 index 0000000..8b99314 --- /dev/null +++ b/src/main/resources/init.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS +users ( + user_id INT PRIMARY KEY, + role VARCHAR(32) NOT NULL, + first_name VARCHAR(32) NOT NULL, + last_name VARCHAR(32) NOT NULL, + email VARCHAR(32) NOT NULL, + password_hash VARCHAR(72) NOT NULL +); + +CREATE TABLE IF NOT EXISTS +donations ( + donation_id INT PRIMARY KEY, + user_id INT NOT NULL, + organization_id INT NOT NULL, + amount DECIMAL(32, 16) NOT NULL, + dating TIMESTAMP NOT NULL, + payment_method VARCHAR(32) NOT NULL, + CONSTRAINT fk_user + FOREIGN KEY (user_id) + REFERENCES users(user_id) +); + diff --git a/src/main/resources/loginpage/login-image.jpg b/src/main/resources/loginpage/login-image.jpg new file mode 100644 index 0000000..5b4b3ed Binary files /dev/null and b/src/main/resources/loginpage/login-image.jpg differ diff --git a/src/main/resources/loginpage/login.css b/src/main/resources/loginpage/login.css new file mode 100644 index 0000000..dc758a1 --- /dev/null +++ b/src/main/resources/loginpage/login.css @@ -0,0 +1,25 @@ +#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%; +} +#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/signin-image.png new file mode 100644 index 0000000..7cb78cd Binary files /dev/null and b/src/main/resources/loginpage/signin-image.png differ diff --git a/src/main/resources/loginpage/signin.css b/src/main/resources/loginpage/signin.css new file mode 100644 index 0000000..4ab0276 --- /dev/null +++ b/src/main/resources/loginpage/signin.css @@ -0,0 +1,25 @@ +#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/organizationpage/organizationpage.css b/src/main/resources/organizationpage/organizationpage.css new file mode 100644 index 0000000..a7276b5 --- /dev/null +++ b/src/main/resources/organizationpage/organizationpage.css @@ -0,0 +1,31 @@ +#main-container { + -fx-padding: 50px +} + +#logo { + -fx-min-height: 50%; +} + +#orgName { + -fx-font-weight: bold; + -fx-font-size: 20pt; +} + +#description { + -fx-font-size: 10pt; +} + +#donate-button { + -fx-pref-height: 55px; + -fx-background-color: #e03030; + -fx-text-fill: white; + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-padding: 0 40 0 40; +} + +#donate-button:hover { + -fx-background-color: #c02020; +} \ No newline at end of file diff --git a/src/main/resources/test_init.sql b/src/main/resources/test_init.sql new file mode 100644 index 0000000..00670e8 --- /dev/null +++ b/src/main/resources/test_init.sql @@ -0,0 +1,24 @@ +DROP TABLE IF EXISTS users, donations; + +CREATE TABLE IF NOT EXISTS +users ( + user_id INT PRIMARY KEY, + role VARCHAR(32) NOT NULL, + first_name VARCHAR(32) NOT NULL, + last_name VARCHAR(32) NOT NULL, + email VARCHAR(32) NOT NULL, + password_hash VARCHAR(72) NOT NULL +); + +CREATE TABLE IF NOT EXISTS +donations ( + donation_id INT PRIMARY KEY, + user_id INT NOT NULL, + organization_id INT NOT NULL, + amount DECIMAL(32, 16) NOT NULL, + dating TIMESTAMP NOT NULL, + payment_method VARCHAR(32) NOT NULL, + CONSTRAINT fk_user + FOREIGN KEY (user_id) + REFERENCES users(user_id) +); diff --git a/src/main/resources/userpage/account_circle.png b/src/main/resources/userpage/account_circle.png new file mode 100644 index 0000000..f53f825 Binary files /dev/null and b/src/main/resources/userpage/account_circle.png differ diff --git a/src/main/resources/userpage/userpage.css b/src/main/resources/userpage/userpage.css new file mode 100644 index 0000000..8401a77 --- /dev/null +++ b/src/main/resources/userpage/userpage.css @@ -0,0 +1,31 @@ +#profile-name { + -fx-font-size: 28px; + -fx-font-weight: bold; +} +.profile-info { + -fx-font-size: 16px; + -fx-text-fill: #444; +} +.section-title { + -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; +} +.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; +} +.logout-button:hover { + -fx-background-color: #c02020; +} \ No newline at end of file diff --git a/src/main/resources/verified_check.png b/src/main/resources/verified_check.png new file mode 100644 index 0000000..520f237 Binary files /dev/null and b/src/main/resources/verified_check.png differ diff --git a/src/test/java/edu/group5/app/control/WrapperTest.java b/src/test/java/edu/group5/app/control/WrapperTest.java deleted file mode 100644 index d626fd7..0000000 --- a/src/test/java/edu/group5/app/control/WrapperTest.java +++ /dev/null @@ -1,5 +0,0 @@ -package edu.group5.app.control; - -public class WrapperTest { - -} diff --git a/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java b/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java new file mode 100644 index 0000000..8c401c4 --- /dev/null +++ b/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java @@ -0,0 +1,207 @@ +package edu.group5.app.control.wrapper; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.math.RoundingMode; +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; + +public class DbWrapperDonationsTest { + private Object[] johnDonation; + private List users; + private Object[] janeDonation; + private Object[] cutoffDonation; + private Object[] freakyDonation; + private Object[] repeatingDonation; + private List donations; + private List donations2; + private List donations3; + private List repeatedDonations; + private List wrongFormatDonations; + private List wrongDatatypeDonations; + private List nullList; + + private static final int PRECISION = 5; + + private DbWrapper db; + + @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" }; + this.users = new ArrayList(); + for (int i = 0; i < 5; i++) { + Object[] row = new Object[6]; + row[0] = i + 1; + row[1] = "Customer"; + row[2] = firstNames[i]; + row[3] = lastNames[i]; + row[4] = firstNames[i] + lastNames[i] + "@email.com"; + row[5] = "password"; + users.add(row); + } + + 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.donations2 = new ArrayList(); + this.donations2.add(this.johnDonation); + this.donations2.add(this.janeDonation); + + this.donations3 = new ArrayList(); + this.donations3.add(this.janeDonation); + + this.repeatingDonation = new Object[] { 3, 3, 333, new BigDecimal(18181818.18), + new Timestamp(new Date().getTime()), "Klarna installations payment" }; + this.repeatedDonations = new ArrayList(); + this.repeatedDonations.add(this.repeatingDonation); + this.repeatedDonations.add(this.repeatingDonation); + + this.cutoffDonation = new Object[] { 4, 4, 21 }; + this.wrongFormatDonations = new ArrayList(); + this.wrongFormatDonations.add(cutoffDonation); + + this.freakyDonation = new Object[] { 5, 5, "Freaks4Education", "lots", false, new String[6] }; + this.wrongDatatypeDonations = new ArrayList(); + this.wrongDatatypeDonations.add(freakyDonation); + + 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); + } + + private static boolean donationEquals(Object[] array1, Object[] array2) { + Object[] tempArray1 = array1.clone(); + tempArray1[3] = ((BigDecimal) tempArray1[3]).setScale(2, RoundingMode.HALF_UP); + Object[] tempArray2 = array2.clone(); + tempArray2[3] = ((BigDecimal) tempArray2[3]).setScale(2, RoundingMode.HALF_UP); + return Arrays.equals(tempArray1, tempArray2); + } + + @Test + public void importDonationsIsOnlyExportDonationsTest() { + assertDoesNotThrow(() -> { + 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(this.db.disconnect()); + }); + } + + @Test + public void nullDataInExportDonationsThrowsExpectedException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(null); + }); + assertTrue(this.db.disconnect()); + assertEquals("data can't be null", exception.getMessage()); + } + + @Test + public void wronglyFormattedDonationsThrowsExpectedException() { + assertTrue(this.db.importDonations((int) this.users.get(2)[0]).size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(this.wrongFormatDonations); + }); + assertTrue(this.db.importDonations((int) this.users.get(2)[0]).size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("data's arrays must have a length of 6", exception.getMessage()); + } + + @Test + public void wronglyDatatypedDonationsThrowsExpectedException() { + assertTrue(this.db.importDonations((int) this.users.get(3)[0]).size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(this.wrongDatatypeDonations); + }); + assertTrue(this.db.importDonations((int) this.users.get(3)[0]).size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("One or more rows in data contains a wrong datatype", exception.getMessage()); + } + + @Test + public void addingSameDonationTwiceThrowsExpectedException() { + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); + assertEquals(1, this.db.exportDonations(this.donations)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(this.donations); + }); + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 1); + assertTrue(this.db.disconnect()); + assertEquals("data can't contain existing rows", exception.getMessage()); + } + + @Test + 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); + }); + 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()); + assertEquals("data can't contain existing rows", exception.getMessage()); + } + + @Test + public void addingDifferentDonationsThrowsNoException() { + assertDoesNotThrow(() -> { + 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(1, this.db.exportDonations(this.donations)); + assertEquals(1, this.db.exportDonations(this.donations3)); + 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()); + }); + } + + @Test + public void addingDonationListWithDuplicateIdsThrowsExpectedException() { + assertTrue(this.db.importDonations((int) this.users.get(4)[0]).size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(this.repeatedDonations); + }); + assertTrue(this.db.importDonations((int) this.users.get(4)[0]).size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("data can't contain duplicate rows", exception.getMessage()); + } + + @Test + public void addingDonationListWithNullInRowThrowsExpectedException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(this.nullList); + }); + assertTrue(this.db.disconnect()); + assertEquals("One or more rows in data contains null values", exception.getMessage()); + } + + @Test + public void dataIsEmptyAfterExportingAndImportingEmptyList() { + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); + assertEquals(0, this.db.exportDonations(new ArrayList())); + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); + assertTrue(this.db.disconnect()); + } +} diff --git a/src/test/java/edu/group5/app/control/wrapper/DbWrapperUserTest.java b/src/test/java/edu/group5/app/control/wrapper/DbWrapperUserTest.java new file mode 100644 index 0000000..10c514f --- /dev/null +++ b/src/test/java/edu/group5/app/control/wrapper/DbWrapperUserTest.java @@ -0,0 +1,191 @@ +package edu.group5.app.control.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; + +public class DbWrapperUserTest { + private Object[] johnDoe; + private Object[] janeDoe; + private Object[] cutoffJoh; + private Object[] freakyBill; + private Object[] repeatingJoeJoe; + private List users; + private List users2; + private List users3; + private List repeatedUsers; + private List wrongFormatUsers; + private List wrongDatatypeUsers; + private List nullList; + + private DbWrapper db; + + @BeforeEach + void init() { + this.db = new DbWrapper(true); + this.johnDoe = new Object[] { 1, "Customer", "John", "Doe", "johndoe@email.com", "password" }; + this.users = new ArrayList(); + this.users.add(this.johnDoe); + + this.janeDoe = new Object[] { 2, "Customer", "Jane", "Doe", "janedoe@email.com", "qwerty" }; + this.users2 = new ArrayList(); + this.users2.add(this.johnDoe); + this.users2.add(this.janeDoe); + + this.users3 = new ArrayList(); + this.users3.add(this.janeDoe); + + 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); + + this.cutoffJoh = new Object[] { 4, "Customer", "Cutoff", "Joh" }; + this.wrongFormatUsers = new ArrayList(); + this.wrongFormatUsers.add(cutoffJoh); + + this.freakyBill = new Object[] { 5, "Customer", 6.1805011125, "Bill", true, false }; + this.wrongDatatypeUsers = new ArrayList(); + this.wrongDatatypeUsers.add(freakyBill); + + Object[] nullRow = new Object[] {null, null, null, null, null, null}; + this.nullList = new ArrayList(); + this.nullList.add(nullRow); + } + + @Test + public void nonTestDbWrapperThrowsNoException() { + assertDoesNotThrow(() -> new DbWrapper(false)); + } + + @Test + public void importUsersIsOnlyExportUsersTest() { + assertDoesNotThrow(() -> { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + assertTrue(this.db.exportUsers(this.users) == 1); + assertTrue(this.db.importUsers().size() == 1); + assertTrue(Arrays.equals(this.db.importUsers().get(0), this.johnDoe)); + assertTrue(this.db.disconnect()); + }); + } + + @Test + public void nullDataInExportUsersThrowsExpectedException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(null); + }); + assertTrue(this.db.importUsers().size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("data can't be null", exception.getMessage()); + } + + @Test + public void wronglyFormattedUsersThrowsExpectedException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(this.wrongFormatUsers); + }); + assertTrue(this.db.importUsers().size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("data's arrays must have a length of 6", exception.getMessage()); + } + + @Test + public void wronglyDatatypedUsersThrowsExpectedException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(this.wrongDatatypeUsers); + }); + assertTrue(this.db.importUsers().size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("One or more rows in data contains a wrong datatype", exception.getMessage()); + } + + @Test + public void addingSameUserTwiceThrowsExpectedException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + assertEquals(1, this.db.exportUsers(this.users)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(this.users); + }); + assertTrue(this.db.importUsers().size() == 1); + assertTrue(this.db.disconnect()); + assertEquals("data can't contain existing rows", exception.getMessage()); + } + + @Test + public void addingSameUserTwiceThrowsExpectedException2() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + assertEquals(2, this.db.exportUsers(this.users2)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(this.users); + }); + assertTrue(this.db.importUsers().size() == 2); + assertTrue(this.db.disconnect()); + assertEquals("data can't contain existing rows", exception.getMessage()); + } + + @Test + public void addingDifferentUsersThrowsNoException() { + assertDoesNotThrow(() -> { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + assertEquals(1, this.db.exportUsers(this.users)); + assertEquals(1, this.db.exportUsers(this.users3)); + assertTrue(this.db.importUsers().size() == 2); + assertTrue(this.db.disconnect()); + }); + } + + @Test + public void addingUserListWithDuplicateIdsThrowsExpectedException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(this.repeatedUsers); + }); + assertTrue(this.db.importUsers().size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("data can't contain duplicate rows", exception.getMessage()); + } + + @Test + public void addingUserListWithNullInRowThrowsExpectedException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(this.nullList); + }); + assertTrue(this.db.importUsers().size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("One or more rows in data contains null values", exception.getMessage()); + } + + +} diff --git a/src/test/java/edu/group5/app/control/OrgApiWrapperTest.java b/src/test/java/edu/group5/app/control/wrapper/OrgApiWrapperTest.java similarity index 92% rename from src/test/java/edu/group5/app/control/OrgApiWrapperTest.java rename to src/test/java/edu/group5/app/control/wrapper/OrgApiWrapperTest.java index 53f08d7..23b30ec 100644 --- a/src/test/java/edu/group5/app/control/OrgApiWrapperTest.java +++ b/src/test/java/edu/group5/app/control/wrapper/OrgApiWrapperTest.java @@ -1,4 +1,4 @@ -package edu.group5.app.control; +package edu.group5.app.control.wrapper; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -30,7 +30,8 @@ void init() { } /** - * Checks if inputting null as the urlString throws the expected exception during construction. + * Checks if inputting null as the urlString throws the expected exception + * during construction. */ @Test public void nullUrlThrowsException() { @@ -40,7 +41,8 @@ public void nullUrlThrowsException() { } /** - * Checks if inputting an empty urlString throws the expected exception during construction. + * Checks if inputting an empty urlString throws the expected exception during + * construction. */ @Test public void emptyUrlThrowsException() { @@ -50,7 +52,8 @@ public void emptyUrlThrowsException() { } /** - * Checks if an invalid urlString throws the expected exception during construction. + * Checks if an invalid urlString throws the expected exception during + * construction. */ @Test public void faultyUrlThrowsException() { @@ -60,8 +63,8 @@ public void faultyUrlThrowsException() { } // /** - // * Checks if import returns False when there's no internet connection. - // */ + // * Checks if import returns False when there's no internet connection. + // */ // @Test // public void noConnectionReturnsFalseImport() { // assertDoesNotThrow(() -> { 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 bdf0110..ce81b2a 100644 --- a/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java @@ -263,4 +263,13 @@ void filterByUserIdThrowsIfNegative() { () -> repo.filterByUser(0)); assertEquals("User ID must be positive", ex.getMessage()); } + + @Test + void exportExportsOnlyNewInformation() { + repo.addContent(donation1); + repo.addContent(donation2); + assertEquals(2, repo.export().size()); + repo.addContent(donation3); + assertEquals(1, repo.export().size()); + } } \ 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 new file mode 100644 index 0000000..e34aba7 --- /dev/null +++ b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java @@ -0,0 +1,70 @@ +package edu.group5.app.model.organization; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +public class OrganizationServiceTest { + private OrganizationRepository repo; + private OrganizationService service; + private Object[] content; + + @BeforeEach + public void setUp() { + Map orgMap = new HashMap<>(); + orgMap.put("org_number", "1"); + orgMap.put("name", "Misjonsalliansen"); + orgMap.put("status", "approved"); + orgMap.put("url", "https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/"); + orgMap.put("is_pre_approved", false); + + content = new Object[]{orgMap}; + repo = new OrganizationRepository(content); + service = new OrganizationService(repo); + } + + @Test + void constructor_throwsIfNull() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new OrganizationService(null)); + assertEquals("OrganizationRepository cannot be null", ex.getMessage()); + } + + @Test + void testGetOrganizationRepository() { + assertEquals(repo, service.getOrganizationRepository()); + } + + @Test + void testGetTrustedOrganizations() { + Map trustedOrgs = service.getTrustedOrganizations(); + assertNotNull(trustedOrgs); + assertTrue(trustedOrgs.containsKey(1)); + Organization org = trustedOrgs.get(1); + assertEquals(1, org.orgNumber()); + assertEquals("Misjonsalliansen", org.name()); + assertTrue(org.trusted()); + assertEquals("https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/", org.websiteUrl()); + assertFalse(org.isPreApproved()); + } + + @Test + void testFindByOrgNumber() { + Organization org = service.findByOrgNumber(1); + assertNotNull(org); + assertEquals(1, org.orgNumber()); + assertEquals("Misjonsalliansen", org.name()); + } + + + @Test + void testFindByOrgName() { + Organization org = service.findByOrgName("Misjonsalliansen"); + assertNotNull(org); + assertEquals(1, org.orgNumber()); + assertEquals("Misjonsalliansen", org.name()); + } +} 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 0700828..a4afbc8 100644 --- a/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java @@ -12,6 +12,7 @@ public class UserRepositoryTest { private UserRepository repo; private List rows; +private User additionalUser; @BeforeEach void setUp() { @@ -19,6 +20,7 @@ void setUp() { rows.add(new Object[]{1, "Customer", "John", "Cena", "john@example.com", "hashedpass"}); rows.add(new Object[]{2, "Customer", "Jane", "Doe", "jane@example.com", "hashedpass"}); repo = new UserRepository(rows); + this.additionalUser = new Customer(3, "John", "Doe", "john@example.com", "hashedpass"); } @Test @@ -145,4 +147,11 @@ void exportContainsAllUsers() { assertEquals(2, exported.get(1)[0]); assertEquals("Customer", exported.get(0)[1]); } + + @Test + void exportExportsOnlyNewInformation() { + assertEquals(0, repo.export().size()); + repo.addContent(this.additionalUser); + assertEquals(1, repo.export().size()); + } } \ No newline at end of file diff --git a/src/test/java/edu/group5/app/model/user/UserServiceTest.java b/src/test/java/edu/group5/app/model/user/UserServiceTest.java index 46d63c5..8f56957 100644 --- a/src/test/java/edu/group5/app/model/user/UserServiceTest.java +++ b/src/test/java/edu/group5/app/model/user/UserServiceTest.java @@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -15,7 +17,7 @@ public class UserServiceTest { private UserRepository repo; - private UserService service; + private UserService service; @BeforeEach void setUp() { @@ -95,8 +97,29 @@ void loginValidPassword() { User testUser = new Customer(10, "Test", "User", "test@example.com", hashedPassword); repo.addContent(testUser); - boolean result = service.login("test@example.com", plainPassword.toCharArray()); - assertTrue(result); + User result = service.login("test@example.com", plainPassword.toCharArray()); + assertNotNull(result); + assertEquals("test@example.com", result.getEmail()); + assertTrue(result.verifyPassword(plainPassword.toCharArray())); + } + + @Test + void loginReturnsCorrectUser() { + String password = "1234"; + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + + User user = new Customer( + 99, "Alice", "Smith", "alice@test.com", + encoder.encode(password) + ); + + repo.addContent(user); + + User result = service.login("alice@test.com", password.toCharArray()); + + assertNotNull(result); + assertEquals(99, result.getUserId()); + assertEquals("Alice", result.getFirstName()); } @Test @@ -107,23 +130,23 @@ void loginInvalidPassword() { User testUser = new Customer(10, "Test", "User", "test@example.com", hashedPassword); repo.addContent(testUser); - boolean result = service.login("test@example.com", "wrongpass".toCharArray()); - boolean result2 = service.login("test@example.com", null); - boolean result3 = service.login("test@example.com", " ".toCharArray()); - boolean result4 = service.login("test@example.com", new char[0]); - assertFalse(result); - assertFalse(result2); - assertFalse(result3); - assertFalse(result4); + User result = service.login("test@example.com", "wrongpass".toCharArray()); + User result2 = service.login("test@example.com", null); + User result3 = service.login("test@example.com", " ".toCharArray()); + User result4 = service.login("test@example.com", new char[0]); + assertNull(result); + assertNull(result2); + assertNull(result3); + assertNull(result4); } @Test void loginInvalidEmail() { - boolean result = service.login("nonexist@example.com", "password".toCharArray()); - boolean result2 = service.login(null, "password".toCharArray()); - boolean result3 = service.login(" ", "password".toCharArray()); - assertFalse(result); - assertFalse(result2); - assertFalse(result3); + User result = service.login("nonexist@example.com", "password".toCharArray()); + User result2 = service.login(null, "password".toCharArray()); + User result3 = service.login(" ", "password".toCharArray()); + assertNull(result); + assertNull(result2); + assertNull(result3); } } \ No newline at end of file