From 69dc43b2f836450a1d41a3f54aea3738d6b26168 Mon Sep 17 00:00:00 2001 From: Marius Klepp Date: Fri, 10 Apr 2026 18:36:35 +0200 Subject: [PATCH 01/13] feat: add multi-step donation flow with session state --- .../java/application/user/UserDonate.java | 21 +++ src/main/java/persistence/DonationDao.java | 11 +- src/main/java/ui/DonationSession.java | 52 ++++++++ src/main/java/ui/DonationSessionAware.java | 5 + src/main/java/ui/Page.java | 9 +- .../donation/DonationAmountController.java | 76 +++++++++++ .../DonationConfirmationController.java | 36 ++++++ .../donation/DonationFlowController.java | 120 ++++++++++++++++++ .../donation/DonationPaymentController.java | 65 ++++++++++ .../donation/DonationUserInfoController.java | 109 ++++++++++++++++ src/main/resources/css/Donation.css | 114 +++++++++++++++++ src/main/resources/view/DonationAmount.fxml | 46 +++++++ .../resources/view/DonationConfirmation.fxml | 21 +++ src/main/resources/view/DonationPayment.fxml | 58 +++++++++ src/main/resources/view/DonationUserInfo.fxml | 75 +++++++++++ 15 files changed, 807 insertions(+), 11 deletions(-) create mode 100644 src/main/java/application/user/UserDonate.java create mode 100644 src/main/java/ui/DonationSession.java create mode 100644 src/main/java/ui/DonationSessionAware.java create mode 100644 src/main/java/ui/controller/donation/DonationAmountController.java create mode 100644 src/main/java/ui/controller/donation/DonationConfirmationController.java create mode 100644 src/main/java/ui/controller/donation/DonationFlowController.java create mode 100644 src/main/java/ui/controller/donation/DonationPaymentController.java create mode 100644 src/main/java/ui/controller/donation/DonationUserInfoController.java create mode 100644 src/main/resources/css/Donation.css create mode 100644 src/main/resources/view/DonationAmount.fxml create mode 100644 src/main/resources/view/DonationConfirmation.fxml create mode 100644 src/main/resources/view/DonationPayment.fxml create mode 100644 src/main/resources/view/DonationUserInfo.fxml diff --git a/src/main/java/application/user/UserDonate.java b/src/main/java/application/user/UserDonate.java new file mode 100644 index 0000000..bf8f187 --- /dev/null +++ b/src/main/java/application/user/UserDonate.java @@ -0,0 +1,21 @@ +package application.user; + +import domain.donation.Donation; +import domain.organization.Organization; +import domain.user.User; +import persistence.DonationDao; + +import java.math.BigDecimal; + +public class UserDonate { + private final DonationDao donationDao; + + public UserDonate() { + this.donationDao = new DonationDao(); + } + + 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/persistence/DonationDao.java b/src/main/java/persistence/DonationDao.java index d467e99..2d8c0e3 100644 --- a/src/main/java/persistence/DonationDao.java +++ b/src/main/java/persistence/DonationDao.java @@ -29,7 +29,7 @@ public class DonationDao { */ public void insert(Donation donation) { String sql = "INSERT INTO donation(amount, donation_date" + - ", user_id, organization_id) VALUES(?, ?, ?, ?)"; + ", user_id, cause_id) VALUES(?, ?, ?, ?)"; try (Connection conn = Database.getConnection()) { PreparedStatement stmt = conn.prepareStatement(sql, @@ -272,11 +272,4 @@ private String mapRowFavorite(ResultSet rs) throws SQLException { return rs.getString("name") + " (" + rs.getInt("donation_count") + " donasjoner)"; } - - - - - - - -} \ No newline at end of file +} diff --git a/src/main/java/ui/DonationSession.java b/src/main/java/ui/DonationSession.java new file mode 100644 index 0000000..991f91a --- /dev/null +++ b/src/main/java/ui/DonationSession.java @@ -0,0 +1,52 @@ +package ui; + +import domain.organization.Organization; + +public class DonationSession { + private Organization organization; + private int amount; + private String name; + private String email; + private String phone; + + public Organization getOrganization() { + return organization; + } + + public int getAmount() { + return amount; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public String getPhone() { + return phone; + } + + public void setOrganization(Organization organization) { + this.organization = organization; + } + + public void setAmount(int amount) { + this.amount = amount; + } + + public void setName(String name) { + this.name = name; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setPhone(String phone) { + this.phone = phone; + } +} + diff --git a/src/main/java/ui/DonationSessionAware.java b/src/main/java/ui/DonationSessionAware.java new file mode 100644 index 0000000..8ceab22 --- /dev/null +++ b/src/main/java/ui/DonationSessionAware.java @@ -0,0 +1,5 @@ +package ui; + +public interface DonationSessionAware { + void setDonationSession(DonationSession session); +} diff --git a/src/main/java/ui/Page.java b/src/main/java/ui/Page.java index e2066c8..a7878b9 100644 --- a/src/main/java/ui/Page.java +++ b/src/main/java/ui/Page.java @@ -3,10 +3,15 @@ public enum Page { HOME("Home.fxml"), PROFILE("MyProfile.fxml"), - SIGNIN("SignIn.fxml"), + SIGN_IN("SignIn.fxml"), REGISTER("Register.fxml"), ORGANIZATIONS("OrganizationsView.fxml"), - MYPROFILE("MyProfile.fxml"); + MY_PROFILE("MyProfile.fxml"), + DONATION_USER_INFO("DonationUserInfo.fxml"), + DONATION_AMOUNT("DonationAmount.fxml"), + DONATION_PAYMENT("DonationPayment.fxml"), + DONATION_CONFIRMATION("DonationConfirmation.fxml"); + private final String fileName; 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..3a35fc2 --- /dev/null +++ b/src/main/java/ui/controller/donation/DonationAmountController.java @@ -0,0 +1,76 @@ +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 ui.Page; +import ui.controller.NavigationAware; + +import java.util.function.Consumer; + +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; + + @Override + public void setDonationSession(DonationSession donationSession) { + this.donationSession = donationSession; + orgLabel.setText("Donating to: " + donationSession.getOrganization().getName()); + } + + public void setOnContinue(Consumer onContinue) { + this.onContinue = onContinue; + } + + public void setOnBack(Runnable onBack) { + this.onBack = onBack; + } + + @FXML + public void handleContinue() { + if (amountField.getText().isBlank()) { + errorLabel.setText("Please enter an amount."); + return; + } + try { + int amount = Integer.parseInt(amountField.getText().trim()); + 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..7fe397c --- /dev/null +++ b/src/main/java/ui/controller/donation/DonationConfirmationController.java @@ -0,0 +1,36 @@ +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; + +public class DonationConfirmationController implements NavigationAware, DonationSessionAware { + + @FXML private Label orgLabel; + @FXML private Label amountLabel; + + private Consumer onNavigate; + private DonationSession donationSession; + + @Override + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; + } + + @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..bb9545e --- /dev/null +++ b/src/main/java/ui/controller/donation/DonationFlowController.java @@ -0,0 +1,120 @@ +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; + +public class DonationFlowController { + private final BorderPane mainPane; + private final NavbarController navbarController; + private DonationSession session; + private Consumer onNavigate; + private final UserDonate userDonate = new UserDonate(); + + public DonationFlowController(BorderPane mainPane, NavbarController navbarController) { + this.mainPane = mainPane; + this.navbarController = navbarController; + } + + public void start(Organization org) throws IOException { + session = new DonationSession(); + session.setOrganization(org); + navbarController.hideNavbar(); + loadStep1(); + } + + private void loadStep1() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/DonationUserInfo.fxml")); + Parent root = loader.load(); + + DonationUserInfoController controller = loader.getController(); + controller.setDonationSession(session); + controller.setOnContinue(updatedSession -> { + session = updatedSession; + try { loadStep2(); } + catch (IOException e) { throw new RuntimeException(e); } + }); + + controller.setOnNavigate(onNavigate); + mainPane.setCenter(root); + } + 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(() -> { + try { loadStep1(); } + catch (IOException e) { throw new RuntimeException(e); } + }); + + mainPane.setCenter(root); + } + + 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); + } + 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); + } + + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; + } +} \ No newline at end of file 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..ece65d7 --- /dev/null +++ b/src/main/java/ui/controller/donation/DonationPaymentController.java @@ -0,0 +1,65 @@ +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; + +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; + + @Override + public void setDonationSession(DonationSession donationSession) { + this.donationSession = donationSession; + orgLabel.setText("Donating to: " + donationSession.getOrganization().getName()); + amountLabel.setText("Amount: " + donationSession.getAmount() + " kr"); + } + + public void setOnContinue(Consumer onContinue) { + this.onContinue = onContinue; + } + + @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); + } + + public void setOnBack(Runnable onBack) { + this.onBack = onBack; + } + + @FXML + private void handleBack() { + onBack.run(); + } + +} diff --git a/src/main/java/ui/controller/donation/DonationUserInfoController.java b/src/main/java/ui/controller/donation/DonationUserInfoController.java new file mode 100644 index 0000000..52e1310 --- /dev/null +++ b/src/main/java/ui/controller/donation/DonationUserInfoController.java @@ -0,0 +1,109 @@ +package ui.controller.donation; + +import domain.organization.Organization; +import domain.user.User; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import ui.DonationSession; +import ui.DonationSessionAware; +import ui.Page; +import ui.controller.NavbarController; +import ui.controller.NavigationAware; +import util.SessionManager; + +import java.util.function.Consumer; + +public class DonationUserInfoController implements NavigationAware, DonationSessionAware { + + @FXML private Label loggedInLabel; + @FXML private VBox notLoggedInBox; + @FXML private Label orgLabel; + @FXML private TextField nameField; + @FXML private TextField emailField; + @FXML private TextField phoneField; + @FXML private Label errorLabel; + + private Consumer onNavigate; + private Consumer onContinue; + private DonationSession donationSession; + private Organization org; + + + public void setOrganization(Organization org) { + this.org = org; + } + + @Override + public void setOnNavigate(Consumer onNavigate) { + this.onNavigate = onNavigate; + } + + @Override + public void setDonationSession(DonationSession session) { + this.donationSession = session; + orgLabel.setText("Donating to: " + donationSession.getOrganization().getName()); + } + + public void setOnContinue(Consumer onContinue) { + this.onContinue = onContinue; + } + + @FXML + public void initialize() { + if (SessionManager.isSignedIn()) { + loggedInLabel.setVisible(true); + loggedInLabel.setManaged(true); + notLoggedInBox.setVisible(false); + notLoggedInBox.setManaged(false); + + User user = SessionManager.getSignedInUser(); + nameField.setText(user.getUsername()); + emailField.setText(user.getEmail()); + phoneField.setText(user.getPhoneNumber()); + + } else { + loggedInLabel.setVisible(false); + loggedInLabel.setManaged(false); + notLoggedInBox.setVisible(true); + notLoggedInBox.setManaged(true); + } + } + + + + public void goToSignIn() { + onNavigate.accept(Page.SIGN_IN); + } + + public void goToRegister() { + onNavigate.accept(Page.REGISTER); + } + + public void handleBack() { + onNavigate.accept(Page.ORGANIZATIONS); + } + + public void handleContinue() { + if (nameField.getText().isBlank()) { + errorLabel.setText("Please enter your full name"); + return; + } + if (emailField.getText().isBlank()) { + errorLabel.setText("Please enter your email"); + return; + } + if (phoneField.getText().isBlank()) { + errorLabel.setText("Please enter your phone number"); + return; + } + + donationSession.setName(nameField.getText()); + donationSession.setEmail(emailField.getText()); + donationSession.setPhone(phoneField.getText()); + onContinue.accept(donationSession); + } + +} + diff --git a/src/main/resources/css/Donation.css b/src/main/resources/css/Donation.css new file mode 100644 index 0000000..8aaf42c --- /dev/null +++ b/src/main/resources/css/Donation.css @@ -0,0 +1,114 @@ +.info-box-success { + -fx-background-color: #f0f7ff; + -fx-border-color: #b8d4f5; + -fx-border-width: 1.5; + -fx-border-radius: 8; + -fx-background-radius: 8; + -fx-padding: 12 16 12 16; +} + +.info-box-warning { + -fx-background-color: #fff8e1; + -fx-border-color: #ffe082; + -fx-border-width: 1.5; + -fx-border-radius: 8; + -fx-background-radius: 8; + -fx-padding: 12 16 12 16; +} + +.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; +} + +.error-label { + -fx-text-fill: #e63946; + -fx-font-size: 12px; +} + +.success-icon { + -fx-font-size: 48px; + -fx-text-fill: #2d6a4f; +} \ No newline at end of file diff --git a/src/main/resources/view/DonationAmount.fxml b/src/main/resources/view/DonationAmount.fxml new file mode 100644 index 0000000..75459ae --- /dev/null +++ b/src/main/resources/view/DonationAmount.fxml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + +