diff --git a/.gitignore b/.gitignore
index 480bdf52..4a8c77b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,4 +36,7 @@ build/
.vscode/
### Mac OS ###
-.DS_Store
\ No newline at end of file
+.DS_Store
+
+## Database related
+connectionpetstore.properties
diff --git a/connectionpetstore.properties b/connectionpetstore.properties
new file mode 100644
index 00000000..426ecafb
--- /dev/null
+++ b/connectionpetstore.properties
@@ -0,0 +1,3 @@
+url=jdbc:mysql://127.0.0.1:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
+user=petapp
+password=petapppass
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 8cb9a1ba..6da4edce 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,9 +8,9 @@
PetShopDesktop
1.0-SNAPSHOT
PetShopDesktop
-
UTF-8
+ 25.0.2
5.12.1
@@ -18,12 +18,12 @@
org.openjfx
javafx-controls
- 21.0.6
+ ${javafx.version}
org.openjfx
javafx-fxml
- 21.0.6
+ ${javafx.version}
@@ -66,8 +66,7 @@
default-cli
- org.example.petshopdesktop/org.example.petshopdesktop.PetShopApplication
-
+ org.example.petshopdesktop/org.example.petshopdesktop.PetShopApplication
app
app
app
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 7459e876..d1f07dd9 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -8,7 +8,9 @@ module org.example.petshopdesktop {
opens org.example.petshopdesktop to javafx.fxml;
opens org.example.petshopdesktop.controllers.dialogcontrollers to javafx.fxml;
opens org.example.petshopdesktop.controllers to javafx.fxml;
+ opens org.example.petshopdesktop.auth to javafx.fxml;
exports org.example.petshopdesktop;
exports org.example.petshopdesktop.controllers;
+ exports org.example.petshopdesktop.auth;
}
\ No newline at end of file
diff --git a/src/main/java/org/example/petshopdesktop/PetShopApplication.java b/src/main/java/org/example/petshopdesktop/PetShopApplication.java
index 8a8fcd65..06def37b 100644
--- a/src/main/java/org/example/petshopdesktop/PetShopApplication.java
+++ b/src/main/java/org/example/petshopdesktop/PetShopApplication.java
@@ -4,15 +4,21 @@ import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
+import org.example.petshopdesktop.database.UserDB;
import java.io.IOException;
public class PetShopApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
- FXMLLoader fxmlLoader = new FXMLLoader(PetShopApplication.class.getResource("main-layout-view.fxml"));
+ try {
+ UserDB.initializeTable();
+ } catch (Exception e) {
+ System.err.println("Warning: could not initialize users table: " + e.getMessage());
+ }
+ FXMLLoader fxmlLoader = new FXMLLoader(PetShopApplication.class.getResource("login-view.fxml"));
Scene scene = new Scene(fxmlLoader.load());
- stage.setTitle("Pet Shop Manager");
+ stage.setTitle("Pet Shop Manager - Login");
stage.setScene(scene);
stage.show();
}
diff --git a/src/main/java/org/example/petshopdesktop/auth/Role.java b/src/main/java/org/example/petshopdesktop/auth/Role.java
new file mode 100644
index 00000000..e631ddf7
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/auth/Role.java
@@ -0,0 +1,13 @@
+package org.example.petshopdesktop.auth;
+
+/*
+Petshop Desktop
+Purpose: Application role definitions used by session state and role based access control.
+*/
+public enum Role {
+ // Administrative access, includes system management screens.
+ ADMIN,
+
+ // Staff access, limited to day to day operational screens.
+ STAFF
+}
diff --git a/src/main/java/org/example/petshopdesktop/auth/UserSession.java b/src/main/java/org/example/petshopdesktop/auth/UserSession.java
new file mode 100644
index 00000000..2cc4538a
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/auth/UserSession.java
@@ -0,0 +1,59 @@
+package org.example.petshopdesktop.auth;
+
+/*
+Petshop Desktop
+Purpose: In memory session state for the authenticated user.
+Notes: Session is process local and cleared on logout or application restart.
+*/
+public class UserSession {
+
+ // Singleton instance used to share session state across controllers.
+ private static UserSession instance;
+
+ // Current authenticated username, null when logged out.
+ private String username;
+
+ // Current authenticated role, null when logged out.
+ private Role role;
+
+ private UserSession() {}
+
+ // Lazily initialised singleton accessor.
+ public static UserSession getInstance() {
+ if (instance == null) {
+ instance = new UserSession();
+ }
+ return instance;
+ }
+
+ // Stores identity and role for the active session.
+ public void login(String username, Role role) {
+ this.username = username;
+ this.role = role;
+ }
+
+ // Clears session state and returns the application to an unauthenticated state.
+ public void logout() {
+ this.username = null;
+ this.role = null;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public Role getRole() {
+ return role;
+ }
+
+ // Convenience check for administrative privileges.
+ // Role.ADMIN.equals(role) remains safe when role is null.
+ public boolean isAdmin() {
+ return Role.ADMIN.equals(role);
+ }
+
+ // Session is considered active only when both username and role are set.
+ public boolean isLoggedIn() {
+ return username != null && role != null;
+ }
+}
diff --git a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java
new file mode 100644
index 00000000..440c5686
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java
@@ -0,0 +1,76 @@
+package org.example.petshopdesktop.controllers;
+
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.control.PasswordField;
+import javafx.scene.control.TextField;
+import javafx.stage.Stage;
+import org.example.petshopdesktop.auth.UserSession;
+import org.example.petshopdesktop.database.UserDB;
+import org.example.petshopdesktop.models.User;
+
+import java.sql.SQLException;
+
+/*
+Petshop Desktop
+Purpose: Authentication controller responsible for validating credentials and initialising the user session.
+*/
+public class LoginController {
+
+ @FXML
+ private TextField txtUsername;
+
+ @FXML
+ private PasswordField txtPassword;
+
+ @FXML
+ private Label lblError;
+
+ @FXML
+ void btnLoginClicked(ActionEvent event) {
+ // Input normalisation keeps authentication behaviour consistent.
+ String username = txtUsername.getText().trim();
+ String password = txtPassword.getText();
+
+ // Basic validation to avoid unnecessary database calls.
+ if (username.isEmpty() || password.isEmpty()) {
+ lblError.setText("Please enter username and password.");
+ return;
+ }
+
+ try {
+ // Credential verification returns a fully populated User on success.
+ User user = UserDB.authenticate(username, password);
+ if (user == null) {
+ lblError.setText("Invalid username or password.");
+ txtPassword.clear();
+ return;
+ }
+
+ // Session state is stored in memory for use by controllers and UI RBAC.
+ UserSession.getInstance().login(user.getUsername(), user.getRole());
+ openMainLayout();
+
+ } catch (SQLException e) {
+ lblError.setText("Database error: " + e.getMessage());
+ }
+ }
+
+ private void openMainLayout() {
+ try {
+ // View transition into the post login application shell.
+ FXMLLoader loader = new FXMLLoader(
+ getClass().getResource("/org/example/petshopdesktop/main-layout-view.fxml"));
+ Scene scene = new Scene(loader.load());
+ Stage stage = (Stage) txtUsername.getScene().getWindow();
+ stage.setScene(scene);
+ stage.setTitle("Pet Shop Manager");
+ } catch (Exception e) {
+ lblError.setText("Error loading application: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java
index a5a3060d..a910432e 100644
--- a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java
+++ b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java
@@ -4,9 +4,17 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
+import javafx.scene.Scene;
import javafx.scene.control.Button;
+import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
+import javafx.stage.Stage;
+import org.example.petshopdesktop.auth.UserSession;
+/*
+Petshop Desktop
+Purpose: Main application shell controller, includes navigation and UI level role based access control.
+*/
public class MainLayoutController {
@FXML
@@ -18,6 +26,9 @@ public class MainLayoutController {
@FXML
private Button btnInventory;
+ @FXML
+ private Button btnLogout;
+
@FXML
private Button btnPets;
@@ -36,6 +47,12 @@ public class MainLayoutController {
@FXML
private Button btnSuppliers;
+ @FXML
+ private Label lblUsername;
+
+ @FXML
+ private Label lblRole;
+
@FXML
private StackPane spContentArea;
@@ -93,11 +110,52 @@ public class MainLayoutController {
updateButtons(btnSuppliers);
}
+ @FXML
+ void btnLogoutClicked(ActionEvent event) {
+ // Logout clears session state before returning to the login view.
+ UserSession.getInstance().logout();
+ try {
+ FXMLLoader loader = new FXMLLoader(
+ getClass().getResource("/org/example/petshopdesktop/login-view.fxml"));
+ Scene scene = new Scene(loader.load());
+ Stage stage = (Stage) btnLogout.getScene().getWindow();
+ stage.setScene(scene);
+ stage.setTitle("Pet Shop Manager - Login");
+ } catch (Exception e) {
+ System.err.println("Error loading login view: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
@FXML
public void initialize() {
+ // RBAC state is applied once during initial layout load.
+ applyRBAC();
+
+ // Default landing view after successful authentication.
loadView("pet-view.fxml");
}
+ private void applyRBAC() {
+ UserSession session = UserSession.getInstance();
+
+ // Session identity is displayed in the header for clarity and auditing.
+ lblUsername.setText(session.getUsername());
+ lblRole.setText(session.getRole().toString());
+
+ // UI level RBAC hides admin only navigation entries for non admin users.
+ // setManaged(false) removes the node from layout calculations to avoid empty spacing.
+ boolean isAdmin = session.isAdmin();
+ btnInventory.setVisible(isAdmin);
+ btnInventory.setManaged(isAdmin);
+ btnSuppliers.setVisible(isAdmin);
+ btnSuppliers.setManaged(isAdmin);
+ btnProductSuppliers.setVisible(isAdmin);
+ btnProductSuppliers.setManaged(isAdmin);
+
+ // Privileged operations should still be enforced within the relevant controllers and database methods.
+ }
+
/**
* Load a view when a button is clicked on the navigation
* @param fxmlFile the fxmlFile name to be loaded
@@ -111,7 +169,8 @@ public class MainLayoutController {
spContentArea.getChildren().clear();
spContentArea.getChildren().add(view);
} catch (Exception e) {
- System.out.println("Error loading view: " + fxmlFile);
+ System.err.println("Error loading view: " + fxmlFile);
+ e.printStackTrace();
}
}
diff --git a/src/main/java/org/example/petshopdesktop/controllers/PetController.java b/src/main/java/org/example/petshopdesktop/controllers/PetController.java
index 26834d72..7eb55cb8 100644
--- a/src/main/java/org/example/petshopdesktop/controllers/PetController.java
+++ b/src/main/java/org/example/petshopdesktop/controllers/PetController.java
@@ -1,11 +1,24 @@
package org.example.petshopdesktop.controllers;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
-import javafx.scene.control.Button;
-import javafx.scene.control.TableColumn;
-import javafx.scene.control.TableView;
-import javafx.scene.control.TextField;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController;
+import org.example.petshopdesktop.database.PetDB;
+import org.example.petshopdesktop.database.ProductDB;
+import org.example.petshopdesktop.models.Pet;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.sql.SQLIntegrityConstraintViolationException;
+import java.util.Optional;
public class PetController {
@@ -19,45 +32,191 @@ public class PetController {
private Button btnEdit;
@FXML
- private TableColumn, ?> colPetAge;
+ private TableColumn colPetAge;
@FXML
- private TableColumn, ?> colPetBreed;
+ private TableColumn colPetBreed;
@FXML
- private TableColumn, ?> colPetId;
+ private TableColumn colPetId;
@FXML
- private TableColumn, ?> colPetName;
+ private TableColumn colPetName;
@FXML
- private TableColumn, ?> colPetPrice;
+ private TableColumn colPetPrice;
@FXML
- private TableColumn, ?> colPetSpecies;
+ private TableColumn colPetSpecies;
@FXML
- private TableColumn, ?> colPetStatus;
+ private TableColumn colPetStatus;
@FXML
- private TableView> tvPets;
+ private TableView tvPets;
@FXML
private TextField txtSearch;
@FXML
void btnAddClicked(ActionEvent event) {
-
+ mode = "Add";
+ openDialog(null,mode);
}
@FXML
void btnDeleteClicked(ActionEvent event) {
+ int numRows = 0;
+ Pet selectedPet = tvPets.getSelectionModel().getSelectedItem();
+ //ask user to confirm
+ Alert question = new Alert(Alert.AlertType.CONFIRMATION);
+ question.setHeaderText("Please confirm delete");
+ question.setContentText("Are you sure you want to delete this pet?");
+ Optional result = question.showAndWait(); //show alert and wait for response
+
+ //if confirmed,start deletion
+ if (result.isPresent() && result.get() == ButtonType.OK) {
+ int petId = selectedPet.getPetId();
+
+ //try deleting
+ try{
+ numRows = PetDB.deletePet(petId);
+ }
+ catch (SQLIntegrityConstraintViolationException e){
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setHeaderText("Database Operation Error");
+ alert.setContentText("Delete failed\n" +
+ "the selected pet is being referred in another table");
+ alert.showAndWait();
+ return;
+ }
+ catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+
+ //prompt user of any errors
+ if (numRows == 0){
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setHeaderText("Database Operation Error");
+ alert.setContentText("Delete failed");
+ alert.showAndWait();
+ }
+ else{
+ //prompt user of delete conformation
+ Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
+ alert.setHeaderText("Database Operation Confirmed");
+ alert.setContentText("Delete successful");
+ alert.showAndWait();
+ //refresh display and reset inputs
+ displayPets();
+ btnDelete.setDisable(true);
+ btnEdit.setDisable(true);
+ txtSearch.setText("");
+ }
+ }
}
@FXML
void btnEditClicked(ActionEvent event) {
+ Pet selectedPet = tvPets.getSelectionModel().getSelectedItem();
+ if(selectedPet != null){
+ mode = "Edit";
+ openDialog(selectedPet,mode);
+ }
}
-}
+ private ObservableList data = FXCollections.observableArrayList();
+ String mode = null;
+
+ @FXML
+ void initialize() {
+ btnEdit.setDisable(true);
+ btnDelete.setDisable(true);
+
+ colPetId.setCellValueFactory(new PropertyValueFactory("petId"));
+ colPetName.setCellValueFactory(new PropertyValueFactory("petName"));
+ colPetSpecies.setCellValueFactory(new PropertyValueFactory("petSpecies"));
+ colPetBreed.setCellValueFactory(new PropertyValueFactory("petBreed"));
+ colPetAge.setCellValueFactory(new PropertyValueFactory("petAge"));
+ colPetStatus.setCellValueFactory(new PropertyValueFactory("petStatus"));
+ colPetPrice.setCellValueFactory(new PropertyValueFactory("petPrice"));
+
+ displayPets();
+
+ tvPets.getSelectionModel().selectedItemProperty().addListener(
+ (observable, oldValue, newValue) -> {
+ btnEdit.setDisable(false);
+ btnDelete.setDisable(false);
+ });
+
+ txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
+ displayFilteredPet(newValue);
+ });
+ }
+
+ private void displayFilteredPet(String filter) {
+ data.clear();
+ try{
+ if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){
+ displayPets();
+ }
+ else {
+ data = PetDB.getFilteredPets(filter);
+ tvPets.setItems(data);
+ }
+ } catch (Exception e) {
+ System.out.println("Error while fetching table data: " + e.getMessage());
+ }
+ }
+
+ private void displayPets() {
+ data.clear();
+
+ try{
+ data = PetDB.getPets();
+ }
+ catch(SQLException e){
+ System.out.println("Error while fetching table data: " + e.getMessage());
+ }
+
+ tvPets.setItems(data);
+ }
+
+ private void openDialog(Pet pet, String mode){
+ //Get new view
+ FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml"));
+ Scene scene = null;
+ try{
+ scene = new Scene(fxmlLoader.load());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ PetDialogController dialogController = fxmlLoader.getController(); //controller associated with this view
+ dialogController.setMode(mode);
+
+ //Open the dialog depending on the mode
+ if(mode.equals("Edit")){
+ //Make it display pet details in dialog
+ dialogController.displayPetDetails(pet);
+ }
+ Stage dialogStage = new Stage();
+ dialogStage.initModality(Modality.APPLICATION_MODAL); //make it modal
+ if(mode.equals("Add")){
+ dialogStage.setTitle("Add Pet");
+ }
+ else {
+ dialogStage.setTitle("Edit Pet");
+ }
+ dialogStage.setScene(scene);
+ dialogStage.showAndWait();
+
+ //When dialog closes update table view and disable edit and delete buttons, and reset search bar
+ displayPets();
+ btnDelete.setDisable(true);
+ btnEdit.setDisable(true);
+ txtSearch.setText("");
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java
new file mode 100644
index 00000000..45be6c3c
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java
@@ -0,0 +1,203 @@
+package org.example.petshopdesktop.controllers.dialogcontrollers;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.EventHandler;
+import javafx.fxml.FXML;
+import javafx.scene.Node;
+import javafx.scene.control.*;
+import javafx.scene.input.MouseEvent;
+import javafx.stage.Stage;
+import org.example.petshopdesktop.DTOs.ProductDTO;
+import org.example.petshopdesktop.Validator;
+import org.example.petshopdesktop.database.PetDB;
+import org.example.petshopdesktop.models.Category;
+import org.example.petshopdesktop.models.Pet;
+
+import java.sql.SQLException;
+
+public class PetDialogController {
+
+ @FXML
+ private Button btnCancel;
+
+ @FXML
+ private Button btnSave;
+
+ @FXML
+ private ComboBox cbPetStatus;
+
+ @FXML
+ private Label lblMode;
+
+ @FXML
+ private Label lblPetId;
+
+ @FXML
+ private TextField txtPetAge;
+
+ @FXML
+ private TextField txtPetBreed;
+
+ @FXML
+ private TextField txtPetName;
+
+ @FXML
+ private TextField txtPetPrice;
+
+ @FXML
+ private TextField txtPetSpecies;
+
+ private String mode = null;
+
+ private ObservableList statusList = FXCollections.observableArrayList(
+ "Available", "Adopted"
+ );
+
+ @FXML
+ void initialize() {
+
+ cbPetStatus.setItems(statusList); //set status combobox
+
+ //Set up mouse handlers for buttons
+ btnSave.setOnMouseClicked(new EventHandler() {
+ @Override
+ public void handle(MouseEvent mouseEvent) {
+ buttonSaveClicked(mouseEvent);
+ }
+ });
+
+ btnCancel.setOnMouseClicked(new EventHandler() {
+ @Override
+ public void handle(MouseEvent mouseEvent) {
+ closeStage(mouseEvent);
+ }
+ });
+ }
+
+ private void buttonSaveClicked(MouseEvent mouseEvent) {
+ int numRow = 0;
+ String errorMsg = "";
+
+ //Check validation (input required)
+ errorMsg += Validator.isPresent(txtPetName.getText(), "Pet Name");
+ errorMsg += Validator.isPresent(txtPetAge.getText(), "Age");
+ errorMsg += Validator.isPresent(txtPetBreed.getText(), "Breed");
+ errorMsg += Validator.isPresent(txtPetSpecies.getText(), "Species");
+ errorMsg += Validator.isPresent(txtPetPrice.getText(), "Price");
+ if (cbPetStatus.getSelectionModel().getSelectedItem() == null){
+ errorMsg += "Status is required";
+ }
+
+ //Check validation (length size)
+ errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50);
+ errorMsg += Validator.isLessThanVarChars(txtPetSpecies.getText(), "Species", 50);
+ errorMsg += Validator.isLessThanVarChars(txtPetBreed.getText(), "Breed", 50);
+ errorMsg += Validator.isLessThanVarChars(txtPetPrice.getText(), "Price", 12);
+ errorMsg += Validator.isLessThanVarChars(txtPetAge.getText(), "Age", 11);
+
+ //Check validation (format)
+ errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price");
+ errorMsg += Validator.isNonNegativeInteger(txtPetAge.getText(), "Age");
+
+ if(errorMsg.isEmpty()){
+ Pet pet = collectPet();
+ if(mode.equals("Add")) {
+ try{
+ numRow = PetDB.insertPet(pet);
+ }
+ catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ else {
+ try {
+ numRow = PetDB.updatePet(pet.getPetId(), pet);
+ }
+ catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ //if no rows were affected then there was an error (prompt user of error)
+ if (numRow == 0){
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setHeaderText("Database Operation Error");
+ alert.setContentText(mode + " failed");
+ alert.showAndWait();
+ closeStage(mouseEvent);
+ }
+ else {
+ //tell the user operation was successful
+ Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
+ alert.setHeaderText("Database Operation Confirmed");
+ alert.setContentText(mode + " succeeded");
+ alert.showAndWait();
+ closeStage(mouseEvent);
+ }
+ }
+ else{
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setHeaderText("Input Error");
+ alert.setContentText(errorMsg);
+ alert.showAndWait();
+ }
+ }
+
+ private Pet collectPet() {
+ int petId =0;
+ Pet pet = null;
+
+ if(lblPetId.isVisible()){
+ petId = Integer.parseInt(lblPetId.getText().split(": ")[1]);
+ }
+ pet = new Pet(
+ petId,
+ txtPetName.getText(),
+ txtPetSpecies.getText(),
+ txtPetBreed.getText(),
+ Integer.parseInt(txtPetAge.getText()),
+ cbPetStatus.getValue(),
+ Double.parseDouble(txtPetPrice.getText())
+ );
+ return pet;
+ }
+
+ private void closeStage(MouseEvent mouseEvent) {
+ Node node = (Node) mouseEvent.getSource();
+ Stage stage = (Stage) node.getScene().getWindow();
+ stage.close();
+ }
+
+ public void displayPetDetails(Pet pet){
+ if (pet!=null){
+ lblPetId.setText("ID: " + pet.getPetId());
+ txtPetName.setText(pet.getPetName());
+ txtPetSpecies.setText(pet.getPetSpecies());
+ txtPetBreed.setText(pet.getPetBreed());
+ txtPetAge.setText(pet.getPetAge() + "");
+ txtPetPrice.setText(pet.getPetPrice() + "");
+
+ //get the right combobox selection
+ for (String status : cbPetStatus.getItems()) {
+ if(status.equals(pet.getPetStatus())){
+ cbPetStatus.getSelectionModel().select(status);
+ }
+ }
+
+
+ }
+ }
+
+ public void setMode(String mode) {
+ this.mode = mode;
+ lblMode.setText(mode + " Pet");
+ if(mode.equals("Add")) {
+ lblPetId.setVisible(false);
+ }
+ else if(mode.equals("Edit")) {
+ lblPetId.setVisible(true);
+ }
+ }
+
+}
diff --git a/src/main/java/org/example/petshopdesktop/database/ConnectionDB.java b/src/main/java/org/example/petshopdesktop/database/ConnectionDB.java
index 2b0bad1d..3435c7ac 100644
--- a/src/main/java/org/example/petshopdesktop/database/ConnectionDB.java
+++ b/src/main/java/org/example/petshopdesktop/database/ConnectionDB.java
@@ -2,6 +2,9 @@ package org.example.petshopdesktop.database;
import java.io.FileInputStream;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@@ -9,8 +12,7 @@ import java.util.Properties;
public class ConnectionDB {
/**
- * Method to try and connect to the database sing cnnection.properties located in the
- * root of C drive
+ * Method to try and connect to the database using connectionpetstore.properties.
* @return Connection to the database
*/
public static Connection getConnection(){
@@ -18,10 +20,23 @@ public class ConnectionDB {
String user = "";
String password = "";
- try{
- //Read connection.properties file
- FileInputStream fis = new FileInputStream("c:\\connectionpetstore.properties"); //location of connection can be changed here
- Properties prop = new Properties();
+ Properties prop = new Properties();
+ Path propsPath;
+
+ String explicitPath = System.getenv("PETSTORE_DB_PROPS");
+ if (explicitPath != null && !explicitPath.isBlank()) {
+ propsPath = Paths.get(explicitPath);
+ } else {
+ Path cwd = Paths.get(System.getProperty("user.dir"), "connectionpetstore.properties");
+ Path xdg = Paths.get(System.getProperty("user.home"), ".config", "petstore", "connectionpetstore.properties");
+ Path legacyWindows = Paths.get("./connectionpetstore.properties");
+
+ if (Files.exists(cwd)) propsPath = cwd;
+ else if (Files.exists(xdg)) propsPath = xdg;
+ else propsPath = legacyWindows;
+ }
+
+ try (FileInputStream fis = new FileInputStream(propsPath.toString())) {
prop.load(fis);
url = prop.getProperty("url");
user = prop.getProperty("user");
@@ -31,12 +46,8 @@ public class ConnectionDB {
throw new RuntimeException("Problem with reading connection info: "+e.getMessage());
}
- Connection conn = null;
-
try{
- //try to get connection with the data taken from connection.properties
- conn = DriverManager.getConnection(url,user,password);
- return conn;
+ return DriverManager.getConnection(url,user,password);
}
catch (SQLException e) {
throw new RuntimeException("Problem with database connection: "+e.getMessage());
diff --git a/src/main/java/org/example/petshopdesktop/database/PetDB.java b/src/main/java/org/example/petshopdesktop/database/PetDB.java
new file mode 100644
index 00000000..d1b7ca37
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/database/PetDB.java
@@ -0,0 +1,145 @@
+package org.example.petshopdesktop.database;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import org.example.petshopdesktop.models.Pet;
+
+import java.sql.*;
+
+public class PetDB {
+ public static ObservableList getPets() throws SQLException {
+ //Connect to the database
+ ObservableList pets = FXCollections.observableArrayList();
+ Connection conn = ConnectionDB.getConnection();
+
+ //Execute Query
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery("SELECT * FROM pet");
+
+ //While there is still data add pets to the list
+ while(rs.next()){
+ Pet pet = new Pet(
+ rs.getInt(1),
+ rs.getString(2),
+ rs.getString(3),
+ rs.getString(4),
+ rs.getInt(5),
+ rs.getString(6),
+ rs.getDouble(7)
+ );
+ pets.add(pet);
+ }
+
+ //close connection and return pets
+ conn.close();
+ return pets;
+ }
+
+ public static ObservableList getFilteredPets(String filter) throws SQLException {
+ //Connect to the database
+ ObservableList pets = FXCollections.observableArrayList();
+ Connection conn = ConnectionDB.getConnection();
+
+ //Get SQL query for filtered word
+ String sql = "SELECT * FROM pet " +
+ " WHERE " +
+ "petName LIKE ? OR " +
+ "petSpecies LIKE ? OR " +
+ "petBreed LIKE ? OR " +
+ "petAge LIKE ? OR " +
+ "petStatus LIKE ? OR " +
+ "petPrice LIKE ?";
+
+ String filteredString = "%" + filter + "%";
+
+ PreparedStatement stmt = conn.prepareStatement(sql);
+ stmt.setString(1, filteredString);
+ stmt.setString(2, filteredString);
+ stmt.setString(3, filteredString);
+ stmt.setString(4, filteredString);
+ stmt.setString(5, filteredString);
+ stmt.setString(6, filteredString);
+
+ ResultSet rs = stmt.executeQuery();
+
+ while(rs.next()){
+ Pet pet = new Pet(
+ rs.getInt(1),
+ rs.getString(2),
+ rs.getString(3),
+ rs.getString(4),
+ rs.getInt(5),
+ rs.getString(6),
+ rs.getDouble(7)
+ );
+ pets.add(pet);
+ }
+
+ conn.close();
+ return pets;
+ }
+
+ public static int insertPet(Pet pet) throws SQLException {
+ int numRows = 0;
+
+ Connection conn = ConnectionDB.getConnection();
+ String sql = "INSERT INTO pet (petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice)" +
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
+
+ PreparedStatement stmt = conn.prepareStatement(sql);
+ stmt.setInt(1, pet.getPetId());
+ stmt.setString(2, pet.getPetName());
+ stmt.setString(3, pet.getPetSpecies());
+ stmt.setString(4, pet.getPetBreed());
+ stmt.setInt(5, pet.getPetAge());
+ stmt.setString(6, pet.getPetStatus());
+ stmt.setDouble(7, pet.getPetPrice());
+
+ numRows = stmt.executeUpdate();
+ conn.close();
+
+ return numRows;
+ }
+
+ public static int updatePet(int petId, Pet pet) throws SQLException {
+ int numRows = 0;
+
+ Connection conn = ConnectionDB.getConnection();
+ String sql = "UPDATE pet SET " +
+ " petName = ?, " +
+ " petSpecies = ?, " +
+ " petBreed = ?, " +
+ " petAge = ?, " +
+ " petStatus = ?, " +
+ " petPrice = ? " +
+ " WHERE petId = ?";
+ PreparedStatement stmt = conn.prepareStatement(sql);
+ stmt.setString(1, pet.getPetName());
+ stmt.setString(2, pet.getPetSpecies());
+ stmt.setString(3, pet.getPetBreed());
+ stmt.setInt(4, pet.getPetAge());
+ stmt.setString(5, pet.getPetStatus());
+ stmt.setDouble(6, pet.getPetPrice());
+ stmt.setInt(7, petId);
+
+ numRows = stmt.executeUpdate();
+ conn.close();
+
+ return numRows;
+ }
+
+ public static int deletePet(int petId) throws SQLException {
+ int numRows = 0;
+ Connection conn = ConnectionDB.getConnection();
+
+ String sql = "DELETE FROM pet WHERE petId = ?";
+ PreparedStatement stmt = conn.prepareStatement(sql);
+
+ stmt.setInt(1, petId);
+
+ numRows = stmt.executeUpdate();
+ conn.close();
+
+ return numRows;
+ }
+}
diff --git a/src/main/java/org/example/petshopdesktop/database/UserDB.java b/src/main/java/org/example/petshopdesktop/database/UserDB.java
new file mode 100644
index 00000000..3b5f78b4
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/database/UserDB.java
@@ -0,0 +1,80 @@
+package org.example.petshopdesktop.database;
+
+import org.example.petshopdesktop.auth.Role;
+import org.example.petshopdesktop.models.User;
+
+import java.sql.*;
+
+/*
+Petshop Desktop
+Purpose: User authentication and role lookup against the users table.
+*/
+public class UserDB {
+
+ /**
+ * Authenticate a user by username and password.
+ * Passwords are stored as SHA-256 hex digests in the database.
+ *
+ * @param username the username to authenticate
+ * @param password the plaintext password
+ * @return the User if credentials are valid, or null if authentication fails
+ */
+ public static User authenticate(String username, String password) throws SQLException {
+ String sql = "SELECT user_id, username, role FROM users " +
+ "WHERE username = ? AND password_hash = SHA2(?, 256)";
+
+ try (Connection conn = ConnectionDB.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+
+ ps.setString(1, username);
+ ps.setString(2, password);
+
+ try (ResultSet rs = ps.executeQuery()) {
+ if (rs.next()) {
+ int userId = rs.getInt("user_id");
+ String uname = rs.getString("username");
+
+ // Role values are stored in the database as strings and normalised to match the enum.
+ // Table constraints limit role values, Role.valueOf is expected to be safe under normal operation.
+ Role role = Role.valueOf(rs.getString("role").toUpperCase());
+
+ return new User(userId, uname, role);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Create the users table and seed default admin/staff accounts if they do not exist.
+ * Passwords are stored as SHA2-256 hashes.
+ */
+ public static void initializeTable() throws SQLException {
+ String createTable = """
+ CREATE TABLE IF NOT EXISTS users (
+ user_id INT AUTO_INCREMENT PRIMARY KEY,
+ username VARCHAR(100) NOT NULL UNIQUE,
+ password_hash CHAR(64) NOT NULL,
+ role ENUM('ADMIN','STAFF') NOT NULL
+ )
+ """;
+
+ // Default accounts support initial development and testing, credentials should be rotated or removed for deployment.
+ String seedAdmin = """
+ INSERT IGNORE INTO users (username, password_hash, role)
+ VALUES ('admin', SHA2('admin123', 256), 'ADMIN')
+ """;
+
+ String seedStaff = """
+ INSERT IGNORE INTO users (username, password_hash, role)
+ VALUES ('staff', SHA2('staff123', 256), 'STAFF')
+ """;
+
+ try (Connection conn = ConnectionDB.getConnection();
+ Statement st = conn.createStatement()) {
+ st.executeUpdate(createTable);
+ st.executeUpdate(seedAdmin);
+ st.executeUpdate(seedStaff);
+ }
+ }
+}
diff --git a/src/main/java/org/example/petshopdesktop/models/Pet.java b/src/main/java/org/example/petshopdesktop/models/Pet.java
new file mode 100644
index 00000000..e1f2e3fb
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/models/Pet.java
@@ -0,0 +1,109 @@
+package org.example.petshopdesktop.models;
+
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleStringProperty;
+
+public class Pet {
+ private SimpleIntegerProperty petId;
+ private SimpleStringProperty petName;
+ private SimpleStringProperty petSpecies;
+ private SimpleStringProperty petBreed;
+ private SimpleIntegerProperty petAge;
+ private SimpleStringProperty petStatus;
+ private SimpleDoubleProperty petPrice;
+
+ public Pet(int petId, String petName, String petSpecies, String petBreed, int petAge, String petStatus, double petPrice) {
+ this.petId = new SimpleIntegerProperty(petId);
+ this.petName = new SimpleStringProperty(petName);
+ this.petSpecies = new SimpleStringProperty(petSpecies);
+ this.petBreed = new SimpleStringProperty(petBreed);
+ this.petAge = new SimpleIntegerProperty(petAge);
+ this.petStatus = new SimpleStringProperty(petStatus);
+ this.petPrice = new SimpleDoubleProperty(petPrice);
+ }
+
+ public int getPetId() {
+ return petId.get();
+ }
+
+ public void setPetId(int petId) {
+ this.petId.set(petId);
+ }
+
+ public SimpleIntegerProperty petIdProperty() {
+ return petId;
+ }
+
+ public String getPetName() {
+ return petName.get();
+ }
+
+ public void setPetName(String petName) {
+ this.petName.set(petName);
+ }
+
+ public SimpleStringProperty petNameProperty() {
+ return petName;
+ }
+
+ public String getPetSpecies() {
+ return petSpecies.get();
+ }
+
+ public void setPetSpecies(String petSpecies) {
+ this.petSpecies.set(petSpecies);
+ }
+
+ public SimpleStringProperty petSpeciesProperty() {
+ return petSpecies;
+ }
+
+ public String getPetBreed() {
+ return petBreed.get();
+ }
+
+ public void setPetBreed(String petBreed) {
+ this.petBreed.set(petBreed);
+ }
+
+ public SimpleStringProperty petBreedProperty() {
+ return petBreed;
+ }
+
+ public int getPetAge() {
+ return petAge.get();
+ }
+
+ public void setPetAge(int petAge) {
+ this.petAge.set(petAge);
+ }
+
+ public SimpleIntegerProperty petAgeProperty() {
+ return petAge;
+ }
+
+ public String getPetStatus() {
+ return petStatus.get();
+ }
+
+ public void setPetStatus(String petStatus) {
+ this.petStatus.set(petStatus);
+ }
+
+ public SimpleStringProperty petStatusProperty() {
+ return petStatus;
+ }
+
+ public double getPetPrice() {
+ return petPrice.get();
+ }
+
+ public void setPetPrice(double petPrice) {
+ this.petPrice.set(petPrice);
+ }
+
+ public SimpleDoubleProperty petPriceProperty() {
+ return petPrice;
+ }
+}
diff --git a/src/main/java/org/example/petshopdesktop/models/User.java b/src/main/java/org/example/petshopdesktop/models/User.java
new file mode 100644
index 00000000..02089be4
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/models/User.java
@@ -0,0 +1,27 @@
+package org.example.petshopdesktop.models;
+
+import org.example.petshopdesktop.auth.Role;
+
+public class User {
+ private int userId;
+ private String username;
+ private Role role;
+
+ public User(int userId, String username, Role role) {
+ this.userId = userId;
+ this.username = username;
+ this.role = role;
+ }
+
+ public int getUserId() {
+ return userId;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public Role getRole() {
+ return role;
+ }
+}
diff --git a/src/main/resources/org/example/petshopdesktop/login-view.fxml b/src/main/resources/org/example/petshopdesktop/login-view.fxml
new file mode 100644
index 00000000..65199c95
--- /dev/null
+++ b/src/main/resources/org/example/petshopdesktop/login-view.fxml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml b/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml
index 9579dff6..983d07a9 100644
--- a/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml
+++ b/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml
@@ -16,12 +16,12 @@
-