Merge conflict resolution and updates

This commit is contained in:
2026-02-24 20:26:50 -07:00
parent 208aaae652
commit 13e8c01a28
38 changed files with 594 additions and 266 deletions

View File

@@ -0,0 +1,93 @@
package org.example.petshopdesktop.DTOs;
import javafx.beans.property.*;
public class SaleDTO {
private IntegerProperty saleId;
private StringProperty saleDate;
private StringProperty employeeName;
private StringProperty productName;
private IntegerProperty quantity;
private DoubleProperty unitPrice;
private DoubleProperty total;
private StringProperty paymentMethod;
public SaleDTO(int saleId, String saleDate, String employeeName, String productName,
int quantity, double unitPrice, double total, String paymentMethod) {
this.saleId = new SimpleIntegerProperty(saleId);
this.saleDate = new SimpleStringProperty(saleDate);
this.employeeName = new SimpleStringProperty(employeeName);
this.productName = new SimpleStringProperty(productName);
this.quantity = new SimpleIntegerProperty(quantity);
this.unitPrice = new SimpleDoubleProperty(unitPrice);
this.total = new SimpleDoubleProperty(total);
this.paymentMethod = new SimpleStringProperty(paymentMethod);
}
// Getters
public int getSaleId() {
return saleId.get();
}
public String getSaleDate() {
return saleDate.get();
}
public String getEmployeeName() {
return employeeName.get();
}
public String getProductName() {
return productName.get();
}
public int getQuantity() {
return quantity.get();
}
public double getUnitPrice() {
return unitPrice.get();
}
public double getTotal() {
return total.get();
}
public String getPaymentMethod() {
return paymentMethod.get();
}
// Properties
public IntegerProperty saleIdProperty() {
return saleId;
}
public StringProperty saleDateProperty() {
return saleDate;
}
public StringProperty employeeNameProperty() {
return employeeName;
}
public StringProperty productNameProperty() {
return productName;
}
public IntegerProperty quantityProperty() {
return quantity;
}
public DoubleProperty unitPriceProperty() {
return unitPrice;
}
public DoubleProperty totalProperty() {
return total;
}
public StringProperty paymentMethodProperty() {
return paymentMethod;
}
}

View File

@@ -1,5 +1,3 @@
//Initial commmit
package org.example.petshopdesktop;
import javafx.application.Application;

View File

@@ -3,14 +3,14 @@ package org.example.petshopdesktop;
public class Validator {
/**
* Checks if string is not empty
* Checks if string is not blank
* @param value string to check
* @param name name of the input
* @return error msg if string is not empty, otherwise empty
* @return error msg if string is blank, otherwise empty
*/
public static String isPresent(String value, String name){
String msg =""; //OK so far
if(value.isEmpty() || name.isBlank()){
String msg = "";
if (value == null || value.isBlank()){
msg += name + " is required. \n";
}
return msg;
@@ -23,7 +23,7 @@ public class Validator {
* @return error msg if input is not a number or negative, otherwise empty
*/
public static String isNonNegativeDouble(String value, String name){
String msg =""; //OK so far
String msg ="";
double result;
try{
result = Double.parseDouble(value);
@@ -40,13 +40,13 @@ public class Validator {
/**
* Checks if the input is a double in 2 different range
* @param value input of string
* @param name name of inpt
* @param name name of input
* @param minValue min value of range
* @param maxValue max value of range
* @return error msg if input is out of range, otherwise empty
*/
public static String isDoubleInRange(String value, String name, double minValue, double maxValue){
String msg =""; //OK so far
String msg ="";
double result;
try{
result = Double.parseDouble(value);
@@ -67,7 +67,7 @@ public class Validator {
* @return error msg if input is not a number or negative, otherwise empty
*/
public static String isNonNegativeInteger(String value, String name){
String msg =""; //OK so far
String msg ="";
int result;
try{
result = Integer.parseInt(value);
@@ -85,6 +85,7 @@ public class Validator {
* check if the string is a given amount of characters or fewer
* @param value input of string
* @param name name of input
* @param length max allowed length
* @return error msg if input is more than given characters length
*/
public static String isLessThanVarChars(String value, String name, int length){
@@ -102,8 +103,7 @@ public class Validator {
* @return error msg if input is not a valid email format, otherwise empty
*/
public static String isValidEmail(String value, String name){
String msg = ""; //OK so far
// Email regex
String msg = "";
String regex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
if (!value.matches(regex)){
@@ -119,8 +119,7 @@ public class Validator {
* @return error msg if input is not in valid phone format, otherwise empty
*/
public static String isValidPhoneNumber(String value, String name){
String msg = ""; //OK so far
// Phone regex
String msg = "";
String regex = "^\\d{3}-\\d{3}-\\d{4}$";
if (!value.matches(regex)){

View File

@@ -1,13 +1,6 @@
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
}

View File

@@ -1,24 +1,15 @@
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();
@@ -26,13 +17,11 @@ public class 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;
@@ -46,13 +35,10 @@ public class UserSession {
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;
}

View File

@@ -14,10 +14,6 @@ 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
@@ -31,18 +27,15 @@ public class LoginController {
@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.");
@@ -50,7 +43,6 @@ public class LoginController {
return;
}
// Session state is stored in memory for use by controllers and UI RBAC.
UserSession.getInstance().login(user.getUsername(), user.getRole());
openMainLayout();
@@ -61,7 +53,6 @@ public class LoginController {
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());

View File

@@ -11,12 +11,23 @@ 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 {
private static final String NAV_BASE_STYLE = "-fx-background-color: transparent; " +
"-fx-text-fill: #cbd5e1; " +
"-fx-background-radius: 10; " +
"-fx-cursor: hand; " +
"-fx-focus-color: transparent; " +
"-fx-faint-focus-color: transparent;";
private static final String NAV_ACTIVE_STYLE = "-fx-background-color: #FF6B6B; " +
"-fx-text-fill: white; " +
"-fx-background-radius: 10; " +
"-fx-cursor: hand; " +
"-fx-focus-color: transparent; " +
"-fx-faint-focus-color: transparent; " +
"-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.22), 10, 0.15, 0, 2);";
@FXML
private Button btnAdoptions;
@@ -124,7 +135,6 @@ public class MainLayoutController {
@FXML
void btnLogoutClicked(ActionEvent event) {
// Logout clears session state before returning to the login view.
UserSession.getInstance().logout();
try {
FXMLLoader loader = new FXMLLoader(
@@ -141,22 +151,18 @@ public class MainLayoutController {
@FXML
public void initialize() {
// RBAC state is applied once during initial layout load.
applyRBAC();
// Default landing view after successful authentication.
loadView("pet-view.fxml");
updateButtons(btnPets);
}
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());
lblRole.setText("Leon's Petstore");
// 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);
@@ -165,7 +171,6 @@ public class MainLayoutController {
btnProductSuppliers.setVisible(isAdmin);
btnProductSuppliers.setManaged(isAdmin);
// Privileged operations should still be enforced within the relevant controllers and database methods.
}
/**
@@ -174,10 +179,8 @@ public class MainLayoutController {
*/
private void loadView(String fxmlFile) {
try {
//Get the location of the fxml for view
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/modelviews/" + fxmlFile));
Parent view = loader.load();
//Clear any content that is in the stack pane and add the new view to display
spContentArea.getChildren().clear();
spContentArea.getChildren().add(view);
} catch (Exception e) {
@@ -191,21 +194,13 @@ public class MainLayoutController {
* @param activeButton the button to be set active
*/
private void updateButtons(Button activeButton) {
//reset all buttons
Button[] BUTTONS = {btnAdoptions, btnPets, btnAppointments, btnInventory,
btnSalesHistory, btnServices, btnSuppliers, btnProductSuppliers, btnProducts};
btnSalesHistory, btnServices, btnSuppliers, btnProductSuppliers, btnProducts, btnPurchaseOrders};
for (Button button : BUTTONS) {
//set all buttons to inactive
button.setStyle("-fx-background-color: transparent; " +
"-fx-text-fill: #CCCCCC; " +
"-fx-cursor: hand");
button.setStyle(NAV_BASE_STYLE);
}
//set active button
activeButton.setStyle("-fx-background-color: #FF6B6B; " +
"-fx-text-fill: white; " +
"-fx-cursor: hand; " +
"-fx-background-radius: 8");
activeButton.setStyle(NAV_ACTIVE_STYLE);
}
}

View File

@@ -1,11 +1,14 @@
package org.example.petshopdesktop.controllers;
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.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import org.example.petshopdesktop.DTOs.SaleDTO;
import org.example.petshopdesktop.database.SaleDB;
import java.sql.SQLException;
public class SaleController {
@@ -13,38 +16,137 @@ public class SaleController {
private Button btnRefresh;
@FXML
private TableColumn<?, ?> colCustomerName;
private TableColumn<SaleDTO, String> colCustomerName;
@FXML
private TableColumn<?, ?> colSaleDate;
private TableColumn<SaleDTO, String> colSaleDate;
@FXML
private TableColumn<?, ?> colSaleId;
private TableColumn<SaleDTO, Integer> colSaleId;
@FXML
private TableColumn<?, ?> colSalePaymentType;
private TableColumn<SaleDTO, String> colSalePaymentType;
@FXML
private TableColumn<?, ?> colSaleQuantity;
private TableColumn<SaleDTO, Integer> colSaleQuantity;
@FXML
private TableColumn<?, ?> colSaleTotal;
private TableColumn<SaleDTO, Double> colSaleTotal;
@FXML
private TableColumn<?, ?> colSaleUnitPrice;
private TableColumn<SaleDTO, Double> colSaleUnitPrice;
@FXML
private TableColumn<?, ?> colServiceProduct;
private TableColumn<SaleDTO, String> colServiceProduct;
@FXML
private TableView<?> tvSales;
private TableView<SaleDTO> tvSales;
@FXML
private TextField txtSearch;
@FXML
void btnRefresh(ActionEvent event) {
private ObservableList<SaleDTO> salesData;
/**
* Initialize the controller - set up table columns and load data
*/
@FXML
public void initialize() {
// Set up table columns
colSaleId.setCellValueFactory(new PropertyValueFactory<>("saleId"));
colSaleDate.setCellValueFactory(new PropertyValueFactory<>("saleDate"));
colCustomerName.setCellValueFactory(new PropertyValueFactory<>("employeeName"));
colServiceProduct.setCellValueFactory(new PropertyValueFactory<>("productName"));
colSaleQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
colSaleUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
colSaleTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
colSalePaymentType.setCellValueFactory(new PropertyValueFactory<>("paymentMethod"));
// Format currency columns
colSaleUnitPrice.setCellFactory(tc -> new TableCell<SaleDTO, Double>() {
@Override
protected void updateItem(Double price, boolean empty) {
super.updateItem(price, empty);
if (empty || price == null) {
setText(null);
} else {
setText(String.format("$%.2f", price));
}
}
});
colSaleTotal.setCellFactory(tc -> new TableCell<SaleDTO, Double>() {
@Override
protected void updateItem(Double total, boolean empty) {
super.updateItem(total, empty);
if (empty || total == null) {
setText(null);
} else {
setText(String.format("$%.2f", total));
}
}
});
// Load initial data
loadSales();
// Add search functionality
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == null || newValue.trim().isEmpty()) {
loadSales();
} else {
searchSales(newValue.trim());
}
});
}
/**
* Load all sales from database
*/
private void loadSales() {
try {
salesData = SaleDB.getSales();
tvSales.setItems(salesData);
} catch (SQLException e) {
e.printStackTrace();
showError("Failed to load sales data", e.getMessage());
}
}
/**
* Search sales based on filter text
* @param filter search term
*/
private void searchSales(String filter) {
try {
ObservableList<SaleDTO> filteredSales = SaleDB.getFilteredSales(filter);
tvSales.setItems(filteredSales);
} catch (SQLException e) {
e.printStackTrace();
showError("Failed to search sales", e.getMessage());
}
}
/**
* Refresh button handler - reload all sales data
* @param event button click event
*/
@FXML
void btnRefresh(ActionEvent event) {
txtSearch.clear();
loadSales();
}
/**
* Show error alert
* @param title alert title
* @param message error message
*/
private void showError(String title, String message) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
}

View File

@@ -115,10 +115,10 @@ public class AdoptionDB {
Connection conn = ConnectionDB.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT customerId, firstName, lastName FROM customer");
ResultSet rs = stmt.executeQuery("SELECT customerId, firstName, lastName, email, phone FROM customer");
while (rs.next()) {
customers.add(new Customer(rs.getInt(1), rs.getString(2), rs.getString(3)));
customers.add(new Customer(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5)));
}
conn.close();

View File

@@ -23,7 +23,7 @@ public class ProductSupplierDB {
//Execute Query
Statement stmt = conn.createStatement();
String sql = "SELECT ps.supId, ps.prodId, s.supCompany, p.prodName, ps.cost " +
"FROM productsupplier ps " +
"FROM productSupplier ps " +
"LEFT JOIN product p " +
"ON p.prodId = ps.prodId " +
"LEFT JOIN supplier s " +
@@ -62,7 +62,7 @@ public class ProductSupplierDB {
String sql =
"SELECT ps.supId, ps.prodId, s.supCompany, p.prodName, ps.cost " +
"FROM product p " +
"LEFT JOIN productsupplier ps " +
"LEFT JOIN productSupplier ps " +
"ON p.prodId = ps.prodId " +
"LEFT JOIN supplier s " +
"ON s.supId = ps.supId " +
@@ -108,7 +108,7 @@ public class ProductSupplierDB {
int numRows = 0;
Connection conn = ConnectionDB.getConnection();
String sql = "INSERT INTO productsupplier (prodId, supId, cost) " +
String sql = "INSERT INTO productSupplier (prodId, supId, cost) " +
"VALUES (?, ?, ?)";
//These are the values from productSupplier to put into query above
@@ -141,7 +141,7 @@ public class ProductSupplierDB {
try{
//Delete old data first
String sql = "DELETE FROM productsupplier WHERE supId = ? AND prodId = ?";
String sql = "DELETE FROM productSupplier WHERE supId = ? AND prodId = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setInt(1, oldSupId);
stmt.setInt(2, oldProdId);
@@ -149,7 +149,7 @@ public class ProductSupplierDB {
//Then change the data by inserting a new relation with given keys (only if delete worked)
if(numRows > 0){
sql = "INSERT INTO productsupplier (prodId, supId, cost) " +
sql = "INSERT INTO productSupplier (prodId, supId, cost) " +
"VALUES (?, ?, ?)";
stmt = conn.prepareStatement(sql);
stmt.setInt(1, productSupplier.getProdId());
@@ -184,7 +184,7 @@ public class ProductSupplierDB {
int numRows = 0;
Connection conn = ConnectionDB.getConnection();
String sql = "DELETE FROM productsupplier WHERE supId = ? AND prodId = ?";
String sql = "DELETE FROM productSupplier WHERE supId = ? AND prodId = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setInt(1, supId);
stmt.setInt(2, prodId);

View File

@@ -0,0 +1,113 @@
package org.example.petshopdesktop.database;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.example.petshopdesktop.DTOs.SaleDTO;
import java.sql.*;
public class SaleDB {
/**
* Get all sale items with details
* @return ObservableList of SaleDTOs
* @throws SQLException if database operation fails
*/
public static ObservableList<SaleDTO> getSales() throws SQLException {
ObservableList<SaleDTO> sales = FXCollections.observableArrayList();
Connection conn = ConnectionDB.getConnection();
String sql = """
SELECT
s.saleId,
DATE_FORMAT(s.saleDate, '%Y-%m-%d %H:%i') as saleDate,
CONCAT(e.firstName, ' ', e.lastName) as employeeName,
p.prodName,
si.quantity,
si.unitPrice,
(si.quantity * si.unitPrice) as lineTotal,
s.paymentMethod
FROM sale s
JOIN saleItem si ON s.saleId = si.saleId
JOIN product p ON si.prodId = p.prodId
JOIN employee e ON s.employeeId = e.employeeId
ORDER BY s.saleDate DESC, s.saleId, si.saleItemId
""";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
sales.add(new SaleDTO(
rs.getInt("saleId"),
rs.getString("saleDate"),
rs.getString("employeeName"),
rs.getString("prodName"),
rs.getInt("quantity"),
rs.getDouble("unitPrice"),
rs.getDouble("lineTotal"),
rs.getString("paymentMethod")
));
}
conn.close();
return sales;
}
/**
* Get filtered sale items
* @param filter search term
* @return ObservableList of SaleDTOs matching the filter
* @throws SQLException if database operation fails
*/
public static ObservableList<SaleDTO> getFilteredSales(String filter) throws SQLException {
ObservableList<SaleDTO> sales = FXCollections.observableArrayList();
Connection conn = ConnectionDB.getConnection();
String sql = """
SELECT
s.saleId,
DATE_FORMAT(s.saleDate, '%Y-%m-%d %H:%i') as saleDate,
CONCAT(e.firstName, ' ', e.lastName) as employeeName,
p.prodName,
si.quantity,
si.unitPrice,
(si.quantity * si.unitPrice) as lineTotal,
s.paymentMethod
FROM sale s
JOIN saleItem si ON s.saleId = si.saleId
JOIN product p ON si.prodId = p.prodId
JOIN employee e ON s.employeeId = e.employeeId
WHERE s.saleId LIKE ?
OR p.prodName LIKE ?
OR CONCAT(e.firstName, ' ', e.lastName) LIKE ?
OR s.paymentMethod LIKE ?
ORDER BY s.saleDate DESC, s.saleId, si.saleItemId
""";
PreparedStatement pstmt = conn.prepareStatement(sql);
String searchPattern = "%" + filter + "%";
pstmt.setString(1, searchPattern);
pstmt.setString(2, searchPattern);
pstmt.setString(3, searchPattern);
pstmt.setString(4, searchPattern);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
sales.add(new SaleDTO(
rs.getInt("saleId"),
rs.getString("saleDate"),
rs.getString("employeeName"),
rs.getString("prodName"),
rs.getInt("quantity"),
rs.getDouble("unitPrice"),
rs.getDouble("lineTotal"),
rs.getString("paymentMethod")
));
}
conn.close();
return sales;
}
}

View File

@@ -5,10 +5,6 @@ import org.example.petshopdesktop.models.User;
import java.sql.*;
/*
Petshop Desktop
Purpose: User authentication and role lookup against the users table.
*/
public class UserDB {
/**
@@ -34,8 +30,6 @@ public class UserDB {
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);
@@ -59,7 +53,6 @@ public class UserDB {
)
""";
// 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')