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 @@ + + + + + + + + + + + + + + + + + + + + +