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/ui/signIn.fxml b/src/main/resources/ui/signIn.fxml
deleted file mode 100644
index 5bb59d4..0000000
--- a/src/main/resources/ui/signIn.fxml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/main/resources/view/DonationAmount.fxml b/src/main/resources/view/DonationAmount.fxml
new file mode 100644
index 0000000..8098a23
--- /dev/null
+++ b/src/main/resources/view/DonationAmount.fxml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/DonationConfirmation.fxml b/src/main/resources/view/DonationConfirmation.fxml
new file mode 100644
index 0000000..48994ec
--- /dev/null
+++ b/src/main/resources/view/DonationConfirmation.fxml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/DonationNotLoggedIn.fxml b/src/main/resources/view/DonationNotLoggedIn.fxml
new file mode 100644
index 0000000..a144080
--- /dev/null
+++ b/src/main/resources/view/DonationNotLoggedIn.fxml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/DonationPayment.fxml b/src/main/resources/view/DonationPayment.fxml
new file mode 100644
index 0000000..de8fd98
--- /dev/null
+++ b/src/main/resources/view/DonationPayment.fxml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/Home.fxml b/src/main/resources/view/Home.fxml
new file mode 100644
index 0000000..0730328
--- /dev/null
+++ b/src/main/resources/view/Home.fxml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/MainView.fxml b/src/main/resources/view/MainView.fxml
new file mode 100644
index 0000000..671a97b
--- /dev/null
+++ b/src/main/resources/view/MainView.fxml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/MyProfile.fxml b/src/main/resources/view/MyProfile.fxml
new file mode 100644
index 0000000..8010e42
--- /dev/null
+++ b/src/main/resources/view/MyProfile.fxml
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/OrganizationDetail.fxml b/src/main/resources/view/OrganizationDetail.fxml
new file mode 100644
index 0000000..b21e906
--- /dev/null
+++ b/src/main/resources/view/OrganizationDetail.fxml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/OrganizationsView.fxml b/src/main/resources/view/OrganizationsView.fxml
new file mode 100644
index 0000000..c755d78
--- /dev/null
+++ b/src/main/resources/view/OrganizationsView.fxml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/Register.fxml b/src/main/resources/view/Register.fxml
new file mode 100644
index 0000000..de4a9d3
--- /dev/null
+++ b/src/main/resources/view/Register.fxml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/SignIn.fxml b/src/main/resources/view/SignIn.fxml
new file mode 100644
index 0000000..38e5f2a
--- /dev/null
+++ b/src/main/resources/view/SignIn.fxml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/application/user/UserLoginTest.java b/src/test/java/application/user/UserLoginTest.java
new file mode 100644
index 0000000..20caec1
--- /dev/null
+++ b/src/test/java/application/user/UserLoginTest.java
@@ -0,0 +1,164 @@
+package application.user;
+
+import application.security.PasswordHasher;
+import domain.user.User;
+import org.junit.jupiter.api.Test;
+import persistence.dao.UserRepository;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class UserLoginTest {
+
+ @Test
+ void executeReturnsUserWhenCredentialsAreValid() {
+ User user = new User("testuser", "12345678", "hashed-secret", "test@example.com");
+ PasswordHasher hasher = new FakePasswordHasher("hashed-secret");
+ UserRepository repo = new FakeUserRepository(Optional.of(user));
+
+ UserLogin userLogin = new UserLogin(hasher, repo);
+
+ User result = userLogin.execute("testuser", "secret");
+
+ assertSame(user, result);
+
+ }
+
+ @Test
+ void executeThrowsWhenLoginDoesNotExist() {
+ PasswordHasher hasher = new FakePasswordHasher("hashed-secret");
+ UserRepository repo = new FakeUserRepository(Optional.empty());
+
+ UserLogin userLogin = new UserLogin(hasher, repo);
+
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> userLogin.execute("missing-user", "secret")
+ );
+
+ assertEquals("Invalid credentials", exception.getMessage());
+ }
+
+ @Test
+ void executeThrowsWhenPasswordIsWrong() {
+ User user = new User("testuser", "12345678", "stored-hash", "test@example.com");
+ PasswordHasher hasher = new FakePasswordHasher("different-hash");
+ UserRepository repo = new FakeUserRepository(Optional.of(user));
+
+ UserLogin userLogin = new UserLogin(hasher, repo);
+
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> userLogin.execute("testuser", "wrong-password")
+ );
+
+ assertEquals("Invalid password", exception.getMessage());
+ }
+
+ @Test
+ void executeUsesNormalizedLoginBeforeLookup() {
+ User user = new User("testuser", "12345678", "hashed-secret", "test@example.com");
+ RecordingUserRepository repo = new RecordingUserRepository(Optional.of(user));
+ PasswordHasher hasher = new FakePasswordHasher("hashed-secret");
+
+ UserLogin userLogin = new UserLogin(hasher, repo);
+
+ userLogin.execute(" TestUser ", "secret");
+
+ assertEquals("testuser", repo.lastLoginUsed);
+ }
+
+ private static class FakePasswordHasher implements PasswordHasher{
+ private final String hashToReturn;
+ private FakePasswordHasher(String hashToReturn) {
+ this.hashToReturn = hashToReturn;
+ }
+
+ @Override
+ public String hash(String password) {
+ return hashToReturn;
+ }
+ }
+
+ private static class FakeUserRepository implements UserRepository {
+ private final Optional userToReturn;
+
+ private FakeUserRepository(Optional userToReturn) {
+ this.userToReturn = userToReturn;
+ }
+
+ @Override
+ public Optional findByUsername(String username){
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional findByEmail(String email){
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional findByLogin(String login) {
+ return userToReturn;
+ }
+
+ @Override
+ public boolean existsByEmail(String email) {
+ return false;
+ }
+
+ @Override
+ public boolean existsByUsername(String username) {
+ return false;
+ }
+
+ @Override
+ public void insert(User user) {
+ }
+ }
+
+ private static class RecordingUserRepository implements UserRepository {
+ private final Optional userToReturn;
+ private String lastLoginUsed;
+
+ private RecordingUserRepository(Optional userToReturn) {
+ this.userToReturn = userToReturn;
+ }
+
+ @Override
+ public Optional findByUsername(String username) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional findByEmail(String email) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional findByLogin(String login) {
+ lastLoginUsed = login;
+ return userToReturn;
+ }
+
+ @Override
+ public boolean existsByEmail(String email) {
+ return false;
+ }
+
+ @Override
+ public boolean existsByUsername(String username) {
+ return false;
+ }
+
+ @Override
+ public void insert(User user) {
+ }
+
+
+ }
+
+
+}
+
diff --git a/src/test/java/application/user/UserRegisterTest.java b/src/test/java/application/user/UserRegisterTest.java
new file mode 100644
index 0000000..a4807cd
--- /dev/null
+++ b/src/test/java/application/user/UserRegisterTest.java
@@ -0,0 +1,170 @@
+package application.user;
+
+import application.security.PasswordHasher;
+import domain.user.User;
+import org.junit.jupiter.api.Test;
+import persistence.dao.UserRepository;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class UserRegisterTest {
+
+ @Test
+ void executeInsertsUserWhenInputIsValid() {
+ RecordingUserRepository repo = new RecordingUserRepository();
+ PasswordHasher hasher = new FakePasswordHasher("hashed-password");
+ UserRegister userRegister = new UserRegister(hasher, repo);
+
+ userRegister.execute("testuser", "12345678", "secret", "test@example.com");
+
+ assertNotNull(repo.insertedUser);
+ assertEquals("testuser", repo.insertedUser.getUsername());
+ assertEquals("12345678", repo.insertedUser.getPhoneNumber());
+ assertEquals("hashed-password", repo.insertedUser.getPassword());
+ assertEquals("test@example.com", repo.insertedUser.getEmail());
+ }
+
+
+ @Test
+ void executeNormalizesUsernameEmailAndPhoneBeforeInsert() {
+ RecordingUserRepository repo = new RecordingUserRepository();
+ PasswordHasher hasher = new FakePasswordHasher("hashed-password");
+ UserRegister userRegister = new UserRegister(hasher, repo);
+
+ userRegister.execute(" testuser ", " 12345678 ", "secret", " TEST@EXAMPLE.COM ");
+
+ assertNotNull(repo.insertedUser);
+ assertEquals("testuser", repo.insertedUser.getUsername());
+ assertEquals("12345678", repo.insertedUser.getPhoneNumber());
+ assertEquals("test@example.com", repo.insertedUser.getEmail());
+ }
+
+
+ @Test
+ void executeThrowsWhenEmailAlreadyExists() {
+ RecordingUserRepository repo = new RecordingUserRepository();
+ repo.emailExists = true;
+ PasswordHasher hasher = new FakePasswordHasher("hashed-password");
+ UserRegister userRegister = new UserRegister(hasher, repo);
+
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> userRegister.execute("testuser", "12345678", "secret", "test@example.com")
+ );
+
+ assertEquals("Email already in use", exception.getMessage());
+ assertNull(repo.insertedUser);
+ }
+
+ @Test
+ void executeThrowsWhenUsernameAlreadyExists() {
+ RecordingUserRepository repo = new RecordingUserRepository();
+ repo.usernameExists = true;
+ PasswordHasher hasher = new FakePasswordHasher("hashed-password");
+ UserRegister userRegister = new UserRegister(hasher, repo);
+
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> userRegister.execute("testuser", "12345678", "secret", "test@example.com")
+ );
+
+ assertEquals("This username is taken", exception.getMessage());
+ assertNull(repo.insertedUser);
+ }
+
+ @Test
+ void executeHashesPasswordBeforeSavingUser() {
+ RecordingUserRepository repo = new RecordingUserRepository();
+ RecordingPasswordHasher hasher = new RecordingPasswordHasher("hashed-password");
+ UserRegister userRegister = new UserRegister(hasher, repo);
+
+ userRegister.execute("testuser", "12345678", "secret", "test@example.com");
+
+ assertEquals("secret", hasher.lastPasswordInput);
+ assertNotNull(repo.insertedUser);
+ assertEquals("hashed-password", repo.insertedUser.getPassword());
+ }
+
+ @Test
+ void executePropagatesDomainValidationErrors() {
+ RecordingUserRepository repo = new RecordingUserRepository();
+ PasswordHasher hasher = new FakePasswordHasher("hashed-password");
+ UserRegister userRegister = new UserRegister(hasher, repo);
+
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> userRegister.execute("testuser", "1234", "secret", "test@example.com")
+ );
+
+ assertEquals("Fill in a phonenumber with 8 digits", exception.getMessage());
+ assertNull(repo.insertedUser);
+ }
+
+ private static class FakePasswordHasher implements PasswordHasher {
+ private final String hashToReturn;
+
+ private FakePasswordHasher(String hashToReturn) {
+ this.hashToReturn = hashToReturn;
+ }
+
+ @Override
+ public String hash(String password) {
+ return hashToReturn;
+ }
+ }
+
+
+ private static class RecordingPasswordHasher implements PasswordHasher {
+ private final String hashToReturn;
+ private String lastPasswordInput;
+
+ private RecordingPasswordHasher(String hashToReturn) {
+ this.hashToReturn = hashToReturn;
+ }
+
+ @Override
+ public String hash(String password) {
+ lastPasswordInput = password;
+ return hashToReturn;
+ }
+ }
+
+ private static class RecordingUserRepository implements UserRepository {
+ private boolean emailExists;
+ private boolean usernameExists;
+ private User insertedUser;
+
+ @Override
+ public Optional findByUsername(String username) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional findByEmail(String email) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional findByLogin(String login) {
+ return Optional.empty();
+ }
+
+
+ @Override
+ public boolean existsByEmail(String email) {
+ return emailExists;
+ }
+
+ @Override
+ public boolean existsByUsername(String username) {
+ return usernameExists;
+ }
+
+ @Override
+ public void insert(User user) {
+ insertedUser = user;
+ }
+ }
+}
diff --git a/src/test/java/application/user/UserStatisticsTest.java b/src/test/java/application/user/UserStatisticsTest.java
new file mode 100644
index 0000000..492ac43
--- /dev/null
+++ b/src/test/java/application/user/UserStatisticsTest.java
@@ -0,0 +1,128 @@
+package application.user;
+
+import domain.user.User;
+import org.junit.jupiter.api.Test;
+import persistence.dao.DonationDao;
+
+import java.lang.reflect.Field;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class UserStatisticsTest {
+
+ @Test
+ void userFavoriteOrganization_returnsListFromDonationDao() throws Exception {
+ FakeDonationDao fakeDao = new FakeDonationDao();
+ fakeDao.favoriteOrganizationsToReturn = List.of(
+ "Org1 (3 donasjoner)",
+ "Org2 (2 donasjoner)"
+ );
+
+ UserStatistics statistics = createStatisticsWithFakeDao(fakeDao);
+ User user = createUserWithId(42L);
+
+ List result = statistics.userFavoriteOrganization(user);
+
+ assertEquals(List.of("Org1 (3 donasjoner)", "Org2 (2 donasjoner)"), result);
+ assertEquals(42L, fakeDao.lastUserIdForFavorites);
+ }
+
+ @Test
+ void userDonations_returnsListFromDonationDao() throws Exception {
+ FakeDonationDao fakeDao = new FakeDonationDao();
+ fakeDao.userDonationsToReturn = List.of(
+ "Amount: 100, Date: 2026-04-15, Organization: Org1",
+ "Amount: 50, Date: 2026-04-16, Organization: Org2"
+ );
+
+ UserStatistics statistics = createStatisticsWithFakeDao(fakeDao);
+ User user = createUserWithId(7L);
+
+ List result = statistics.userDonations(user);
+
+ assertEquals(List.of(
+ "Amount: 100, Date: 2026-04-15, Organization: Org1",
+ "Amount: 50, Date: 2026-04-16, Organization: Org2"
+ ), result);
+ assertEquals(7L, fakeDao.lastUserIdForDonations);
+ }
+
+ @Test
+ void userTotalDonationAmount_returnsValueFromDonationDao() throws Exception {
+ FakeDonationDao fakeDao = new FakeDonationDao();
+ fakeDao.totalDonationAmountToReturn = "250.00";
+
+ UserStatistics statistics = createStatisticsWithFakeDao(fakeDao);
+ User user = createUserWithId(15L);
+
+ String result = statistics.userTotalDonationAmount(user);
+
+ assertEquals("250.00", result);
+ assertEquals(15L, fakeDao.lastUserIdForTotalAmount);
+ }
+
+ @Test
+ void getTotalDonationsMade_returnsValueFromDonationDao() throws Exception {
+ FakeDonationDao fakeDao = new FakeDonationDao();
+ fakeDao.totalDonationsMadeToReturn = "5";
+
+ UserStatistics statistics = createStatisticsWithFakeDao(fakeDao);
+ User user = createUserWithId(99L);
+
+ String result = statistics.getTotalDonationsMade(user);
+
+ assertEquals("5", result);
+ assertEquals(99L, fakeDao.lastUserIdForTotalCount);
+ }
+
+ private UserStatistics createStatisticsWithFakeDao(FakeDonationDao fakeDao) throws Exception {
+ UserStatistics statistics = new UserStatistics();
+ Field field = UserStatistics.class.getDeclaredField("donationDao");
+ field.setAccessible(true);
+ field.set(statistics, fakeDao);
+ return statistics;
+ }
+
+ private User createUserWithId(long id) {
+ User user = new User("testuser", "12345678", "password", "test@example.com");
+ user.setId(id);
+ return user;
+ }
+
+ private static class FakeDonationDao extends DonationDao {
+ List favoriteOrganizationsToReturn = List.of();
+ List userDonationsToReturn = List.of();
+ String totalDonationAmountToReturn = "0";
+ String totalDonationsMadeToReturn = "0";
+
+ long lastUserIdForFavorites;
+ long lastUserIdForDonations;
+ long lastUserIdForTotalAmount;
+ long lastUserIdForTotalCount;
+
+ @Override
+ public List getFavoriteOrganization(long userId) {
+ lastUserIdForFavorites = userId;
+ return favoriteOrganizationsToReturn;
+ }
+
+ @Override
+ public List getUserDonations(long userId) {
+ lastUserIdForDonations = userId;
+ return userDonationsToReturn;
+ }
+
+ @Override
+ public String getTotalDonationAmount(long userId) {
+ lastUserIdForTotalAmount = userId;
+ return totalDonationAmountToReturn;
+ }
+
+ @Override
+ public String getTotalDonationsMade(long userId) {
+ lastUserIdForTotalCount = userId;
+ return totalDonationsMadeToReturn;
+ }
+ }
+}
diff --git a/src/test/java/domain/DonationTest.java b/src/test/java/domain/DonationTest.java
index bb9c875..57c5be1 100644
--- a/src/test/java/domain/DonationTest.java
+++ b/src/test/java/domain/DonationTest.java
@@ -1,5 +1,83 @@
package domain;
+import domain.donation.Donation;
+import domain.organization.Organization;
+import domain.user.User;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
class DonationTest {
+ private User testUser;
+ private Organization testOrganization;
+ private BigDecimal testAmount = new BigDecimal("1500");
+
+ @BeforeEach
+ public void init() {
+ testUser = new User("testUser", "90909090", "password", "test@email.com");
+ testOrganization = new Organization();
+ testOrganization.setOrgNumber("123456789");
+ testOrganization.setName("Test Organization");
+ testOrganization.setStatus("approved");
+ testOrganization.setUrl("https://test.com");
+ }
+
+ @Test
+ public void validDonation_createsSuccessfully() {
+ Donation donation = new Donation(testAmount, testUser, testOrganization);
+ }
+
+ @Test
+ public void nullUser_throwsNullPointerException() {
+ NullPointerException exception = assertThrows(NullPointerException.class,
+ () -> new Donation(testAmount, null, testOrganization));
+ assertEquals("User cannot be null", exception.getMessage());
+ }
+
+ @Test
+ public void nullOrganization_throwsNullPointerException() {
+ NullPointerException exception = assertThrows(NullPointerException.class,
+ () -> new Donation(testAmount, testUser, null));
+ assertEquals("Organization cannot be null", exception.getMessage());
+ }
+
+ @Test
+ public void negativeAmount_throwsIllegalArgumentException() {
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> new Donation(new BigDecimal("-23"), testUser, testOrganization));
+ assertEquals("Amount must be greater than 0", exception.getMessage());
+ }
+
+ @Test
+ public void getAmount_returnsCorrectValue() {
+ Donation donation = new Donation(testAmount, testUser, testOrganization);
+ assertEquals(testAmount, donation.getAmount());
+ }
+
+ @Test
+ public void getDateTime_returnsCurrentTime() {
+ LocalDateTime now = LocalDateTime.now();
+ Donation donation = new Donation(testAmount, testUser, testOrganization);
+ assertTrue(Duration.between(now, donation.getDateTime()).abs().toMillis() < 1000);
+ }
+
+ @Test
+ public void getUser_returnsCorrectUser() {
+ Donation donation = new Donation(testAmount, testUser, testOrganization);
+ assertEquals(testUser, donation.getUser());
+ }
+
+ @Test
+ public void getOrganization_returnsCorrectOrganization() {
+ Donation donation = new Donation(testAmount, testUser, testOrganization);
+ assertEquals(testOrganization, donation.getOrganization());
+ }
}
\ No newline at end of file
diff --git a/src/test/java/domain/OrganizationTest.java b/src/test/java/domain/OrganizationTest.java
index 815aa53..b15d2da 100644
--- a/src/test/java/domain/OrganizationTest.java
+++ b/src/test/java/domain/OrganizationTest.java
@@ -1,5 +1,51 @@
package domain;
+import domain.organization.Organization;
+import domain.user.User;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
class OrganizationTest {
+ private Organization organization;
+
+ @BeforeEach
+ public void init() {
+ organization = new Organization();
+ organization.setOrgNumber("123456789");
+ organization.setName("Test Organization");
+ organization.setStatus("approved");
+ organization.setUrl("https://test.com");
+ organization.setPreApproved(true);
+ }
+
+ @Test
+ public void getOrgNumber_returnsCorrectValue() {
+ assertEquals("123456789", organization.getOrgNumber());
+ }
+
+ @Test
+ public void getName_returnsCorrectValue() {
+ assertEquals("Test Organization", organization.getName());
+ }
+
+ @Test
+ public void getStatus_returnsCorrectValue() {
+ assertEquals("approved", organization.getStatus());
+ }
+
+ @Test
+ public void getUrl_returnsCorrectValue() {
+ assertEquals("https://test.com", organization.getUrl());
+ }
+
+ @Test
+ public void isPreApproved_returnsTrue() {
+ assertTrue(organization.isPreApproved());
+ }
+
}
\ No newline at end of file
diff --git a/src/test/java/domain/UserTest.java b/src/test/java/domain/UserTest.java
index c21aa90..2aa440c 100644
--- a/src/test/java/domain/UserTest.java
+++ b/src/test/java/domain/UserTest.java
@@ -1,5 +1,142 @@
package domain;
+import domain.user.User;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
class UserTest {
+ private String testUserName = "testUser";
+ private String testPhoneNumber = "90909090";
+ private String testEmail = "test@email.com";
+ private final String testPassword = "password";
+
+ //Positive tests
+ @Test
+ public void userTestPositive(){
+ User User = new User(testUserName, testPhoneNumber, testPassword, testEmail);
+ }
+
+ //Negative tests
+ @Test
+ public void userTestUserNameBlank(){
+ testUserName = "";
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> new User(testUserName, testPhoneNumber, testPassword, testEmail));
+
+ assertEquals("Username has to be filled in", exception.getMessage());
+ }
+
+ @Test
+ public void userTestUserNameNull(){
+ testUserName = null;
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> new User(testUserName, testPhoneNumber, testPassword, testEmail));
+
+ assertEquals("Username has to be filled in", exception.getMessage());
+ }
+
+ @Test
+ public void userTestPhoneNumberBlank(){
+ testPhoneNumber = "";
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> new User(testUserName, testPhoneNumber, testPassword, testEmail));
+
+ assertEquals("Phonenumber has to be filled in", exception.getMessage());
+ }
+
+ @Test
+ public void userTestPhoneNumberNull(){
+ testPhoneNumber = null;
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> new User(testUserName, testPhoneNumber, testPassword, testEmail));
+
+ assertEquals("Phonenumber has to be filled in", exception.getMessage());
+ }
+
+ @Test
+ public void userTestPhoneNumberShort() {
+ testPhoneNumber = "909090";
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> new User(testUserName, testPhoneNumber, testPassword, testEmail));
+
+ assertEquals("Fill in a phonenumber with 8 digits", exception.getMessage());
+ }
+
+ @Test
+ public void userTestEmailBlank(){
+ testEmail = "";
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> new User(testUserName, testPhoneNumber, testPassword, testEmail));
+
+ assertEquals("Invalid email address: ", exception.getMessage());
+ }
+
+ @Test
+ public void userTestEmailNull(){
+ testEmail = null;
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> new User(testUserName, testPhoneNumber, testPassword, testEmail));
+
+ assertEquals("Invalid email address: null", exception.getMessage());
+ }
+
+ //Getter tests
+ @Test
+ public void userTestUserNameGet(){
+ User User = new User(testUserName, testPhoneNumber, testPassword, testEmail);
+ assertEquals(User.getUsername(), testUserName);
+ }
+
+ @Test
+ public void userTestPhoneNumberGet(){
+ User User = new User(testUserName, testPhoneNumber, testPassword, testEmail);
+ assertEquals(User.getPhoneNumber(), testPhoneNumber);
+ }
+
+ @Test
+ public void userTestEmailGet(){
+ User User = new User(testUserName, testPhoneNumber, testPassword, testEmail);
+ assertEquals(User.getEmail(), testEmail);
+ }
+
+ @Test
+ public void userTestPasswordGet(){
+ User User = new User(testUserName, testPhoneNumber, testPassword, testEmail);
+ assertEquals(User.getPassword(), testPassword);
+ }
+
+ //Setter tests
+ @Test
+ public void userTestPasswordSet(){
+ User User = new User(testUserName, testPhoneNumber, testPassword, testEmail);
+ String newPassword = "NewPassoword";
+ User.setPassword(newPassword);
+ assertEquals(newPassword, User.getPassword());
+ }
+
+ @Test
+ public void userTestUserNameSet(){
+ User User = new User(testUserName, testPhoneNumber, testPassword, testEmail);
+ String newUserName = "NewUsername";
+ User.setUsername(newUserName);
+ assertEquals(newUserName, User.getUsername());
+ }
+
+ @Test
+ public void userTestPhoneNumberSet(){
+ User User = new User(testUserName, testPhoneNumber, testPassword, testEmail);
+ String newPhoneNumber = "12345678";
+ User.setPhoneNumber(newPhoneNumber);
+ assertEquals(newPhoneNumber, User.getPhoneNumber());
+ }
+
+ @Test
+ public void userTestEmailSet() {
+ User user = new User(testUserName, testPhoneNumber, testPassword, testEmail);
+ String newEmail = "new@email.com";
+ user.setEmail(newEmail);
+ assertEquals(newEmail, user.getEmail());
+ }
}
\ No newline at end of file
diff --git a/src/test/java/integration/security/Sha256PasswordHasherTest.java b/src/test/java/integration/security/Sha256PasswordHasherTest.java
new file mode 100644
index 0000000..67b360e
--- /dev/null
+++ b/src/test/java/integration/security/Sha256PasswordHasherTest.java
@@ -0,0 +1,26 @@
+package integration.security;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class Sha256PasswordHasherTest {
+ @Test
+ void hashReturnsExpectedSha256ForKnownInput() {
+ Sha256PasswordHasher hasher = new Sha256PasswordHasher();
+
+ String result = hasher.hash("password");
+
+ assertEquals("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", result);
+ }
+
+ @Test
+ void hashReturns64CharacterLowercaseHexString() {
+ Sha256PasswordHasher hasher = new Sha256PasswordHasher();
+
+ String result = hasher.hash("secret");
+
+ assertEquals(64, result.length());
+ assertTrue(result.matches("[0-9a-f]{64}"));
+ }
+}