diff --git a/.gitignore b/.gitignore index b272176..6015372 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,5 @@ build/ .DS_Store ### data ### -*.db +givehope.db /data/ \ No newline at end of file diff --git a/pom.xml b/pom.xml index 85afbce..0021470 100644 --- a/pom.xml +++ b/pom.xml @@ -17,8 +17,6 @@ - - org.openjfx javafx-controls @@ -31,7 +29,6 @@ ${javafx.version} - org.junit.jupiter junit-jupiter @@ -42,7 +39,7 @@ com.fasterxml.jackson.core jackson-databind - 2.17.0 + 2.18.3 @@ -51,28 +48,34 @@ 3.46.1.0 + + org.jsoup + jsoup + 1.17.2 + + - - org.openjfx javafx-maven-plugin 0.0.8 - ui.GiveHopeGIU + app.Main + + + + - org.apache.maven.plugins maven-surefire-plugin 3.5.4 - \ No newline at end of file diff --git a/src/main/java/app/GiveHopeApp.java b/src/main/java/app/GiveHopeApp.java deleted file mode 100644 index 9738139..0000000 --- a/src/main/java/app/GiveHopeApp.java +++ /dev/null @@ -1,4 +0,0 @@ -package app; - -public class GiveHopeApp { -} diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 7cc167d..b6dd8e0 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -1,63 +1,87 @@ package app; +import domain.organization.Organization; +import integration.OrganizationDetails; +import integration.InnsamlingskontrollenClient; import application.security.PasswordHasher; -import application.user.UserRegister; import domain.donation.Donation; -import domain.organization.Organization; import domain.user.User; -import integration.InnsamlingskontrollenClient; import integration.security.Sha256PasswordHasher; import java.math.BigDecimal; -import java.util.List; -import persistence.DonationDao; -import persistence.OrganizationDao; -import persistence.UserDao; -import persistence.UserRepository; +import java.sql.SQLException; + +import persistence.dao.DonationDao; +import persistence.dao.OrganizationDao; +import persistence.dao.UserDao; import persistence.db.Database; +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; -public class Main { +public class Main extends Application{ public static void main(String[] args) { - try { - Database.getConnection(); - System.out.println("Database connected"); - - UserDao userDao = new UserDao(); - OrganizationDao orgDao = new OrganizationDao(); - DonationDao donationDao = new DonationDao(); - - PasswordHasher hasher = new Sha256PasswordHasher(); - UserRepository userRepo = new UserDao(); - UserRegister userReg = new UserRegister(hasher, userRepo); - - // Test brukerregistrering - userReg.execute("testuser", "12345678", "passord123", "test@example.com"); - System.out.println("Inserted user!"); - - User user = userDao.findByEmail("test@example.com").orElseThrow(); - System.out.println("Hentet bruker: " + user.getUsername() + ", id=" + user.getID()); - - // Hent og lagre organisasjoner fra Innsamlingskontrollen - System.out.println("Henter organisasjoner fra Innsamlingskontrollen..."); - Organization[] fetchedOrgs = InnsamlingskontrollenClient.fetchOrganizations(); - for (Organization org : fetchedOrgs) { - orgDao.insert(org); + launch(args); + } + + @Override + public void start(Stage stage) throws Exception { + Database.initializeDatabase(); + System.out.println("DB path: " + new java.io.File("givehope.db").getAbsolutePath()); + OrganizationDetails details = InnsamlingskontrollenClient.fetchDetails( + "https://www.innsamlingskontrollen.no/organisasjoner/caritas-norge/" + ); + System.out.println("Description: " + details.getDescription()); + System.out.println("Logo: " + details.getLogoUrl()); + OrganizationDao orgDao = new OrganizationDao(); + Organization[] orgs = InnsamlingskontrollenClient.fetchOrganizations(); + for (Organization org : orgs) { + orgDao.insert(org); } - System.out.println("Lastet inn " + fetchedOrgs.length + " organisasjoner"); - List orgs = orgDao.getAll(); - System.out.println("Organisasjoner i DB: " + orgs.size()); - System.out.println("Første org: " + orgs.get(0).getName() + ", org_number=" + orgs.get(0).getOrgNumber()); + addTestdata(); - // Test donasjon med første org fra DB - Donation donation = new Donation(new BigDecimal("100.00"), user, orgs.get(0)); - donationDao.insert(donation); - System.out.println("Inserted donation, id=" + donation.getId()); + Parent root = FXMLLoader.load(getClass().getResource("/view/MainView.fxml")); + Scene scene = new Scene(root, 1275, 725); + + stage.setTitle("GiveHope"); + stage.setScene(scene); + stage.show(); + } - List donations = donationDao.findByUser(user.getID()); - System.out.println("Antall donasjoner for bruker: " + donations.size()); + private void addTestdata() throws SQLException { + UserDao userDao = new UserDao(); + DonationDao donationDao = new DonationDao(); + OrganizationDao orgDao = new OrganizationDao(); - } catch (Exception e) { - e.printStackTrace(); + if (userDao.existsByEmail("test@test.no")) return; + + PasswordHasher hasher = new Sha256PasswordHasher(); + String hashedPassword = hasher.hash("test"); + User user = new User("test", "12345678", hashedPassword, "test@test.no"); + userDao.insert(user); + + Organization org = orgDao.getAll().get(0); + + donationDao.insert(new Donation(new BigDecimal(100), user, org)); + donationDao.insert(new Donation(new BigDecimal(250), user, org)); + donationDao.insert(new Donation(new BigDecimal(500), user, org)); + donationDao.insert(new Donation(new BigDecimal(300), user, org)); + donationDao.insert(new Donation(new BigDecimal(1000), user, org)); + donationDao.insert(new Donation(new BigDecimal(700), user, org)); + donationDao.insert(new Donation(new BigDecimal(250), user, org)); + + if (!userDao.existsByEmail("test2@test.no")) { + String hashedPassword2 = hasher.hash("test2"); + User user2 = new User("test2", "87654321", hashedPassword2, "test2@test.no"); + userDao.insert(user2); + + Organization org2 = orgDao.getAll().get(1); + + donationDao.insert(new Donation(new BigDecimal(150), user2, org2)); + donationDao.insert(new Donation(new BigDecimal(400), user2, org2)); + donationDao.insert(new Donation(new BigDecimal(600), user2, org2)); + } } - } } diff --git a/src/main/java/application/security/PasswordHasher.java b/src/main/java/application/security/PasswordHasher.java index d708691..03e2564 100644 --- a/src/main/java/application/security/PasswordHasher.java +++ b/src/main/java/application/security/PasswordHasher.java @@ -1,5 +1,9 @@ package application.security; +/** + * Interface that creates a "contract" for which methods the class that implements it, needs. + * Interface is created for SHA256PasswordHasher. + */ public interface PasswordHasher { String hash(String password); diff --git a/src/main/java/application/user/UserDonate.java b/src/main/java/application/user/UserDonate.java new file mode 100644 index 0000000..bf3889f --- /dev/null +++ b/src/main/java/application/user/UserDonate.java @@ -0,0 +1,33 @@ +package application.user; + +import domain.donation.Donation; +import domain.organization.Organization; +import domain.user.User; +import persistence.dao.DonationDao; +import java.math.BigDecimal; + +/** + * Use case for handling donations made by a user to an organization. + * Creates a {@link Donation} and persists it to the database. + */ +public class UserDonate { + private final DonationDao donationDao; + + /** + * Creates a new {@code UserDonate} with a default {@link DonationDao}. + */ + public UserDonate() { + this.donationDao = new DonationDao(); + } + + /** + * Executes the donation use case by creating and saving a donation. + * @param user the user making the donation + * @param org the organization receiving the donation + * @param amount the donation amount in NOK + */ + public void execute(User user, Organization org, BigDecimal amount) { + Donation donation = new Donation(amount, user, org); + donationDao.insert(donation); + } +} diff --git a/src/main/java/application/user/UserLogin.java b/src/main/java/application/user/UserLogin.java index e116498..4392ee4 100644 --- a/src/main/java/application/user/UserLogin.java +++ b/src/main/java/application/user/UserLogin.java @@ -2,7 +2,7 @@ import application.security.PasswordHasher; import domain.user.User; -import persistence.UserRepository; +import persistence.dao.UserRepository; public class UserLogin { @@ -16,7 +16,7 @@ public UserLogin(PasswordHasher hasher, UserRepository repo) { public User execute(String login, String password) { // normalize (trim etc.) - login.trim().toLowerCase(); + login = login.trim().toLowerCase(); User user = repo.findByLogin(login) .orElseThrow(() -> new IllegalArgumentException("Invalid credentials")); diff --git a/src/main/java/application/user/UserRegister.java b/src/main/java/application/user/UserRegister.java index 337b378..0a07316 100644 --- a/src/main/java/application/user/UserRegister.java +++ b/src/main/java/application/user/UserRegister.java @@ -2,24 +2,47 @@ import application.security.PasswordHasher; import domain.user.User; -import persistence.UserRepository; - +import persistence.dao.UserRepository; + +/** + * The UserRegister class is responsible for handling the user registration process. + * It ensures that the provided user details meet the necessary criteria and that + * the username and email are not already in use. It also hashes the user's password + * before saving the user information into the repository. + */ public class UserRegister { private final PasswordHasher hasher; private final UserRepository repo; - public UserRegister(PasswordHasher hasher, UserRepository repo) { + /** + * Constructs a {@code UserRegister} instance responsible for handling the user registration process. + * It ensures user details meet validation criteria, such as uniqueness of username and email, + * and hashes passwords before saving user information into the repository. + * + * @param hasher a {@code PasswordHasher} implementation used to hash user passwords. + * @param repo a {@code UserRepository} implementation responsible for storing user data. + */ + public UserRegister(PasswordHasher hasher, UserRepository repo) { this.hasher = hasher; this.repo = repo; } + /** + * Executes a user registration. + * Validation checks, checks if username or e-mail already exists. + * Hashes inputted password using PasswordHasher, that uses SHA256PasswordHasher. + * Creates user object, and inserts it into UserRepository using insert method. + * @param username + * @param phoneNumber + * @param password + * @param email + */ public void execute(String username, String phoneNumber, String password, String email) { username = username.trim(); email = email.trim().toLowerCase(); phoneNumber = phoneNumber.trim(); - // duplicate check (repo.existsByEmail) if (repo.existsByEmail(email)) { throw new IllegalArgumentException("Email already in use"); } @@ -27,13 +50,9 @@ public void execute(String username, String phoneNumber, String password, String throw new IllegalArgumentException("This username is taken"); } - // hash password (optional) String hashedPassword = hasher.hash(password); - // new User(...) (domain validates) User user = new User(username, phoneNumber, hashedPassword, email); - - // saves user repo.insert(user); } } diff --git a/src/main/java/application/user/UserSignIn.java b/src/main/java/application/user/UserSignIn.java new file mode 100644 index 0000000..f7ea596 --- /dev/null +++ b/src/main/java/application/user/UserSignIn.java @@ -0,0 +1,52 @@ +package application.user; + +import application.security.PasswordHasher; +import domain.user.User; +import persistence.dao.UserRepository; + +/** + * The UserSignIn class handles the process of user authentication. + * It verifies the input credentials (login and password) against the stored user data. + * The password is validated through hashing and comparison with the stored hash. + */ +public class UserSignIn { + + private final PasswordHasher hasher; + private final UserRepository repo; + + /** + * Constructs a {@code UserSignIn} instance responsible for user authentication. + * It handles the process of verifying user credentials by comparing the provided + * input with stored data in the repository and validating the password through hashing. + * + * @param hasher a {@code PasswordHasher} implementation used to hash and validate user passwords. + * @param repo a {@code UserRepository} implementation that stores user authentication data. + */ + public UserSignIn(PasswordHasher hasher, UserRepository repo) { + this.hasher = hasher; + this.repo = repo; + } + + /** + * Execute methods, that takes in inputted login and password. + * Checks via UserRepository if login exists in database. + * Compares hashed input password with hashed password in database. + * @param login inputted login by user + * @param password inputted password by user + * @return user object, if it exists. + */ + public User execute(String login, String password) { + login.trim().toLowerCase(); + + User user = repo.findByLogin(login) + .orElseThrow(() -> new IllegalArgumentException("Invalid credentials")); + + String hashedInput = hasher.hash(password); + + if (!hashedInput.equals(user.getPassword())) { + throw new IllegalArgumentException("Invalid password"); + } + + return user; + } +} diff --git a/src/main/java/application/user/UserStatistics.java b/src/main/java/application/user/UserStatistics.java index 5fe67d4..4d3b960 100644 --- a/src/main/java/application/user/UserStatistics.java +++ b/src/main/java/application/user/UserStatistics.java @@ -1,15 +1,14 @@ package application.user; import domain.user.User; -import java.util.ArrayList; + import java.util.List; -import persistence.DonationDao; +import persistence.dao.DonationDao; /** * Provides statistical information about a user's donation activity. * Acts as a service layer between the application and {@link DonationDao}, - * exposing user-friendly methods for retrieving donation history, - * total amounts, and favorite organizations. + * showing relevant userstatistics to the user. */ public class UserStatistics { private DonationDao donationDao; @@ -24,46 +23,42 @@ public UserStatistics() { /** * Returns the top three organizations that the user has donated to most frequently. * - * @param user the user whose favorite organizations are to be retrieved; must not be null + * @param user the user whose favorite organizations are going to be returned. * @return a list of up to three formatted strings, each representing an organization - * and its donation count; empty list if the user has no donations + * and its donation count. Returns empty list if the user has no donations. */ - public List userFavoriteOrganizations(User user) { - List favoriteOrganizations = donationDao.getFavoriteOrganizations(user.getID()); - return favoriteOrganizations; + public List userFavoriteOrganization(User user) { + return donationDao.getFavoriteOrganization(user.getID()); } /** * Returns a formatted list of all donations made by the user. * Each entry contains the donation amount, date, and organization. * - * @param user the user whose donation history is to be retrieved; must not be null - * @return a list of formatted donation strings; empty list if the user has no donations + * @param user the user whose donation history is returned. + * @return a list of formatted donation strings. Returns empty list if the user has no donations */ public List userDonations(User user) { - List userDonationList = donationDao.getUserDonations(user.getID()); - return userDonationList; + return donationDao.getUserDonations(user.getID()); } /** * Returns the total amount donated by the user across all donations. * - * @param user the user whose total donation amount is to be calculated; must not be null - * @return a string representation of the total donated amount; {@code "0"} if no donations exist + * @param user the user whose total donation amount is returned. + * @return a string representation of the total donated amount. Returns "0" if no donations exist */ public String userTotalDonationAmount(User user) { - String totalDonations = donationDao.getTotalDonationAmount(user.getID()); - return totalDonations; + return donationDao.getTotalDonationAmount(user.getID()); } /** * Returns the total number of donations made by the user. * - * @param user the user whose donation count is to be retrieved; must not be null - * @return a string representation of the total number of donations; {@code "0"} if none exist + * @param user the user whose donation count is returned. + * @return a string representation of the total number of donations. Returns "0" if none exist. */ public String getTotalDonationsMade(User user) { - String totalDonationsMade = donationDao.getTotalDonationsMade(user.getID()); - return totalDonationsMade; + return donationDao.getTotalDonationsMade(user.getID()); } } diff --git a/src/main/java/domain/donation/Cause.java b/src/main/java/domain/donation/Cause.java deleted file mode 100644 index ae9311d..0000000 --- a/src/main/java/domain/donation/Cause.java +++ /dev/null @@ -1,3 +0,0 @@ -package domain.donation; - -public enum Cause { HEALTH, EMERGENCY_RELIEF, CHILDREN, ENVIRONMENT, CONFLICTS; } diff --git a/src/main/java/domain/donation/Donation.java b/src/main/java/domain/donation/Donation.java index ff49864..53e1063 100644 --- a/src/main/java/domain/donation/Donation.java +++ b/src/main/java/domain/donation/Donation.java @@ -7,14 +7,24 @@ import java.time.LocalDateTime; import java.util.Objects; +/** + * Donation object that defines what a Donation is. + * Provides set and get methods. + */ public class Donation { - private Long id; //get set to null by default, - // Gets set to a value by database when registered? + private Long id; private final BigDecimal amount; private final LocalDateTime dateTime; private final User user; private final Organization organization; + /** + * Donation takes in parameters that is needed to define what a donation is. + * Requires both user and organization to be not null. + * @param amount + * @param user + * @param organization + */ public Donation(BigDecimal amount, User user, Organization organization) { Objects.requireNonNull(amount, "Amount cannot be null"); if (amount.compareTo(BigDecimal.ZERO) <= 0) { @@ -27,6 +37,15 @@ public Donation(BigDecimal amount, User user, Organization organization) { this.organization = Objects.requireNonNull(organization, "Organization cannot be null"); } + + /** + * Sets the database-generated ID for this donation. + * Can only be set once — throws if the ID is already assigned. + * + * @param id the ID to assign; must be a positive non-null value + * @throws IllegalStateException if the ID has already been set + * @throws IllegalArgumentException if {@code id} is null or not positive + */ public void setId(Long id) { if (this.id != null) { throw new IllegalStateException("ID is already set"); @@ -37,22 +56,47 @@ public void setId(Long id) { this.id = id; } + /** + * Returns the unique database ID of this donation. + * + * @return the donation ID + */ public long getId() { return id; } + /** + * Returns the amount of this donation. + * + * @return the donation amount as a {@link BigDecimal} + */ public BigDecimal getAmount() { return amount; } + /** + * Returns the date and time when this donation was created. + * + * @return the donation timestamp as a {@link LocalDateTime} + */ public LocalDateTime getDateTime() { return dateTime; } + /** + * Returns the user who made this donation. + * + * @return the {@link User} associated with this donation + */ public User getUser() { return user; } + /** + * Returns the organization this donation was made to. + * + * @return the {@link Organization} associated with this donation + */ public Organization getOrganization() { return organization; } diff --git a/src/main/java/domain/organization/Organization.java b/src/main/java/domain/organization/Organization.java index fd2cabf..fd7cec1 100644 --- a/src/main/java/domain/organization/Organization.java +++ b/src/main/java/domain/organization/Organization.java @@ -2,43 +2,92 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import domain.donation.Cause; -import java.util.Objects; +/** + * Represents a charitable organization retrieved from the Innsamlingskontrollen API. + * Contains basic information such as name, organization number, status and URL. + */ @JsonIgnoreProperties(ignoreUnknown = true) public class Organization { + + @JsonProperty("org_number") + private String orgNumber; - @JsonProperty("org_number") - private String orgNumber; + @JsonProperty("name") + private String name; - @JsonProperty("name") - private String name; + @JsonProperty("status") + private String status; - @JsonProperty("status") - private String status; + @JsonProperty("url") + private String url; - @JsonProperty("url") - private String url; + @JsonProperty("is_pre_approved") + public boolean isPreApproved; - @JsonProperty("is_pre_approved") - private boolean isPreApproved; + public Organization() {} - public Organization() {} + /** + * Returns the organization number. + * @return the organization number + */ + public String getOrgNumber() { return orgNumber; } + /** + * Sets the organization number. + * @param orgNumber the organization number to set + */ + public void setOrgNumber(String orgNumber) { this.orgNumber = orgNumber; } - public String getOrgNumber() { return orgNumber; } - public void setOrgNumber(String orgNumber) { this.orgNumber = orgNumber; } + /** + * Returns the name of the organization. + * @return the organization name + */ + public String getName() { return name; } - public String getName() { return name; } - public void setName(String name) { this.name = name; } + /** + * Sets the name of the organization. + * @param name the name to set + */ + public void setName(String name) { this.name = name; } - public String getStatus() { return status; } - public void setStatus(String status) { this.status = status; } + /** + * Returns the approval status of the organization. + * @return the status, either "approved" or "obs" + */ + public String getStatus() { return status; } - public String getUrl() { return url; } - public void setUrl(String url) { this.url = url; } + /** + * Sets the approval status of the organization. + * @param status the status to set + */ + public void setStatus(String status) { this.status = status; } + + /** + * Returns the URL to the organization's page on Innsamlingskontrollen. + * @return the URL + */ + public String getUrl() { return url; } + + /** + * Sets the URL to the organization's page on Innsamlingskontrollen. + * @param url the URL to set + */ + public void setUrl(String url) { this.url = url; } + + /** + * Returns whether the organization is pre-approved. + * @return true if pre-approved, false otherwise + */ + public boolean isPreApproved() { + return isPreApproved; + } + + /** + * Sets whether the organization is pre-approved. + * @param preApproved true if pre-approved, false otherwise + */ + public void setPreApproved(boolean preApproved) { isPreApproved = preApproved; } - public boolean isPreApproved() { return isPreApproved; } - public void setPreApproved(boolean preApproved) { isPreApproved = preApproved; } } diff --git a/src/main/java/domain/user/Role.java b/src/main/java/domain/user/Role.java deleted file mode 100644 index 1eba6cd..0000000 --- a/src/main/java/domain/user/Role.java +++ /dev/null @@ -1,4 +0,0 @@ -package domain.user; - -public enum Role {ADMIN, USER -} diff --git a/src/main/java/domain/user/User.java b/src/main/java/domain/user/User.java index ee6a2a2..e423710 100644 --- a/src/main/java/domain/user/User.java +++ b/src/main/java/domain/user/User.java @@ -1,22 +1,30 @@ package domain.user; +/** + * Class that defines what a User is. + * Contains get and set methods for each parameter, and validating methods. + */ public class User { private String userName; private String phoneNumber; - private final String eMail; + private String eMail; private String password; private Long id; + /** + * Creates a new User with the given parameters. + * Validates all input before assigning. + * @param userName the username; must not be null or blank + * @param phoneNumber the phone number; must be exactly 8 digits + * @param password the hashed password + * @param eMail the email address; must be a valid email format + */ public User(String userName, String phoneNumber, String password, String eMail) { if (userName == null || userName.isBlank()) { throw new IllegalArgumentException("Username has to be filled in"); } - if (eMail == null || eMail.isBlank()) { - throw new IllegalArgumentException("E-mail has to be filled in"); - } - if (phoneNumber == null || phoneNumber.isBlank()) { throw new IllegalArgumentException("Phonenumber has to be filled in"); } @@ -25,45 +33,117 @@ public User(String userName, String phoneNumber, String password, String eMail) throw new IllegalArgumentException("Fill in a phonenumber with 8 digits"); } + if (!isValidEmail(eMail)) { + throw new IllegalArgumentException("Invalid email address: " + eMail); + } + this.userName = userName; this.phoneNumber = phoneNumber; this.eMail = eMail.trim(); this.password = password; - this.id = id; } + /** + * Returns the username of this user. + * + * @return the username + */ public String getUsername() { return userName; } + /** + * Returns the phone number of this user. + * + * @return the phone number + */ public String getPhoneNumber() { return phoneNumber; } - public String getEMail() { + + /** + * Returns the email address of this user. + * + * @return the email address + */ + public String getEmail() { return eMail; } + /** + * Returns the password of this user. + * + * @return the password hash + */ public String getPassword() { return password; } + /** + * Returns the ID of this user. + * + * @return the user ID + */ public long getID() { return id; } - public void setPassword(String password) { - this.password = password; - } - + /** + * Sets the username of this user. + * @param userName the new username; must not be null or blank + */ public void setUsername(String userName) { + if (userName == null || userName.isBlank()) { + throw new IllegalArgumentException("Username has to be filled in"); + } this.userName = userName; } - public void setPhonenumber (String phoneNumber) { + /** + * Sets the phone number of this user. + * @param phoneNumber the new phone number; must be exactly 8 digits + */ + public void setPhoneNumber(String phoneNumber) { + if (phoneNumber == null || phoneNumber.isBlank() || phoneNumber.length() != 8) { + throw new IllegalArgumentException("Fill in a phonenumber with 8 digits"); + } this.phoneNumber = phoneNumber; } + /** + * Sets the email address of this user. + * @param eMail the new email address; must be a valid email format + */ + public void setEmail(String eMail) { + if (!isValidEmail(eMail)) { + throw new IllegalArgumentException("Invalid email address: " + eMail); + } + this.eMail = eMail.trim(); + } + + /** + * Sets the password hash of this user. + * @param password the new hashed password + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Sets the ID for this user. + * + * @param id the ID to assign + */ public void setId(Long id) { - if (this.id != null) throw new IllegalStateException("ID already set"); this.id = id; } + + /** + * Validates that an email address is in the correct format. + * @param email the email address to validate + * @return true if the email is valid, false otherwise + */ + private static boolean isValidEmail(String email) { + if (email == null || email.isBlank()) return false; + return email.matches("^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$"); + } } \ No newline at end of file diff --git a/src/main/java/integration/InnsamlingskontrollenClient.java b/src/main/java/integration/InnsamlingskontrollenClient.java index c0786fa..a4acc6d 100644 --- a/src/main/java/integration/InnsamlingskontrollenClient.java +++ b/src/main/java/integration/InnsamlingskontrollenClient.java @@ -4,12 +4,26 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; import domain.organization.Organization; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +/** + * The InnsamlingskontrollenClient class provides functionality to fetch a list + * of organizations from the Innsamlingskontrollen API. The fetched data is + * represented as an array of Organization objects. + */ public class InnsamlingskontrollenClient { + /** + * Fetches all organizations from the Innsamlingskontrollen API. + * @return an array of Organization objects + * @throws Exception if the request fails + */ public static Organization[] fetchOrganizations() throws Exception { HttpClient client = HttpClient.newHttpClient(); @@ -24,4 +38,44 @@ public static Organization[] fetchOrganizations() throws Exception { return mapper.readValue(response.body(), Organization[].class); } + + /** + * Fetches additional details for an organization from its Innsamlingskontrollen page. + * Scrapes the organization's description and logo URL using Jsoup. + * Returns empty strings if the details cannot be fetched. + * @param url the URL to the organization's page on Innsamlingskontrollen + * @return an OrganizationDetails object containing description and logo URL + */ + public static OrganizationDetails fetchDetails(String url) { + try { + org.jsoup.Connection conn = Jsoup.connect(url); + conn.timeout(10000); + Document doc = conn.get(); + doc.charset(StandardCharsets.UTF_8); + + String description = ""; + for (org.jsoup.nodes.Element p : doc.select("section.information div > p")) { + if (!p.text().isBlank()) { + description = p.text(); + break; + } + } + + if (description.isBlank()) { + String sectionText = doc.select("section.information").text(); + if (!sectionText.isBlank()) { + int lesmerIndex = sectionText.indexOf("Les mer"); + description = lesmerIndex > 0 + ? sectionText.substring(0, lesmerIndex).trim() + : sectionText; + } + } + + String logoUrl = doc.select("img[src*=logo]").attr("src"); + + return new OrganizationDetails(description, logoUrl); + } catch (Exception e) { + return new OrganizationDetails("", ""); + } + } } diff --git a/src/main/java/integration/OrganizationDetails.java b/src/main/java/integration/OrganizationDetails.java new file mode 100644 index 0000000..ca9421c --- /dev/null +++ b/src/main/java/integration/OrganizationDetails.java @@ -0,0 +1,36 @@ +package integration; + +/** + * Holds additional details about an organization fetched from Innsamlingskontrollen's website. + * Contains a short description and a URL to the organization's logo. + */ +public class OrganizationDetails { + private final String description; + private final String logoUrl; + + /** + * Creates a new OrganizationDetails with the given description and logo URL. + * @param description a short description of the organization + * @param logoUrl the URL to the organization's logo + */ + public OrganizationDetails(String description, String logoUrl) { + this.description = description; + this.logoUrl = logoUrl; + } + + /** + * Returns the organization's description. + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Returns the URL to the organization's logo. + * @return the logo URL + */ + public String getLogoUrl() { + return logoUrl; + } +} diff --git a/src/main/java/integration/security/Sha256PasswordHasher.java b/src/main/java/integration/security/Sha256PasswordHasher.java index 2e59caa..95b760c 100644 --- a/src/main/java/integration/security/Sha256PasswordHasher.java +++ b/src/main/java/integration/security/Sha256PasswordHasher.java @@ -4,9 +4,21 @@ import java.security.MessageDigest; import java.nio.charset.StandardCharsets; +/** + * Class that takes in the chosen password of a user, and hashes it. + */ public class Sha256PasswordHasher implements PasswordHasher { - @Override + /** + * Method for hashing an inputted password. + * Uses the SHA-256 algorithm. + * Transforms the password to a UTF-8 byte-array, then digests it using SHA-256. + * The bytes are then formatted as a 64-character string. + * @param password the password to hash + * @return a 64-character hash of the password. + * @throws RuntimeException if the hashing fails. + */ + @Override public String hash(String password) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); diff --git a/src/main/java/persistence/CauseDao.java b/src/main/java/persistence/CauseDao.java deleted file mode 100644 index 4ed035d..0000000 --- a/src/main/java/persistence/CauseDao.java +++ /dev/null @@ -1,4 +0,0 @@ -package persistence; - -public class CauseDao { -} diff --git a/src/main/java/persistence/DonationDao.java b/src/main/java/persistence/dao/DonationDao.java similarity index 93% rename from src/main/java/persistence/DonationDao.java rename to src/main/java/persistence/dao/DonationDao.java index 0361f2b..dcf6cb1 100644 --- a/src/main/java/persistence/DonationDao.java +++ b/src/main/java/persistence/dao/DonationDao.java @@ -1,4 +1,4 @@ -package persistence; +package persistence.dao; import domain.donation.Donation; import domain.organization.Organization; @@ -28,6 +28,10 @@ public class DonationDao { * @throws RuntimeException if a database error occurs during the insert */ public void insert(Donation donation) { + System.out.println("Inserting donation:"); + System.out.println(" user_id = " + donation.getUser().getID()); + System.out.println(" org_id = " + donation.getOrganization().getOrgNumber()); + String sql = "INSERT INTO donation(amount, donation_date" + ", user_id, organization_id) VALUES(?, ?, ?, ?)"; @@ -156,14 +160,15 @@ public List getUserDonations(long userId) { * ordered by the number of donations in descending order * @throws RuntimeException if an error occurs while querying the database */ - public List getFavoriteOrganizations(long userId) { + public List getFavoriteOrganization(long userId) { String sql = """ - SELECT organization_id, COUNT(*) AS donation_count - FROM donation - WHERE user_Id = ? - GROUP BY organization_id + SELECT o.name, COUNT(*) AS donation_count + FROM donation d + JOIN organization o ON d.organization_id = o.org_number + WHERE d.user_id = ? + GROUP BY d.organization_id ORDER BY donation_count DESC - LIMIT 3 + LIMIT 1 """; try (Connection conn = Database.getConnection()) { @@ -202,6 +207,7 @@ SELECT SUM(CAST(amount as REAL)) ResultSet rs = stmt.executeQuery(); if (rs.next()) { BigDecimal amount = rs.getBigDecimal(1); + if (amount == null) return "0"; return amount.toString(); } return "0"; @@ -269,14 +275,7 @@ private String mapRowSimple(ResultSet rs) throws SQLException { * @throws SQLException if a database access error occurs or a column label cannot be found */ private String mapRowFavorite(ResultSet rs) throws SQLException { - return rs.getString("organization_id") - + " (" + rs.getInt("donation_count") + " donasjoner)"; + return rs.getString("name") + + " (" + rs.getInt("donation_count") + " donations)"; } - - - - - - - -} \ No newline at end of file +} diff --git a/src/main/java/persistence/OrganizationDao.java b/src/main/java/persistence/dao/OrganizationDao.java similarity index 66% rename from src/main/java/persistence/OrganizationDao.java rename to src/main/java/persistence/dao/OrganizationDao.java index a0d1346..cd035e1 100644 --- a/src/main/java/persistence/OrganizationDao.java +++ b/src/main/java/persistence/dao/OrganizationDao.java @@ -1,4 +1,4 @@ -package persistence; +package persistence.dao; import domain.organization.Organization; @@ -6,17 +6,29 @@ import java.util.ArrayList; import java.util.List; +/** + * Data Access Object for organization-related database operations. + * Provides methods for inserting and retrieving {@link Organization} records + * from the SQLite database. + */ public class OrganizationDao { private static final String URL = "jdbc:sqlite:givehope.db"; + /** + * Inserts a new organization into the database. + * Uses {@code INSERT OR IGNORE} so duplicate org numbers are silently skipped. + * + * @param org the {@link Organization} to insert; must not be null and must have a valid org number + * @throws SQLException if a database error occurs during the insert + */ public void insert(Organization org) throws SQLException { String sql = """ INSERT OR IGNORE INTO organization (org_number, name, status, url, is_pre_approved) VALUES (?, ?, ?, ?, ?) """; - + try (Connection conn = DriverManager.getConnection(URL); PreparedStatement stmt = conn.prepareStatement(sql)) { @@ -29,6 +41,12 @@ public void insert(Organization org) throws SQLException { } } + /** + * Retrieves all organizations stored in the database. + * + * @return a list of all {@link Organization} objects; empty list if none exist + * @throws SQLException if a database error occurs during the query + */ public List getAll() throws SQLException { String sql = "SELECT * FROM organization"; List list = new ArrayList<>(); @@ -49,4 +67,4 @@ public List getAll() throws SQLException { } return list; } -} \ No newline at end of file +} diff --git a/src/main/java/persistence/UserDao.java b/src/main/java/persistence/dao/UserDao.java similarity index 64% rename from src/main/java/persistence/UserDao.java rename to src/main/java/persistence/dao/UserDao.java index 2fa7765..a1b0544 100644 --- a/src/main/java/persistence/UserDao.java +++ b/src/main/java/persistence/dao/UserDao.java @@ -1,17 +1,29 @@ -package persistence; +package persistence.dao; import java.sql.ResultSet; import java.sql.Statement; import java.util.Optional; + import persistence.db.Database; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import domain.user.User; +/** + * Data Access Object for user-related database operations. + * Implements {@link UserRepository} and provides methods for inserting users + * and looking them up by email, username, or either. + */ public class UserDao implements UserRepository { - + /** + * Inserts a new user into the database. + * Upon successful insertion, the generated database ID is set on the user object. + * + * @param user the {@link User} to insert; must not be null and must have a unique email and username + * @throws RuntimeException if a database error occurs, including unique constraint violations + */ public void insert(User user) { String sql = "INSERT INTO user(user_name, phone_number, e_mail, password_hash) VALUES(?, ?, ?, ?)"; @@ -21,7 +33,7 @@ public void insert(User user) { stmt.setString(1, user.getUsername()); stmt.setString(2, user.getPhoneNumber()); - stmt.setString(3, user.getEMail()); + stmt.setString(3, user.getEmail()); //endre denne til passwordhash når det er fikset. stmt.setString(4, user.getPassword()); @@ -37,8 +49,14 @@ public void insert(User user) { } } - //private User mapRow(ResultSet rs) throws SQLException {} + /** + * Finds a user by their email address. + * + * @param email the email address to search for; leading/trailing whitespace is trimmed + * @return an {@link Optional} containing the user if found, or empty if no match exists + * @throws RuntimeException if a database error occurs during the query + */ @Override public Optional findByEmail(String email) { String sql = """ @@ -67,6 +85,13 @@ public Optional findByEmail(String email) { } } + /** + * Finds a user by their username. + * + * @param username the username to search for + * @return an {@link Optional} containing the user if found, or empty if no match exists + * @throws RuntimeException if a database error occurs during the query + */ @Override public Optional findByUsername(String username) { String sql = """ @@ -95,6 +120,13 @@ public Optional findByUsername(String username) { } } + /** + * Finds a user by either email or username. + * First attempts to match by email, then falls back to username if no email match is found. + * + * @param login the email or username to search for; returns empty if null or blank + * @return an {@link Optional} containing the user if found, or empty if no match exists + */ @Override public Optional findByLogin(String login) { if (login == null || login.isBlank()) { @@ -110,6 +142,13 @@ public Optional findByLogin(String login) { } + /** + * Checks whether a user with the given email address already exists in the database. + * + * @param email the email address to check + * @return {@code true} if a user with the given email exists, {@code false} otherwise + * @throws RuntimeException if a database error occurs during the query + */ @Override public boolean existsByEmail(String email) { String sql = "SELECT 1 FROM user WHERE e_mail = ?"; @@ -128,6 +167,13 @@ public boolean existsByEmail(String email) { } } + /** + * Checks whether a user with the given username already exists in the database. + * + * @param username the username to check + * @return {@code true} if a user with the given username exists, {@code false} otherwise + * @throws RuntimeException if a database error occurs during the query + */ @Override public boolean existsByUsername(String username) { String sql = "SELECT 1 FROM user WHERE user_name = ?"; @@ -138,7 +184,7 @@ public boolean existsByUsername(String username) { stmt.setString(1, username.trim().toLowerCase()); try (ResultSet rs = stmt.executeQuery()) { - return rs.next(); // true hvis minst én rad finnes + return rs.next(); } } catch (SQLException e) { @@ -146,13 +192,6 @@ public boolean existsByUsername(String username) { } } - //User findById(long id) throws SQLException {} - - //List findAllUsers() throws SQLException {} - - void update(User user) throws SQLException {} - - void updatePassword(long userId, String newPasswordHash) throws SQLException {} private User mapUser(ResultSet rs) throws SQLException { diff --git a/src/main/java/persistence/UserRepository.java b/src/main/java/persistence/dao/UserRepository.java similarity index 61% rename from src/main/java/persistence/UserRepository.java rename to src/main/java/persistence/dao/UserRepository.java index 3aa8d43..0ee64c6 100644 --- a/src/main/java/persistence/UserRepository.java +++ b/src/main/java/persistence/dao/UserRepository.java @@ -1,9 +1,13 @@ -package persistence; +package persistence.dao; import domain.user.User; - import java.util.Optional; +/** + * Interface that defines a "contract" for which methods the class that implements it has + * to contiain. + * Is created for UserDao class, to define which methods needs to be implemented. + */ public interface UserRepository { Optional findByUsername(String username); Optional findByEmail(String email); diff --git a/src/main/java/persistence/db/Database.java b/src/main/java/persistence/db/Database.java index d4b51f2..b110a70 100644 --- a/src/main/java/persistence/db/Database.java +++ b/src/main/java/persistence/db/Database.java @@ -8,6 +8,9 @@ import java.sql.SQLException; import java.sql.Statement; +/** + * Class that initializes and connects to the generated database. + */ public final class Database { public static final String URL = "jdbc:sqlite:givehope.db"; @@ -15,6 +18,13 @@ public final class Database { initializeDatabase(); } + /** + * Methods that provides a connection the URL/file that is initialized. + * Runs "PRAGMA FOREIGN KEYS = ON", which turns foreign key check on in SQLite. + * Connection class is used connects the java-code to the database/SQL-code. + * @return returns connection to database. + * @throws SQLException + */ public static Connection getConnection() throws SQLException { Connection conn = DriverManager.getConnection(URL); try (Statement stmt = conn.createStatement()) { @@ -23,7 +33,13 @@ public static Connection getConnection() throws SQLException { return conn; } - private static void initializeDatabase() { + /** + * Method that initializes/creates the database. + * Uses file "schema.sql" to create, which contains SQL-code that creates the tables. + * Splits string at ";", to separate each CREATE TABLE-sentences. + * @throws RuntimeException if the schema file cannot be read or a database error occurs. + */ + public static void initializeDatabase() { try (Connection conn = DriverManager.getConnection(URL); Statement stmt = conn.createStatement()) { diff --git a/src/main/java/ui/DonationSession.java b/src/main/java/ui/DonationSession.java new file mode 100644 index 0000000..0d3bf1d --- /dev/null +++ b/src/main/java/ui/DonationSession.java @@ -0,0 +1,47 @@ +package ui; + +import domain.organization.Organization; + +/** + * Holds the state of an ongoing donation session. + * Passed between donation flow controllers to carry + * the selected organization and amount. + */ +public class DonationSession { + private Organization organization; + private int amount; + + /** + * Returns the organization selected for donation. + * @return the selected organization + */ + public Organization getOrganization() { + return organization; + } + + /** + * Returns the donation amount. + * @return the amount in NOK + */ + public int getAmount() { + return amount; + } + + /** + * Sets the organization to donate to. + * @param organization the organization to set + */ + public void setOrganization(Organization organization) { + this.organization = organization; + } + + /** + * Sets the donation amount. + * @param amount the amount in NOK + */ + public void setAmount(int amount) { + this.amount = amount; + } + +} + diff --git a/src/main/java/ui/DonationSessionAware.java b/src/main/java/ui/DonationSessionAware.java new file mode 100644 index 0000000..75b9965 --- /dev/null +++ b/src/main/java/ui/DonationSessionAware.java @@ -0,0 +1,15 @@ +package ui; + +/** + * Interface for controllers that participate in the donation flow. + * Implementing controllers receive the current {@link DonationSession} + * when they are loaded. + */ +public interface DonationSessionAware { + + /** + * Sets the current donation session. + * @param session the active donation session + */ + void setDonationSession(DonationSession session); +} diff --git a/src/main/java/ui/GiveHopeGIU.java b/src/main/java/ui/GiveHopeGIU.java deleted file mode 100644 index 2617122..0000000 --- a/src/main/java/ui/GiveHopeGIU.java +++ /dev/null @@ -1,23 +0,0 @@ -package ui; - -import javafx.application.Application; -import javafx.fxml.FXMLLoader; -import javafx.scene.Scene; -import javafx.stage.Stage; - -public class GiveHopeGIU extends Application { - - @Override - public void start(Stage stage) throws Exception { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/ui/home.fxml")); - Scene scene = new Scene(loader.load(), 600, 400); - - stage.setTitle("GiveHope"); - stage.setScene(scene); - stage.show(); - } - - public static void main(String[] args) { - launch(); - } -} \ No newline at end of file diff --git a/src/main/java/ui/Page.java b/src/main/java/ui/Page.java new file mode 100644 index 0000000..77cac0f --- /dev/null +++ b/src/main/java/ui/Page.java @@ -0,0 +1,36 @@ +package ui; + +/** + * Represents the different pages available in the application, where each page + * is associated with a specific FXML file used for its layout. + */ +public enum Page { + HOME("Home.fxml"), + PROFILE("MyProfile.fxml"), + SIGN_IN("SignIn.fxml"), + REGISTER("Register.fxml"), + ORGANIZATIONS("OrganizationsView.fxml"), + MY_PROFILE("MyProfile.fxml"), + DONATION_NOT_LOGGED_IN("DonationNotLoggedIn.fxml"), + DONATION_AMOUNT("DonationAmount.fxml"), + DONATION_PAYMENT("DonationPayment.fxml"), + DONATION_CONFIRMATION("DonationConfirmation.fxml"); + + private final String fileName; + + /** + * Constructs a Page with the associated FXML filename. + * @param fileName the name of the FXML file for this page + */ + Page(String fileName) { + this.fileName = fileName; + } + + /** + * Returns the FXML filename associated with this page. + * @return the FXML filename + */ + public String getFileName() { + return fileName; + } +} diff --git a/src/main/java/ui/controller/HomeController.java b/src/main/java/ui/controller/HomeController.java index bda7d61..02420a6 100644 --- a/src/main/java/ui/controller/HomeController.java +++ b/src/main/java/ui/controller/HomeController.java @@ -1,24 +1,27 @@ package ui.controller; import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.control.Button; +import ui.Page; +import java.util.function.Consumer; -import java.io.IOException; +/** + * Controller for the home page. + */ +public class HomeController implements NavigationAware { -public class HomeController { + private Consumer onNavigate; - public Button signInButton; - - @FXML - private void goToSignIn() throws IOException { - Parent root = FXMLLoader.load(getClass().getResource("/ui/signIn.fxml")); - signInButton.getScene().setRoot(root); + /** + * Sets the navigation callback. + * @param onNavigate the callback for navigating to a page + */ + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; } @FXML - private void handleBrowse() { - System.out.println("Browse button pressed"); + private void handleBrowseOrganizations() { + onNavigate.accept(Page.ORGANIZATIONS); } + } \ No newline at end of file diff --git a/src/main/java/ui/controller/MainController.java b/src/main/java/ui/controller/MainController.java new file mode 100644 index 0000000..c586dbf --- /dev/null +++ b/src/main/java/ui/controller/MainController.java @@ -0,0 +1,158 @@ +package ui.controller; + +import application.user.UserRegister; +import application.user.UserSignIn; +import domain.organization.Organization; +import integration.OrganizationDetails; +import integration.InnsamlingskontrollenClient; +import integration.security.Sha256PasswordHasher; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; +import persistence.dao.UserDao; +import ui.DonationSession; +import ui.Page; +import ui.DonationSessionAware; +import ui.controller.donation.DonationFlowController; +import java.io.IOException; + +/** + * Main controller for the application. + * Manages page navigation, the donation flow, and the organization detail popup. + */ +public class MainController { + + @FXML + private BorderPane mainPane; + + @FXML + private NavbarController navbarController; + + @FXML + private VBox overlay; + + private DonationSession donationSession; + private DonationFlowController donationFlowController; + + public void initialize() { + donationFlowController = new DonationFlowController(mainPane, navbarController); + donationFlowController.setOnNavigate(page -> { + try { loadPage(page); } + catch (IOException e) { throw new RuntimeException(e); } + }); + + + try { + navbarController.setOnNavigate(page -> { + try { + loadPage(page); + } catch (IOException e) { + + } + }); + loadPage(Page.HOME); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Loads and displays the given page in the main content area. + * Sets up the appropriate controller callbacks for each page. + * @param page the page to load + * @throws IOException if the FXML file cannot be loaded + */ + public void loadPage(Page page) throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/" + page.getFileName())); + Parent root = loader.load(); + + Object controller = loader.getController(); + if (controller instanceof NavigationAware navigationAware) { + navigationAware.setOnNavigate(p -> { + try { + loadPage(p); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + if (controller instanceof SignInController c) { + c.setUserSignIn(new UserSignIn(new Sha256PasswordHasher(), new UserDao())); + } + if (controller instanceof RegisterController c) { + c.setUserRegister(new UserRegister(new Sha256PasswordHasher(), new UserDao())); + } + if (controller instanceof DonationSessionAware c) { + c.setDonationSession(donationSession); + } + if (controller instanceof OrganizationController c) { + c.setOnDonate(org -> { + try { loadDonationPage(org); } + catch (IOException e) { throw new RuntimeException(e); } + }); + c.setOnShowDetail(org -> showOrganizationDetail(org)); + } + + mainPane.setCenter(root); + navbarController.setActivePage(page); + navbarController.updateAuthButton(); + navbarController.showNavbar(); + } + + /** + * Starts the donation flow for the given organization. + * @param org the organization to donate to + * @throws IOException if the FXML file cannot be loaded + */ + public void loadDonationPage(Organization org) throws IOException { + donationFlowController.start(org); + } + + /** + * Shows the organization detail popup for the given organization. + * Fetches description and logo from Innsamlingskontrollen and displays them. + * @param org the organization to show details for + */ + public void showOrganizationDetail(Organization org) { + try { + FXMLLoader loader = new FXMLLoader( + getClass().getResource("/view/OrganizationDetail.fxml")); + Parent root = loader.load(); + + OrganizationDetails details = + InnsamlingskontrollenClient.fetchDetails(org.getUrl()); + + OrganizationDetailController controller = loader.getController(); + controller.setOrganization(org, details); + controller.setOnClose(() -> { + overlay.setVisible(false); + overlay.setManaged(false); + overlay.getChildren().clear(); + }); + controller.setOnDonate(o -> { + overlay.setVisible(false); + overlay.setManaged(false); + overlay.getChildren().clear(); + try { + loadDonationPage(o); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + overlay.getChildren().clear(); + overlay.getChildren().add(root); + overlay.setVisible(true); + overlay.setManaged(true); + overlay.setStyle("-fx-background-color: rgba(0,0,0,0.5);"); + overlay.setMaxWidth(Double.MAX_VALUE); + overlay.setMaxHeight(Double.MAX_VALUE); + overlay.setAlignment(javafx.geometry.Pos.CENTER); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/ui/controller/MyProfileController.java b/src/main/java/ui/controller/MyProfileController.java new file mode 100644 index 0000000..b8dd62c --- /dev/null +++ b/src/main/java/ui/controller/MyProfileController.java @@ -0,0 +1,81 @@ +package ui.controller; + +import application.user.UserStatistics; +import domain.donation.Donation; +import domain.user.User; +import java.util.List; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import persistence.dao.DonationDao; +import ui.Page; +import util.SessionManager; + +import java.util.function.Consumer; + +/** + * Controller for the user profile page. + * Displays user information, donation statistics and donation history. + */ +public class MyProfileController implements NavigationAware { + + private Consumer onNavigate; + @FXML private Label totalDonatedLabel; + @FXML private Label numberOfDonationsLabel; + @FXML private Label favoriteOrgLabel; + @FXML private Label nameLabel; + @FXML private Label emailLabel; + @FXML private Label phoneLabel; + @FXML private TableView donationTable; + @FXML private TableColumn orgColumn; + @FXML private TableColumn amountColumn; + @FXML private TableColumn dateColumn; + + /** + * Sets the navigation callback. + * @param onNavigate the callback for navigating to a page + */ + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; + } + + @FXML + public void initialize() { + User user = SessionManager.getSignedInUser(); + if (user == null) return; + + nameLabel.setText("Name: " + user.getUsername()); + emailLabel.setText("E-mail: " + user.getEmail()); + phoneLabel.setText("Phonenumber: " + user.getPhoneNumber()); + + UserStatistics stats = new UserStatistics(); + totalDonatedLabel.setText(stats.userTotalDonationAmount(user) + " kr"); + numberOfDonationsLabel.setText(stats.getTotalDonationsMade(user)); + + List favOrg = stats.userFavoriteOrganization(user); + favoriteOrgLabel.setText(favOrg.isEmpty() ? "None" : favOrg.get(0)); + + orgColumn.setCellValueFactory(cell -> + new SimpleStringProperty(cell.getValue().getOrganization().getName())); + amountColumn.setCellValueFactory(cell -> + new SimpleStringProperty(cell.getValue().getAmount().toString() + " kr")); + dateColumn.setCellValueFactory(cell -> + new SimpleStringProperty(cell.getValue().getDateTime().toLocalDate().toString())); + + DonationDao donationDao = new DonationDao(); + ObservableList data = FXCollections.observableArrayList( + donationDao.findByUser(user.getID()) + ); + donationTable.setItems(data); + } + + @FXML + private void handleSignOut() { + SessionManager.signOut(); + onNavigate.accept(Page.HOME); + } +} diff --git a/src/main/java/ui/controller/NavbarController.java b/src/main/java/ui/controller/NavbarController.java new file mode 100644 index 0000000..af0d65f --- /dev/null +++ b/src/main/java/ui/controller/NavbarController.java @@ -0,0 +1,123 @@ +package ui.controller; + +import java.util.function.Consumer; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import ui.Page; +import util.SessionManager; + +/** + * Controller for the navigation bar. + * Handles navigation between pages and updates the active button state. + */ +public class NavbarController { + + private Consumer onNavigate; + @FXML + private HBox navBar; + @FXML + private Button homeBtn; + @FXML + private Button orgBtn; + @FXML + private Button signInBtn; + @FXML + private Label logoLabel; + + /** + * Sets the navigation callback. + * @param onNavigate the callback for navigating to a page + */ + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; + } + + /** + * Highlights the navbar button corresponding to the given page. + * @param page the currently active page + */ + public void setActivePage(Page page) { + homeBtn.getStyleClass().remove("nav-button-active"); + orgBtn.getStyleClass().remove("nav-button-active"); + signInBtn.getStyleClass().remove("nav-button-active"); + + Button buttonForPage = switch (page) { + case HOME -> homeBtn; + case ORGANIZATIONS -> orgBtn; + case SIGN_IN, PROFILE -> signInBtn; + default -> null; + }; + + if (buttonForPage != null) { + buttonForPage.getStyleClass().add("nav-button-active"); + } + } + + /** + * Navigates to the home page. + */ + @FXML + public void goHome() { + onNavigate.accept(Page.HOME); + } + + /** + * Navigates to the sign in page or profile page depending on authentication state. + */ + @FXML + public void goToSignIn() { + if (SessionManager.isSignedIn()) { + onNavigate.accept(Page.PROFILE); + } else { + onNavigate.accept(Page.SIGN_IN); + } + } + + /** + * Navigates to the organizations page. + */ + @FXML + public void goToOrganizations() { + onNavigate.accept(Page.ORGANIZATIONS); + } + + + /** + * Updates the sign in button text based on the user's authentication state. + * Shows "My Profile" if signed in, "Sign In" otherwise. + */ + public void updateAuthButton() { + if (SessionManager.isSignedIn()) { + signInBtn.setText("My Profile"); + } else { + signInBtn.setText("Sign In"); + } + } + + /** + * Navigates to the home page. + */ + @FXML + private void handleLogoClick() { + onNavigate.accept(Page.HOME); + } + + /** + * Hides the navbar. + */ + public void hideNavbar() { + navBar.setVisible(false); + navBar.setManaged(false); + } + + /** + * Shows the navbar. + */ + public void showNavbar() { + navBar.setVisible(true); + navBar.setManaged(true); + } +} diff --git a/src/main/java/ui/controller/NavigationAware.java b/src/main/java/ui/controller/NavigationAware.java new file mode 100644 index 0000000..82f215f --- /dev/null +++ b/src/main/java/ui/controller/NavigationAware.java @@ -0,0 +1,20 @@ +package ui.controller; + +import ui.Page; + +import java.util.function.Consumer; + +/** + * Interface for controllers that need to navigate between pages. + * Controllers that implement this interface can switch to a different page + * by calling the navigation function they receive. + */ +public interface NavigationAware { + + /** + * Sets the navigation function that the controller can use to switch pages. + * + * @param onNavigate a function that takes a {@link Page} and navigates to it + */ + void setOnNavigate(Consumer onNavigate); +} \ No newline at end of file diff --git a/src/main/java/ui/controller/OrganizationController.java b/src/main/java/ui/controller/OrganizationController.java new file mode 100644 index 0000000..00ad56c --- /dev/null +++ b/src/main/java/ui/controller/OrganizationController.java @@ -0,0 +1,160 @@ +package ui.controller; + +import domain.organization.Organization; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.VBox; +import persistence.dao.OrganizationDao; +import ui.Page; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Controller for the organizations page. + * Displays a paginated list of approved organizations with search functionality. + */ +public class OrganizationController implements NavigationAware{ + + @FXML + private TextField searchField; + @FXML + private FlowPane organizationContainer; + @FXML + private Button previousPageBtn; + @FXML + private Button nextPageBtn; + @FXML + private Label pageLabel; + + private List allOrgs; + private List filtrertOrgs; + private int page = 0; + private final int PER_PAGE = 15; + + private Consumer onNavigate; + private Consumer onDonate; + private Consumer onShowDetail; + + /** + * Sets the navigation callback. + * @param onNavigate the callback for navigating to a page + */ + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; + } + + /** + * Sets the callback to invoke when the user clicks Donate on an organization card. + * @param onDonate the callback with the selected organization + */ + public void setOnDonate(Consumer onDonate) { + this.onDonate = onDonate; + } + + /** + * Sets the callback to invoke when the user clicks on an organization card. + * @param onShowDetail the callback with the selected organization + */ + public void setOnShowDetail(Consumer onShowDetail) { + this.onShowDetail = onShowDetail; + } + + public void initialize() { + try { + OrganizationDao dao = new OrganizationDao(); + allOrgs = dao.getAll().stream() + .filter(org -> "approved".equals(org.getStatus())) + .collect(Collectors.toList()); + filtrertOrgs = new ArrayList<>(allOrgs); + showPage(); + + searchField.textProperty().addListener((obs, old, nyTekst) -> { + filtrertOrgs = allOrgs.stream() + .filter(org -> org.getName().toLowerCase() + .contains(nyTekst.toLowerCase())) + .collect(Collectors.toList()); + page = 0; + showPage(); + }); + + organizationContainer.widthProperty().addListener((obs, oldWidth, newWidth) -> { + double cardWidth = (newWidth.doubleValue() - 60) / 3; + for (var node : organizationContainer.getChildren()) { + if (node instanceof VBox card) { + card.setPrefWidth(cardWidth); + } + } + }); + + } catch (SQLException e) { + System.out.println("Feil ved henting: " + e.getMessage()); + } + } + + private void showPage() { + if (filtrertOrgs == null || filtrertOrgs.isEmpty()) { + System.out.println("No organizations to show!"); + return; + } + + organizationContainer.getChildren().clear(); + + int fra = page * PER_PAGE; + int til = Math.min(fra + PER_PAGE, filtrertOrgs.size()); + + for (int i = fra; i < til; i++) { + organizationContainer.getChildren().add(createCard(filtrertOrgs.get(i))); + } + + int totalSider = (int) Math.ceil((double) filtrertOrgs.size() / PER_PAGE); + pageLabel.setText("Side " + (page + 1) + " av " + totalSider); + previousPageBtn.setDisable(page == 0); + nextPageBtn.setDisable(page >= totalSider - 1); + } + + private VBox createCard(Organization org) { + VBox card = new VBox(8); + card.getStyleClass().add("org-card"); + double width = organizationContainer.getWidth(); + card.setPrefWidth(width > 0 ? (width - 60) / 3 : 400); + + Label name = new Label(org.getName()); + name.getStyleClass().add("org-name"); + name.setWrapText(true); + + Label orgnr = new Label("Org.nr: " + org.getOrgNumber()); + orgnr.getStyleClass().add("org-detail"); + + Label status = new Label(org.isPreApproved() ? "✓ Forhåndsgodkjent" : "✓ Verified"); + status.getStyleClass().add("org-status"); + + Button donateBtn = new Button("Donate"); + donateBtn.getStyleClass().add("donate-button"); + donateBtn.setMaxWidth(Double.MAX_VALUE); + donateBtn.setOnAction(e -> onDonate.accept(org)); + + card.setOnMouseClicked(e -> onShowDetail.accept(org)); + card.getChildren().addAll(name, orgnr, status, donateBtn); + return card; + } + + @FXML + private void previousPage() { + page--; + showPage(); + } + + @FXML + private void nextPage() { + page++; + showPage(); + } +} diff --git a/src/main/java/ui/controller/OrganizationDetailController.java b/src/main/java/ui/controller/OrganizationDetailController.java new file mode 100644 index 0000000..39b2e92 --- /dev/null +++ b/src/main/java/ui/controller/OrganizationDetailController.java @@ -0,0 +1,119 @@ +package ui.controller; + +import domain.organization.Organization; +import integration.OrganizationDetails; +import javafx.fxml.FXML; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import java.awt.Desktop; +import java.net.URI; +import java.util.function.Consumer; + +/** + * Controller for the organization detail popup. + * Displays organization name, verified status, description and logo. + */ +public class OrganizationDetailController { + + @FXML private Label nameLabel; + @FXML private Label orgNumberLabel; + @FXML private Label verifiedLabel; + @FXML private Label descriptionLabel; + @FXML private ImageView logoImageView; + @FXML private Hyperlink readMoreLink; + + + private Organization organization; + private Consumer onDonate; + private Runnable onClose; + + /** + * Sets the organization and its details, and updates all UI elements. + * Cuts the description at 300 characters if it is too long. + * @param org the organization to display + * @param details the scraped details including description and logo URL + */ + public void setOrganization(Organization org, OrganizationDetails details) { + this.organization = org; + nameLabel.setText(org.getName()); + orgNumberLabel.setText("Org.nr: " + org.getOrgNumber()); + verifiedLabel.setText(org.isPreApproved() ? "✓ Forhåndsgodkjent" : "✓ Verified"); + + if (details.getDescription() != null && !details.getDescription().isBlank()) { + String description = details.getDescription(); + if (description.length() > 300) { + int lastPeriod = description.lastIndexOf(".", 300); + description = lastPeriod > 0 + ? description.substring(0, lastPeriod + 1) + : description.substring(0, 300) + "..."; + readMoreLink.setVisible(true); + readMoreLink.setManaged(true); + } + descriptionLabel.setText(description); + } else { + descriptionLabel.setText("No description available."); + } + + + + if (details.getLogoUrl() != null && !details.getLogoUrl().isBlank()) { + try { + Image image = new Image(details.getLogoUrl(), true); + image.errorProperty().addListener((obs, old, error) -> { + if (error) { + logoImageView.setVisible(false); + } + }); + logoImageView.setImage(image); + } catch (Exception e) { + logoImageView.setVisible(false); + } + } else { + logoImageView.setVisible(false); + } + System.out.println("Beskrivelse: " + details.getDescription()); + System.out.println("Lengde: " + details.getDescription().length()); + } + + /** + * Sets the callback to invoke when the user clicks Donate. + * @param onDonate the callback with the selected organization + */ + public void setOnDonate(Consumer onDonate) { + this.onDonate = onDonate; + } + + /** + * Sets the callback to invoke when the user closes the popup. + * @param onClose the callback to run on close + */ + public void setOnClose(Runnable onClose) { + this.onClose = onClose; + } + + @FXML + public void handleClose() { + if (onClose != null) onClose.run(); + } + + @FXML + public void handleDonate() { + if (onDonate != null) onDonate.accept(organization); + } + + @FXML + public void handleVisitWebsite() { + try { + Desktop.getDesktop().browse(new URI(organization.getUrl())); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @FXML + public void handleReadMore() { + handleVisitWebsite(); + } +} diff --git a/src/main/java/ui/controller/RegisterController.java b/src/main/java/ui/controller/RegisterController.java new file mode 100644 index 0000000..db662f8 --- /dev/null +++ b/src/main/java/ui/controller/RegisterController.java @@ -0,0 +1,82 @@ +package ui.controller; + +import application.user.UserRegister; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import ui.Page; + +import java.util.function.Consumer; + +/** + * Controller for the register page. + * Handles user input validation and registration. + */ +public class RegisterController implements NavigationAware { + + @FXML + private TextField usernameField; + @FXML + private TextField emailField; + @FXML + private TextField phoneNumberField; + @FXML + private PasswordField passwordField; + @FXML + private PasswordField confirmPasswordField; + @FXML + private Label errorLabel; + + private UserRegister userRegister; + private Consumer onNavigate; + + /** + * Sets the user register use case. + * @param userRegister the use case for registering a new user + */ + public void setUserRegister(UserRegister userRegister) { + this.userRegister = userRegister; + } + + /** + * Sets the navigation callback. + * @param onNavigate the callback for navigating to a page + */ + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; + } + + @FXML + private void handleRegister() { + String username = usernameField.getText().trim(); + String email = emailField.getText().trim(); + String phoneNr = phoneNumberField.getText().trim(); + String password = passwordField.getText().trim(); + String confirmPassword = confirmPasswordField.getText().trim(); + + if (username.isEmpty() || email.isEmpty() || phoneNr.isEmpty() || password.isEmpty() || confirmPassword.isEmpty()) { + errorLabel.setText("Please fill in all fields."); + return; + } + + if (!password.equals(confirmPassword)) { + errorLabel.setText("Passwords do not match."); + return; + } + + errorLabel.setText(""); + + try { + userRegister.execute(username, phoneNr, password, email); + onNavigate.accept(Page.SIGN_IN); + } catch (IllegalArgumentException e) { + errorLabel.setText(e.getMessage()); + } + } + + @FXML + private void handleSignIn() { + onNavigate.accept(Page.SIGN_IN); + } +} diff --git a/src/main/java/ui/controller/SignInController.java b/src/main/java/ui/controller/SignInController.java index 3e141bf..6cde88c 100644 --- a/src/main/java/ui/controller/SignInController.java +++ b/src/main/java/ui/controller/SignInController.java @@ -1,36 +1,68 @@ package ui.controller; +import application.user.UserSignIn; +import domain.user.User; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; +import ui.Page; +import util.SessionManager; +import java.io.IOException; +import java.util.function.Consumer; -public class SignInController { +/** + * Controller for the sign-in page. + * Handles user authentication and navigation after sign in. + */ +public class SignInController implements NavigationAware { - @FXML private TextField emailField; + @FXML private TextField loginField; @FXML private PasswordField passwordField; @FXML private Label errorLabel; + private UserSignIn userSignIn; + private Consumer onNavigate; + + /** + * Sets the sign in use case. + * @param userSignIn the use case for signing in a user + */ + public void setUserSignIn(UserSignIn userSignIn) { + this.userSignIn = userSignIn; + } + + /** + * Sets the navigation callback. + * @param onNavigate the callback for navigating to a page + */ + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; + } + @FXML private void handleSignIn() { - String email = emailField.getText(); - String password = passwordField.getText(); + String login = loginField.getText().trim(); + String password = passwordField.getText().trim(); - // Mini-sjekk for å se at wiring funker - if (email == null || email.isBlank() || password == null || password.isBlank()) { + if (login.isEmpty() || password.isEmpty()) { errorLabel.setText("Please enter email and password."); return; } - errorLabel.setText(""); // nullstill - System.out.println("Trying to sign in with: " + email); + errorLabel.setText(""); - // Her skal du senere kalle service/DB (ikke her inne direkte!) + try { + User user = userSignIn.execute(login, password); + SessionManager.signIn(user); + onNavigate.accept(Page.HOME); + } catch (IllegalArgumentException e) { + errorLabel.setText(e.getMessage()); + } } @FXML - private void goToRegister() { - System.out.println("Go to Register clicked"); - // Neste steg: bytte scene/view + private void handleRegister() throws IOException { + onNavigate.accept(Page.REGISTER); } } \ No newline at end of file diff --git a/src/main/java/ui/controller/donation/DonationAmountController.java b/src/main/java/ui/controller/donation/DonationAmountController.java new file mode 100644 index 0000000..2b35c83 --- /dev/null +++ b/src/main/java/ui/controller/donation/DonationAmountController.java @@ -0,0 +1,98 @@ +package ui.controller.donation; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import ui.DonationSession; +import ui.DonationSessionAware; + + +import java.util.function.Consumer; + +/** + * Controller for the donation amount selection page. + * Handles amount input validation and navigation in the donation flow. + */ +public class DonationAmountController implements DonationSessionAware { + + @FXML private Label orgLabel; + @FXML private TextField amountField; + @FXML private Label errorLabel; + + private DonationSession donationSession; + private Consumer onContinue; + private Runnable onBack; + + /** + * Sets the current donation session and updates the organization label. + * @param donationSession the active donation session + */ + @Override + public void setDonationSession(DonationSession donationSession) { + this.donationSession = donationSession; + orgLabel.setText("Donating to: " + donationSession.getOrganization().getName()); + } + + /** + * Sets the callback to invoke when the user continues to the next step. + * @param onContinue the callback with the updated donation session + */ + public void setOnContinue(Consumer onContinue) { + this.onContinue = onContinue; + } + + /** + * Sets the callback to invoke when the user navigates back. + * @param onBack the callback to run on back navigation + */ + public void setOnBack(Runnable onBack) { + this.onBack = onBack; + } + + /** + * Validates the entered amount and continues to the next step in the donation flow. + */ + @FXML + public void handleContinue() { + if (amountField.getText().isBlank()) { + errorLabel.setText("Please enter an amount."); + return; + } + try { + int amount = Integer.parseInt(amountField.getText().trim()); + if (amount <= 0) { + errorLabel.setText("Please enter an amount greater than 0."); + return; + } + donationSession.setAmount(amount); + onContinue.accept(donationSession); + } catch (NumberFormatException e) { + errorLabel.setText("Please enter a valid number."); + } + } + + @FXML + private void handleBack() { + onBack.run(); + } + + @FXML + private void handleSelect50() { + amountField.setText("50"); + } + + @FXML + private void handleSelect100() { + amountField.setText("100"); + } + + @FXML + private void handleSelect500() { + amountField.setText("500"); + } + + @FXML + private void handleSelect1000() { + amountField.setText("1000"); + } +} diff --git a/src/main/java/ui/controller/donation/DonationConfirmationController.java b/src/main/java/ui/controller/donation/DonationConfirmationController.java new file mode 100644 index 0000000..5e2df03 --- /dev/null +++ b/src/main/java/ui/controller/donation/DonationConfirmationController.java @@ -0,0 +1,47 @@ +package ui.controller.donation; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import ui.DonationSession; +import ui.DonationSessionAware; +import ui.Page; +import ui.controller.NavigationAware; + +import java.util.function.Consumer; +/** + * Controller for the donation confirmation page. + * Displays the organization name and donation amount after a successful donation. + */ +public class DonationConfirmationController implements NavigationAware, DonationSessionAware { + + @FXML private Label orgLabel; + @FXML private Label amountLabel; + + private Consumer onNavigate; + private DonationSession donationSession; + + /** + * Sets the navigation callback. + * @param onNavigate the callback for navigating to a page + */ + @Override + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; + } + + /** + * Sets the donation session and updates the confirmation labels. + * @param session the completed donation session + */ + @Override + public void setDonationSession(DonationSession session) { + this.donationSession = session; + orgLabel.setText(session.getOrganization().getName()); + amountLabel.setText(session.getAmount() + " kr"); + } + + @FXML + private void handleBackToHome() { + onNavigate.accept(Page.HOME); + } +} diff --git a/src/main/java/ui/controller/donation/DonationFlowController.java b/src/main/java/ui/controller/donation/DonationFlowController.java new file mode 100644 index 0000000..6b70027 --- /dev/null +++ b/src/main/java/ui/controller/donation/DonationFlowController.java @@ -0,0 +1,144 @@ +package ui.controller.donation; + +import application.user.UserDonate; +import domain.organization.Organization; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.layout.BorderPane; +import ui.DonationSession; +import ui.Page; +import ui.controller.NavbarController; +import util.SessionManager; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.function.Consumer; + +/** + * Manages the multi-step donation flow. + * Handles navigation between donation steps and hides the navbar during the flow. + */ +public class DonationFlowController { + private final BorderPane mainPane; + private final NavbarController navbarController; + private DonationSession session; + private Consumer onNavigate; + private final UserDonate userDonate = new UserDonate(); + + /** + * Creates a new DonationFlowController. + * @param mainPane the main BorderPane to load donation steps into + * @param navbarController the navbar controller used to hide/show the navbar + */ + public DonationFlowController(BorderPane mainPane, NavbarController navbarController) { + this.mainPane = mainPane; + this.navbarController = navbarController; + } + + /** + * Starts the donation flow for the given organization. + * Navigates to the amount step if signed in, otherwise to the not logged in step. + * @param org the organization to donate to + * @throws IOException if an FXML file cannot be loaded + */ + public void start(Organization org) throws IOException { + session = new DonationSession(); + session.setOrganization(org); + navbarController.hideNavbar(); + + if (SessionManager.isSignedIn()) { + loadStep2(); + } else { + loadStep1(); + } + } + + // Loads the "not logged in" page + private void loadStep1() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/DonationNotLoggedIn.fxml")); + Parent root = loader.load(); + + DonationNotLoggedInController controller = loader.getController(); + controller.setDonationSession(session); + + controller.setOnNavigate(onNavigate); + mainPane.setCenter(root); + } + + // Loads the amount selection page + private void loadStep2() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/DonationAmount.fxml")); + Parent root = loader.load(); + + DonationAmountController controller = loader.getController(); + controller.setDonationSession(session); + + + controller.setOnContinue(updatedSession -> { + session = updatedSession; + try { + loadStep3(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + controller.setOnBack(() -> { + onNavigate.accept(Page.ORGANIZATIONS); + }); + + mainPane.setCenter(root); + } + + // Loads the payment information page + private void loadStep3() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/DonationPayment.fxml")); + Parent root = loader.load(); + + DonationPaymentController controller = loader.getController(); + controller.setDonationSession(session); + + + controller.setOnContinue(updatedSession -> { + session = updatedSession; + userDonate.execute( + SessionManager.getSignedInUser(), + session.getOrganization(), + new BigDecimal(session.getAmount()) + ); + try { + loadStep4(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + controller.setOnBack(() -> { + try { loadStep2(); } + catch (IOException e) { throw new RuntimeException(e); } + }); + + mainPane.setCenter(root); + } + + // Loads the confirmation page + private void loadStep4() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/DonationConfirmation.fxml")); + Parent root = loader.load(); + + DonationConfirmationController controller = loader.getController(); + controller.setDonationSession(session); + + controller.setOnNavigate(onNavigate); + + mainPane.setCenter(root); + } + + /** + * Sets the callback for navigating to a page after the donation flow ends. + * @param onNavigate the navigation callback + */ + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; + } +} \ No newline at end of file diff --git a/src/main/java/ui/controller/donation/DonationNotLoggedInController.java b/src/main/java/ui/controller/donation/DonationNotLoggedInController.java new file mode 100644 index 0000000..fcfe421 --- /dev/null +++ b/src/main/java/ui/controller/donation/DonationNotLoggedInController.java @@ -0,0 +1,55 @@ +package ui.controller.donation; + + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import ui.DonationSession; +import ui.DonationSessionAware; +import ui.Page; +import ui.controller.NavigationAware; + +import java.util.function.Consumer; + +/** + * Controller for the "not logged in" page in the donation flow. + * Shown when a user tries to donate without being signed in. + */ +public class DonationNotLoggedInController implements NavigationAware, DonationSessionAware { + + @FXML private Label orgLabel; + + private Consumer onNavigate; + private DonationSession donationSession; + + /** + * Sets the navigation callback. + * @param onNavigate the callback for navigating to a page + */ + @Override + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; + } + + /** + * Sets the donation session and updates the organization label. + * @param session the active donation session + */ + @Override + public void setDonationSession(DonationSession session) { + this.donationSession = session; + orgLabel.setText("You need an account to make a donation to " + donationSession.getOrganization().getName()); + } + + public void goToSignIn() { + onNavigate.accept(Page.SIGN_IN); + } + + public void goToRegister() { + onNavigate.accept(Page.REGISTER); + } + + public void handleBack() { + onNavigate.accept(Page.ORGANIZATIONS); + } +} + diff --git a/src/main/java/ui/controller/donation/DonationPaymentController.java b/src/main/java/ui/controller/donation/DonationPaymentController.java new file mode 100644 index 0000000..5c44b7c --- /dev/null +++ b/src/main/java/ui/controller/donation/DonationPaymentController.java @@ -0,0 +1,84 @@ +package ui.controller.donation; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import ui.DonationSession; +import ui.DonationSessionAware; +import java.util.function.Consumer; + +/** + * Controller for the payment information page in the donation flow. + * Handles validation of card number, expiry date and CVC. + */ +public class DonationPaymentController implements DonationSessionAware { + + @FXML private Label orgLabel; + @FXML private Label amountLabel; + @FXML private Label errorLabel; + @FXML private TextField cardNumberField; + @FXML private TextField expiryField; + @FXML private TextField cvcField; + + private DonationSession donationSession; + private Consumer onContinue; + private Runnable onBack; + + /** + * Sets the donation session and updates the organization and amount labels. + * @param donationSession the active donation session + */ + @Override + public void setDonationSession(DonationSession donationSession) { + this.donationSession = donationSession; + orgLabel.setText("Donating to: " + donationSession.getOrganization().getName()); + amountLabel.setText("Amount: " + donationSession.getAmount() + " kr"); + } + + /** + * Sets the callback to invoke when the user confirms the donation. + * @param onContinue the callback with the updated donation session + */ + public void setOnContinue(Consumer onContinue) { + this.onContinue = onContinue; + } + + + /** + * Validates payment information and continues to the confirmation step. + */ + @FXML + public void handleConfirmDonation() { + String cardNumber = cardNumberField.getText().trim().replace(" ", ""); + if (!cardNumber.matches("\\d{16}")) { + errorLabel.setText("Please enter a valid 16-digit card number."); + return; + } + String expiry = expiryField.getText().trim(); + if (!expiry.matches("\\d{2}/\\d{2}")) { + errorLabel.setText("Please enter expiry date in MM/YY format."); + return; + } + String cvc = cvcField.getText().trim(); + if (!cvc.matches("\\d{3}")) { + errorLabel.setText("Please enter a valid 3-digit CVC."); + return; + } + + onContinue.accept(donationSession); + } + + /** + * Sets the callback to invoke when the user navigates back. + * @param onBack the callback to run on back navigation + */ + public void setOnBack(Runnable onBack) { + this.onBack = onBack; + } + + @FXML + private void handleBack() { + onBack.run(); + } + +} diff --git a/src/main/java/util/SessionManager.java b/src/main/java/util/SessionManager.java new file mode 100644 index 0000000..3e40ea2 --- /dev/null +++ b/src/main/java/util/SessionManager.java @@ -0,0 +1,48 @@ +package util; + +import domain.user.User; + +/** + * Class that manages current user session for the application. + * Has methods that keep track of if a user is signed in or not. + * Only one user can be signed in at a time. + */ +public class SessionManager { + private static boolean SignedIn = false; + private static User currentUser = null; + + /** + * Signs in a user. + * Stores the user as the current session holder. + * @param user the user to sign in + */ + public static void signIn(User user) { + SignedIn = true; + currentUser = user; + } + + /** + * Signs out current user by clearing session. + */ + public static void signOut() { + SignedIn = false; + currentUser = null; + } + + /** + * Method returns boolean value on whether a user is signed in. + * @return false if user is not signed in, true if signed in. + */ + public static boolean isSignedIn() { + return SignedIn; + } + + + /** + * Method that returns the current user that the session contains. + * @return the current user + */ + public static User getSignedInUser() { + return currentUser; + } +} diff --git a/src/main/resources/css/Donation.css b/src/main/resources/css/Donation.css new file mode 100644 index 0000000..ec92d1f --- /dev/null +++ b/src/main/resources/css/Donation.css @@ -0,0 +1,99 @@ +.donation-container { + -fx-alignment: center; + -fx-padding: 40 15% 40 15%; + -fx-spacing: 20; +} + +.donation-title { + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-text-fill: #1a1a2e; +} + +.donation-subtitle { + -fx-font-size: 14px; + -fx-text-fill: #666666; +} + +.donation-label { + -fx-font-size: 13px; + -fx-text-fill: #555555; + -fx-font-weight: bold; +} + +.donation-field { + -fx-background-radius: 6; + -fx-border-radius: 6; + -fx-border-color: #c0c8dc; + -fx-border-width: 1.5; + -fx-padding: 8 12 8 12; + -fx-font-size: 13px; +} + +.amount-button { + -fx-background-color: #f0f4ff; + -fx-border-color: #1e3a8a; + -fx-border-width: 1.5; + -fx-border-radius: 6; + -fx-background-radius: 6; + -fx-text-fill: #1e3a8a; + -fx-font-weight: bold; + -fx-cursor: hand; + -fx-padding: 8 16 8 16; +} + +.amount-button:hover { + -fx-background-color: #dce6f7; + -fx-scale-x: 1.03; + -fx-scale-y: 1.03; +} + +.confirm-button { + -fx-background-color: #1e3a8a; + -fx-text-fill: white; + -fx-font-weight: bold; + -fx-background-radius: 6; + -fx-padding: 10 24 10 24; + -fx-cursor: hand; + -fx-font-size: 14px; +} + +.confirm-button:hover { + -fx-background-color: #2649a8; + -fx-scale-x: 1.03; + -fx-scale-y: 1.03; +} + +.confirm-button:pressed { + -fx-background-color: #162d6e; + -fx-scale-x: 0.97; + -fx-scale-y: 0.97; +} + +.back-button { + -fx-background-color: transparent; + -fx-border-color: #c0c8dc; + -fx-border-width: 1.5; + -fx-border-radius: 6; + -fx-background-radius: 6; + -fx-text-fill: #555555; + -fx-cursor: hand; + -fx-padding: 6 14 6 14; +} + +.back-button:hover { + -fx-background-color: #f0f0f0; + -fx-scale-x: 1.03; + -fx-scale-y: 1.03; +} + +.back-button:pressed { + -fx-background-color: #e0e0e0; + -fx-scale-x: 0.97; + -fx-scale-y: 0.97; +} + +.success-icon { + -fx-font-size: 48px; + -fx-text-fill: #2d6a4f; +} \ No newline at end of file diff --git a/src/main/resources/css/Global.css b/src/main/resources/css/Global.css new file mode 100644 index 0000000..e0253c6 --- /dev/null +++ b/src/main/resources/css/Global.css @@ -0,0 +1,189 @@ +.root { + -fx-background-color: #f5f5f5; + -fx-font-family: "Segoe UI"; +} + + +.nav-button:hover { + -fx-background-color: #bfc3c7; + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} +.nav-button:pressed { + -fx-background-color: #a5a8ab; + -fx-scale-x: 0.97; + -fx-scale-y: 0.97; +} + +.page-button { + -fx-background-color: #d9d9d9; + -fx-background-radius: 50; + -fx-font-size: 16px; + -fx-cursor: hand; + -fx-padding: 8px 20px; +} + +.page-button:hover { + -fx-background-color: #bfc3c7; + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +.page-button:pressed { + -fx-background-color: #a5a8ab; + -fx-scale-x: 0.97; + -fx-scale-y: 0.97; +} + +.page-button:disabled { + -fx-background-color: #eeeeee; + -fx-text-fill: #aaaaaa; + -fx-cursor: default; +} + +.page-label { + -fx-font-size: 14px; + -fx-text-fill: #555555; + -fx-padding: 8px 15px; +} + + +.page-title { + -fx-font-size: 36px; + -fx-font-weight: bold; + -fx-text-fill: #1a1a1a; +} + +.scroll-pane { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-border-width: 0; + -fx-padding: 0; +} + +.scroll-pane > .viewport { + -fx-background-color: transparent; +} + +.scroll-bar:vertical { + -fx-background-color: transparent; + -fx-pref-width: 10px; +} + +.scroll-bar:vertical .thumb { + -fx-background-color: #aaaaaa; + -fx-background-radius: 4px; +} + +.scroll-bar:vertical .thumb:hover { + -fx-background-color: #888888; + -fx-cursor: hand; +} + +.scroll-bar:vertical .track { + -fx-background-color: transparent; +} + +.scroll-bar .increment-button, +.scroll-bar .decrement-button { + -fx-pref-height: 0; + -fx-pref-width: 0; + -fx-opacity: 0; +} + +.section-title { + -fx-font-size: 22px; + -fx-text-fill: #1a1a1a; +} + + +/*NavigationBar*/ +.navbar { + -fx-background-color: #1e3a8a; + -fx-padding: 12 20 12 20; +} + +.nav-button { + -fx-background-color: transparent; + -fx-border-color: rgba(255,255,255,0.4); + -fx-border-radius: 20; + -fx-background-radius: 20; + -fx-text-fill: white; + -fx-font-size: 13px; + -fx-cursor: hand; + -fx-padding: 6 16 6 16; +} + +.nav-button-active { + -fx-background-color: white; + -fx-text-fill: #1a1a2e; + -fx-font-weight: bold; +} + +.logo-label { + -fx-font-size: 32px; + -fx-font-family: "Times New Roman"; + -fx-text-fill: white; + -fx-font-weight: bold; +} + +.confirm-button { + -fx-background-color: #1e3a8a; + -fx-text-fill: white; + -fx-font-weight: bold; + -fx-background-radius: 6; + -fx-padding: 10 24 10 24; + -fx-cursor: hand; + -fx-font-size: 14px; +} + +.confirm-button:hover { + -fx-background-color: #2649a8; + -fx-scale-x: 1.03; + -fx-scale-y: 1.03; +} + +.confirm-button:pressed { + -fx-background-color: #162d6e; + -fx-scale-x: 0.97; + -fx-scale-y: 0.97; +} + +.field-label { + -fx-font-size: 15px; + -fx-font-weight: bold; + -fx-text-fill: #1a1a2e; +} + +.input-field { + -fx-background-radius: 6; + -fx-border-radius: 6; + -fx-border-color: #c0c8dc; + -fx-border-width: 1.5; + -fx-padding: 8 12 8 12; + -fx-font-size: 13px; + -fx-pref-height: 40px; +} + +.secondary-button { + -fx-background-color: transparent; + -fx-border-color: #1e3a8a; + -fx-border-width: 1.5; + -fx-border-radius: 6; + -fx-background-radius: 6; + -fx-text-fill: #1e3a8a; + -fx-font-weight: bold; + -fx-padding: 10 24 10 24; + -fx-cursor: hand; + -fx-font-size: 14px; +} + +.secondary-button:hover { + -fx-background-color: #f0f4ff; +} + +.secondary-button:pressed { + -fx-background-color: #dce6f7; + -fx-scale-x: 0.97; + -fx-scale-y: 0.97; +} diff --git a/src/main/resources/css/Home.css b/src/main/resources/css/Home.css new file mode 100644 index 0000000..616c23c --- /dev/null +++ b/src/main/resources/css/Home.css @@ -0,0 +1,121 @@ +.hero-section { + -fx-background-color: #1e3a8a; + -fx-padding: 56 48 48 48; +} + +.hero-tag { + -fx-background-color: rgba(255,255,255,0.15); + -fx-text-fill: white; + -fx-font-size: 12px; + -fx-background-radius: 20; + -fx-padding: 4 12 4 12; +} + +.hero-title { + -fx-font-size: 28px; + -fx-text-fill: white; + -fx-font-weight: bold; +} + +.hero-subtitle { + -fx-font-size: 16px; + -fx-text-fill: white; + -fx-wrap-text: true; + -fx-max-width: 440; +} + +.hero-button-primary { + -fx-background-color: white; + -fx-text-fill: #1e3a8a; + -fx-font-weight: bold; + -fx-background-radius: 6; + -fx-padding: 10 22 10 22; + -fx-cursor: hand; +} + +.hero-button-secondary { + -fx-background-color: transparent; + -fx-text-fill: white; + -fx-border-color: rgba(255,255,255,0.6); + -fx-border-radius: 6; + -fx-background-radius: 6; + -fx-padding: 10 22 10 22; + -fx-cursor: hand; +} + +.hero-button-primary:hover { + -fx-background-color: #f0f0f0; + -fx-scale-x: 1.03; + -fx-scale-y: 1.03; +} + +.hero-button-primary:pressed { + -fx-background-color: #e0e0e0; + -fx-scale-x: 0.97; + -fx-scale-y: 0.97; +} + +.hero-button-secondary:hover { + -fx-background-color: rgba(255,255,255,0.15); + -fx-scale-x: 1.03; + -fx-scale-y: 1.03; +} + +.hero-button-secondary:pressed { + -fx-scale-x: 0.97; + -fx-scale-y: 0.97; +} + +.stats-stripe { + -fx-background-color: #162d6e; + -fx-padding: 20 48 20 48; +} + +.stats-number { + -fx-font-size: 24px; + -fx-font-weight: bold; + -fx-text-fill: white; +} + +.stats-label { + -fx-font-size: 14px; + -fx-text-fill: rgba(255,255,255,0.7); +} + +.features-section { + -fx-background-color: #f8faff; + -fx-padding: 32 48 48 48; +} + +.features-title { + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-text-fill: #1a1a2e; +} + +.features-grid { + -fx-spacing: 16; +} + +.feature-card { + -fx-background-color: white; + -fx-border-color: #dde3f0; + -fx-border-width: 1; + -fx-border-radius: 10; + -fx-background-radius: 10; + -fx-min-height: 120; + -fx-padding: 20; + -fx-spacing: 8; +} + +.feature-card-title { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: #1a1a2e; +} + +.feature-card-desc { + -fx-font-size: 13px; + -fx-text-fill: #666666; + -fx-wrap-text: true; +} \ No newline at end of file diff --git a/src/main/resources/css/MyProfile.css b/src/main/resources/css/MyProfile.css new file mode 100644 index 0000000..a9b04ec --- /dev/null +++ b/src/main/resources/css/MyProfile.css @@ -0,0 +1,4 @@ + +.profile-label{ + -fx-font-size: 16px; +} \ No newline at end of file diff --git a/src/main/resources/css/OrganizationView.css b/src/main/resources/css/OrganizationView.css new file mode 100644 index 0000000..18c0a90 --- /dev/null +++ b/src/main/resources/css/OrganizationView.css @@ -0,0 +1,117 @@ +.org-card { + -fx-background-color: #e8e8e8; + -fx-background-radius: 12px; + -fx-padding: 20px; + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.08), 8, 0, 0, 3); +} + +.org-card:hover { + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.15), 12, 0, 0, 5); + -fx-cursor: hand; +} + +.org-card:pressed { + -fx-scale-x: 0.98; + -fx-scale-y: 0.98; + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.05), 4, 0, 0, 2); +} + +.org-name { + -fx-font-size: 20px; +} + +.donate-button { + -fx-background-color: #1e3a8a; + -fx-text-fill: white; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 8px; + -fx-padding: 10px; + -fx-cursor: hand; +} + +.donate-button:hover { + -fx-background-color: #2649a8; + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} +.donate-button:pressed { + -fx-background-color: #162d6e; + -fx-scale-x: 0.97; + -fx-scale-y: 0.97; +} + + +.page-subtitle { + -fx-font-size: 14px; + -fx-text-fill: #555555; +} + + + +.search-field { + -fx-background-color: white; + -fx-border-color: #e0e0e0; + -fx-border-radius: 8px; + -fx-background-radius: 8px; + -fx-padding: 12px 15px; + -fx-font-size: 14px; + -fx-prompt-text-fill: #aaaaaa; +} + +.search-field:focused { + -fx-border-color: #3a86ff; +} + + +.detail-header { + -fx-background-color: #1e3a8a; + -fx-padding: 15 20 15 20; + -fx-alignment: CENTER_LEFT; + -fx-spacing: 10; +} + +.detail-title { + -fx-font-size: 20px; + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-hgrow: always; +} + +.detail-close-button { + -fx-background-color: transparent; + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-cursor: hand; + -fx-border-color: transparent; +} + +.detail-close-button:hover { + -fx-background-color: rgba(255,255,255,0.2); + -fx-background-radius: 50; +} + +.detail-content { + -fx-padding: 20; + -fx-spacing: 15; + -fx-background-color: white; +} + +.detail-verified { + -fx-background-color: #e8f5e9; + -fx-text-fill: #2d6a4f; + -fx-padding: 4 10 4 10; + -fx-background-radius: 4; + -fx-font-size: 12px; +} + +.detail-orgnr { + -fx-font-size: 12px; + -fx-text-fill: #666666; +} + +.detail-description { + -fx-font-size: 13px; + -fx-text-fill: #333333; + -fx-wrap-text: true; +} \ No newline at end of file diff --git a/src/main/resources/ui/home.fxml b/src/main/resources/ui/home.fxml deleted file mode 100644 index 5028391..0000000 --- a/src/main/resources/ui/home.fxml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - + + + + + + + + + diff --git a/src/main/resources/view/Navbar.fxml b/src/main/resources/view/Navbar.fxml new file mode 100644 index 0000000..7b61d6f --- /dev/null +++ b/src/main/resources/view/Navbar.fxml @@ -0,0 +1,19 @@ + + + + + + + + + +