diff --git a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java
index 51aba3f9..76294154 100644
--- a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java
+++ b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java
@@ -190,8 +190,10 @@ public class MainLayoutController {
btnPurchaseOrders.setVisible(isAdmin);
btnPurchaseOrders.setManaged(isAdmin);
- btnStaffAccounts.setVisible(isAdmin);
- btnStaffAccounts.setManaged(isAdmin);
+ if (btnStaffAccounts != null) {
+ btnStaffAccounts.setVisible(isAdmin);
+ btnStaffAccounts.setManaged(isAdmin);
+ }
btnSalesHistory.setText(isAdmin ? "Sales History" : "Sales");
diff --git a/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java
new file mode 100644
index 00000000..2c3bd479
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java
@@ -0,0 +1,117 @@
+package org.example.petshopdesktop.controllers.dialogcontrollers;
+
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.PasswordField;
+import javafx.scene.control.TextField;
+import javafx.stage.Stage;
+import org.example.petshopdesktop.database.UserDB;
+import org.example.petshopdesktop.util.ActivityLogger;
+
+import java.sql.SQLException;
+
+public class StaffRegisterDialogController {
+
+ @FXML
+ private TextField txtFirstName;
+
+ @FXML
+ private TextField txtLastName;
+
+ @FXML
+ private TextField txtEmail;
+
+ @FXML
+ private TextField txtPhone;
+
+ @FXML
+ private TextField txtUsername;
+
+ @FXML
+ private PasswordField txtPassword;
+
+ @FXML
+ private PasswordField txtPasswordConfirm;
+
+ @FXML
+ private Label lblError;
+
+ @FXML
+ private Button btnCreate;
+
+ @FXML
+ void btnCreateClicked(ActionEvent event) {
+ lblError.setText("");
+
+ String firstName = value(txtFirstName);
+ String lastName = value(txtLastName);
+ String email = value(txtEmail);
+ String phone = value(txtPhone);
+ String username = value(txtUsername);
+ String password = txtPassword.getText() == null ? "" : txtPassword.getText();
+ String confirm = txtPasswordConfirm.getText() == null ? "" : txtPasswordConfirm.getText();
+
+ if (firstName.isBlank() || lastName.isBlank()) {
+ lblError.setText("First name and last name are required.");
+ return;
+ }
+ if (email.isBlank()) {
+ lblError.setText("Email is required.");
+ return;
+ }
+ if (phone.isBlank()) {
+ lblError.setText("Phone is required.");
+ return;
+ }
+ if (username.isBlank()) {
+ lblError.setText("Username is required.");
+ return;
+ }
+ if (password.isBlank()) {
+ lblError.setText("Password is required.");
+ return;
+ }
+ if (!password.equals(confirm)) {
+ lblError.setText("Passwords do not match.");
+ return;
+ }
+
+ try {
+ UserDB.createStaffAccount(firstName, lastName, email, phone, username, password);
+ Alert alert = new Alert(Alert.AlertType.INFORMATION);
+ alert.setTitle("Staff Account");
+ alert.setHeaderText(null);
+ alert.setContentText("Staff account created. You can log in now.");
+ alert.showAndWait();
+ close();
+ } catch (SQLException e) {
+ ActivityLogger.getInstance().logException("StaffRegisterDialogController.btnCreateClicked", e, "Creating staff account");
+ String msg = e.getMessage() == null ? "Could not create staff account." : e.getMessage();
+ if (msg.toLowerCase().contains("duplicate") || msg.toLowerCase().contains("unique")) {
+ lblError.setText("Username already exists.");
+ } else {
+ lblError.setText(msg);
+ }
+ } catch (RuntimeException e) {
+ ActivityLogger.getInstance().logException("StaffRegisterDialogController.btnCreateClicked", e, "Database connection");
+ lblError.setText("Database is not connected.");
+ }
+ }
+
+ @FXML
+ void btnCancelClicked(ActionEvent event) {
+ close();
+ }
+
+ private void close() {
+ Stage stage = (Stage) btnCreate.getScene().getWindow();
+ stage.close();
+ }
+
+ private static String value(TextField tf) {
+ return tf.getText() == null ? "" : tf.getText().trim();
+ }
+}
diff --git a/src/main/java/org/example/petshopdesktop/database/EmployeeDB.java b/src/main/java/org/example/petshopdesktop/database/EmployeeDB.java
new file mode 100644
index 00000000..18cd2167
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/database/EmployeeDB.java
@@ -0,0 +1,132 @@
+package org.example.petshopdesktop.database;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class EmployeeDB {
+
+ public static int ensureDefaultEmployee(String firstName, String lastName, String email, String phone, String role, boolean isActive) throws SQLException {
+ Integer existingId = findEmployeeIdByEmail(email);
+ if (existingId != null) {
+ return existingId;
+ }
+
+ try (Connection conn = ConnectionDB.getConnection()) {
+ conn.setAutoCommit(false);
+ try {
+ int storeId = getDefaultStoreId(conn);
+ int employeeId = createEmployee(conn, firstName, lastName, email, phone, role, isActive);
+ assignEmployeeToStore(conn, employeeId, storeId);
+ conn.commit();
+ return employeeId;
+ } catch (SQLException e) {
+ conn.rollback();
+ throw e;
+ } finally {
+ conn.setAutoCommit(true);
+ }
+ }
+ }
+
+ public static Integer findEmployeeIdByEmail(String email) throws SQLException {
+ String sql = "SELECT employeeId FROM employee WHERE email = ? LIMIT 1";
+ try (Connection conn = ConnectionDB.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setString(1, email);
+ try (ResultSet rs = ps.executeQuery()) {
+ if (rs.next()) {
+ return rs.getInt("employeeId");
+ }
+ }
+ }
+ return null;
+ }
+
+ public static int createEmployee(Connection conn, String firstName, String lastName, String email, String phone, String role, boolean isActive) throws SQLException {
+ String sql = "INSERT INTO employee (firstName, lastName, email, phone, role, isActive) VALUES (?, ?, ?, ?, ?, ?)";
+ try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
+ ps.setString(1, firstName);
+ ps.setString(2, lastName);
+ ps.setString(3, email);
+ ps.setString(4, phone);
+ ps.setString(5, role);
+ ps.setBoolean(6, isActive);
+ ps.executeUpdate();
+
+ try (ResultSet keys = ps.getGeneratedKeys()) {
+ if (keys.next()) {
+ return keys.getInt(1);
+ }
+ }
+ }
+
+ throw new SQLException("Could not create employee.");
+ }
+
+ public static void assignEmployeeToStore(Connection conn, int employeeId, int storeId) throws SQLException {
+ String sql = "INSERT IGNORE INTO employeeStore (employeeId, storeId) VALUES (?, ?)";
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setInt(1, employeeId);
+ ps.setInt(2, storeId);
+ ps.executeUpdate();
+ }
+ }
+
+ public static int getDefaultStoreId() throws SQLException {
+ try (Connection conn = ConnectionDB.getConnection()) {
+ return getDefaultStoreId(conn);
+ }
+ }
+
+ public static int getDefaultStoreId(Connection conn) throws SQLException {
+ Integer existing = firstStoreId(conn);
+ if (existing != null) {
+ return existing;
+ }
+
+ String insert = "INSERT INTO storeLocation (storeName, address, phone, email) VALUES ('Main Store', 'N/A', '000-000-0000', 'main@petshop.com')";
+ try (PreparedStatement ps = conn.prepareStatement(insert, Statement.RETURN_GENERATED_KEYS)) {
+ ps.executeUpdate();
+ try (ResultSet keys = ps.getGeneratedKeys()) {
+ if (keys.next()) {
+ return keys.getInt(1);
+ }
+ }
+ }
+
+ Integer after = firstStoreId(conn);
+ if (after != null) {
+ return after;
+ }
+
+ return 1;
+ }
+
+ public static Integer getPrimaryStoreId(int employeeId) throws SQLException {
+ String sql = "SELECT storeId FROM employeeStore WHERE employeeId = ? ORDER BY storeId ASC LIMIT 1";
+ try (Connection conn = ConnectionDB.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setInt(1, employeeId);
+ try (ResultSet rs = ps.executeQuery()) {
+ if (rs.next()) {
+ return rs.getInt("storeId");
+ }
+ }
+ }
+ return null;
+ }
+
+ private static Integer firstStoreId(Connection conn) throws SQLException {
+ String sql = "SELECT storeId FROM storeLocation ORDER BY storeId ASC LIMIT 1";
+ try (PreparedStatement ps = conn.prepareStatement(sql);
+ ResultSet rs = ps.executeQuery()) {
+ if (rs.next()) {
+ return rs.getInt("storeId");
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/org/example/petshopdesktop/models/SaleCartItem.java b/src/main/java/org/example/petshopdesktop/models/SaleCartItem.java
new file mode 100644
index 00000000..25c4da36
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/models/SaleCartItem.java
@@ -0,0 +1,39 @@
+package org.example.petshopdesktop.models;
+
+public class SaleCartItem {
+ private final int prodId;
+ private final String prodName;
+ private int quantity;
+ private final double unitPrice;
+
+ public SaleCartItem(int prodId, String prodName, int quantity, double unitPrice) {
+ this.prodId = prodId;
+ this.prodName = prodName;
+ this.quantity = quantity;
+ this.unitPrice = unitPrice;
+ }
+
+ public int getProdId() {
+ return prodId;
+ }
+
+ public String getProdName() {
+ return prodName;
+ }
+
+ public int getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(int quantity) {
+ this.quantity = quantity;
+ }
+
+ public double getUnitPrice() {
+ return unitPrice;
+ }
+
+ public double getTotal() {
+ return unitPrice * quantity;
+ }
+}
diff --git a/src/main/java/org/example/petshopdesktop/models/SaleLineItem.java b/src/main/java/org/example/petshopdesktop/models/SaleLineItem.java
new file mode 100644
index 00000000..d44e3662
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/models/SaleLineItem.java
@@ -0,0 +1,55 @@
+package org.example.petshopdesktop.models;
+
+public class SaleLineItem {
+ private final int saleId;
+ private final String saleDate;
+ private final String employeeName;
+ private final String itemName;
+ private final int quantity;
+ private final double unitPrice;
+ private final double total;
+ private final String paymentMethod;
+
+ public SaleLineItem(int saleId, String saleDate, String employeeName, String itemName, int quantity, double unitPrice, double total, String paymentMethod) {
+ this.saleId = saleId;
+ this.saleDate = saleDate;
+ this.employeeName = employeeName;
+ this.itemName = itemName;
+ this.quantity = quantity;
+ this.unitPrice = unitPrice;
+ this.total = total;
+ this.paymentMethod = paymentMethod;
+ }
+
+ public int getSaleId() {
+ return saleId;
+ }
+
+ public String getSaleDate() {
+ return saleDate;
+ }
+
+ public String getEmployeeName() {
+ return employeeName;
+ }
+
+ public String getItemName() {
+ return itemName;
+ }
+
+ public int getQuantity() {
+ return quantity;
+ }
+
+ public double getUnitPrice() {
+ return unitPrice;
+ }
+
+ public double getTotal() {
+ return total;
+ }
+
+ public String getPaymentMethod() {
+ return paymentMethod;
+ }
+}
diff --git a/src/main/java/org/example/petshopdesktop/models/StaffAccount.java b/src/main/java/org/example/petshopdesktop/models/StaffAccount.java
new file mode 100644
index 00000000..c17d0472
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/models/StaffAccount.java
@@ -0,0 +1,73 @@
+package org.example.petshopdesktop.models;
+
+import java.sql.Timestamp;
+
+public class StaffAccount {
+ private final int userId;
+ private final int employeeId;
+ private final String username;
+ private final String firstName;
+ private final String lastName;
+ private final String email;
+ private final String phone;
+ private final boolean active;
+ private final Timestamp createdAt;
+
+ public StaffAccount(int userId, int employeeId, String username, String firstName, String lastName, String email, String phone, boolean active, Timestamp createdAt) {
+ this.userId = userId;
+ this.employeeId = employeeId;
+ this.username = username;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.email = email;
+ this.phone = phone;
+ this.active = active;
+ this.createdAt = createdAt;
+ }
+
+ public int getUserId() {
+ return userId;
+ }
+
+ public int getEmployeeId() {
+ return employeeId;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public String getFullName() {
+ String fn = firstName == null ? "" : firstName.trim();
+ String ln = lastName == null ? "" : lastName.trim();
+ return (fn + " " + ln).trim();
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public Timestamp getCreatedAt() {
+ return createdAt;
+ }
+
+ public String getStatus() {
+ return active ? "Active" : "Inactive";
+ }
+}
diff --git a/src/main/java/org/example/petshopdesktop/util/ActivityLogger.java b/src/main/java/org/example/petshopdesktop/util/ActivityLogger.java
new file mode 100644
index 00000000..6a104a39
--- /dev/null
+++ b/src/main/java/org/example/petshopdesktop/util/ActivityLogger.java
@@ -0,0 +1,90 @@
+package org.example.petshopdesktop.util;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+public final class ActivityLogger {
+
+ private static final ActivityLogger INSTANCE = new ActivityLogger();
+
+ private static final String LOG_FILE_NAME = "log.txt";
+ private static final DateTimeFormatter TS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ private final Path logFilePath;
+
+ private ActivityLogger() {
+ this.logFilePath = resolveProjectRoot().resolve(LOG_FILE_NAME);
+ }
+
+ public static ActivityLogger getInstance() {
+ return INSTANCE;
+ }
+
+ public void logInsert(String tableName, String recordId, String details) {
+ write("INSERT", String.format("DB_INSERT | Table: %s | ID: %s | Details: %s", tableName, recordId, details));
+ }
+
+ public void logUpdate(String tableName, String recordId, String details) {
+ write("UPDATE", String.format("DB_UPDATE | Table: %s | ID: %s | Details: %s", tableName, recordId, details));
+ }
+
+ public void logDelete(String tableName, String recordId, String details) {
+ write("DELETE", String.format("DB_DELETE | Table: %s | ID: %s | Details: %s", tableName, recordId, details));
+ }
+
+ public void logException(String location, Exception exception, String context) {
+ write("ERROR", String.format(
+ "EXCEPTION | Location: %s | Type: %s | Message: %s | Context: %s",
+ location,
+ exception.getClass().getSimpleName(),
+ String.valueOf(exception.getMessage()),
+ context
+ ));
+ }
+
+ public void logInfo(String category, String message) {
+ write(category, message);
+ }
+
+ public Path getLogFilePath() {
+ return logFilePath;
+ }
+
+ private static Path resolveProjectRoot() {
+ Path start = Paths.get(System.getProperty("user.dir")).toAbsolutePath();
+ Path current = start;
+
+ for (int i = 0; i < 6 && current != null; i++) {
+ if (Files.exists(current.resolve("pom.xml"))
+ || Files.exists(current.resolve("mvnw"))
+ || Files.exists(current.resolve("connectionpetstore.properties.example"))) {
+ return current;
+ }
+ current = current.getParent();
+ }
+
+ return start;
+ }
+
+ private synchronized void write(String level, String message) {
+ String timestamp = LocalDateTime.now().format(TS);
+ String logEntry = String.format("[%s] [%s] %s%n", timestamp, level, message);
+
+ try {
+ Files.writeString(
+ logFilePath,
+ logEntry,
+ StandardCharsets.UTF_8,
+ StandardOpenOption.CREATE,
+ StandardOpenOption.APPEND
+ );
+ } catch (Exception e) {
+ System.err.println("Failed to write log entry: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/resources/org/example/petshopdesktop/dialogviews/staff-register-dialog-view.fxml b/src/main/resources/org/example/petshopdesktop/dialogviews/staff-register-dialog-view.fxml
new file mode 100644
index 00000000..2ad88420
--- /dev/null
+++ b/src/main/resources/org/example/petshopdesktop/dialogviews/staff-register-dialog-view.fxml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 fdf5c267..28f412c5 100644
--- a/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml
+++ b/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml
@@ -134,6 +134,15 @@
+
+