diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/CustomerAccountsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/CustomerAccountsController.java new file mode 100644 index 00000000..4de53100 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/CustomerAccountsController.java @@ -0,0 +1,172 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.example.petshopdesktop.api.dto.user.UserResponse; +import org.example.petshopdesktop.api.endpoints.CustomerApi; +import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TableViewSupport; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class CustomerAccountsController { + + @FXML + private TableView tvCustomers; + + @FXML + private TableColumn colCustomerUsername; + + @FXML + private TableColumn colCustomerName; + + @FXML + private TableColumn colCustomerEmail; + + @FXML + private TableColumn colCustomerPhone; + + @FXML + private TableColumn colCustomerLoyaltyPoints; + + @FXML + private TableColumn colCustomerStatus; + + @FXML + private TableColumn colCustomerCreated; + + @FXML + private TextField txtSearchCustomer; + + @FXML + private Button btnEditCustomer; + + @FXML + private Button btnRefresh; + + @FXML + private Label lblError; + + @FXML + private Label lblStatus; + + private final ObservableList customerAccounts = FXCollections.observableArrayList(); + private FilteredList filteredCustomers; + + @FXML + public void initialize() { + colCustomerUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername())); + colCustomerName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName())); + colCustomerEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail())); + colCustomerPhone.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getPhone())); + colCustomerLoyaltyPoints.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getLoyaltyPoints())); + colCustomerStatus.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getActive() != null && data.getValue().getActive() ? "Active" : "Inactive")); + colCustomerCreated.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getCreatedAt())); + + filteredCustomers = new FilteredList<>(customerAccounts, a -> true); + TableViewSupport.bindSortedItems(tvCustomers, filteredCustomers); + TableViewSupport.installDoubleClickAction(tvCustomers, this::openEditDialog); + + tvCustomers.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> + btnEditCustomer.setDisable(newVal == null)); + btnEditCustomer.setDisable(true); + + txtSearchCustomer.textProperty().addListener((obs, o, n) -> applyCustomerFilter(n)); + + refresh(); + } + + @FXML + void btnRefreshClicked(ActionEvent event) { + txtSearchCustomer.clear(); + TableViewSupport.clearSort(tvCustomers); + refresh(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); + } + + @FXML + void btnEditCustomerClicked(ActionEvent event) { + lblError.setText(""); + openEditDialog(tvCustomers.getSelectionModel().getSelectedItem()); + } + + private void openEditDialog(UserResponse selected) { + if (selected == null) { + lblError.setText("Select a customer to edit."); + return; + } + + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml")); + Stage dialog = new Stage(); + dialog.initOwner(tvCustomers.getScene().getWindow()); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Edit Customer Account"); + dialog.setScene(new Scene(loader.load())); + dialog.setResizable(false); + var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController(); + controller.setUser(selected); + dialog.showAndWait(); + refresh(); + } catch (Exception e) { + ActivityLogger.getInstance().logException("CustomerAccountsController.openEditDialog", e, "Opening customer edit dialog"); + lblError.setText("Could not open customer account editor."); + } + } + + private void refresh() { + lblError.setText(""); + tvCustomers.setDisable(true); + + new Thread(() -> { + try { + Comparator byCreated = Comparator.comparing( + UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder())); + + List customers = CustomerApi.getInstance().listCustomers(null).stream() + .sorted(byCreated) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + customerAccounts.setAll(customers); + tvCustomers.setDisable(false); + }); + } catch (Exception e) { + ActivityLogger.getInstance().logException("CustomerAccountsController.refresh", e, "Loading customer accounts"); + Platform.runLater(() -> { + lblError.setText("Could not load customer accounts."); + tvCustomers.setDisable(false); + }); + } + }).start(); + } + + private void applyCustomerFilter(String text) { + String q = text == null ? "" : text.trim().toLowerCase(); + if (q.isEmpty()) { + filteredCustomers.setPredicate(a -> true); + return; + } + filteredCustomers.setPredicate(a -> + safe(a.getUsername()).contains(q) + || safe(a.getFullName()).contains(q) + || safe(a.getEmail()).contains(q) + || safe(a.getPhone()).contains(q) + ); + } + + private static String safe(String v) { + return v == null ? "" : v.toLowerCase(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java index 61e7fa4e..997899d3 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java @@ -89,6 +89,9 @@ public class MainLayoutController { @FXML private Button btnStaffAccounts; + @FXML + private Button btnCustomers; + @FXML private Button btnAnalytics; @@ -179,6 +182,12 @@ public class MainLayoutController { updateButtons(btnStaffAccounts); } + @FXML + void btnCustomersClicked(ActionEvent event) { + loadView("customer-accounts-view.fxml"); + updateButtons(btnCustomers); + } + @FXML void btnAnalyticsClicked(ActionEvent event) { loadView("analytics-view.fxml"); @@ -415,8 +424,13 @@ public class MainLayoutController { btnPurchaseOrders.setManaged(isAdmin); if (btnStaffAccounts != null) { - btnStaffAccounts.setVisible(true); - btnStaffAccounts.setManaged(true); + btnStaffAccounts.setVisible(isAdmin); + btnStaffAccounts.setManaged(isAdmin); + } + + if (btnCustomers != null) { + btnCustomers.setVisible(true); + btnCustomers.setManaged(true); } if (lblAdminSection != null) { @@ -493,6 +507,7 @@ public class MainLayoutController { btnProducts, btnPurchaseOrders, btnStaffAccounts, + btnCustomers, btnAnalytics, btnActivityLogs, btnCoupons, diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index 3a90b787..9c7c6225 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -255,6 +255,8 @@ public class SaleController { boolean isAdmin = UserSession.getInstance().isAdmin(); vbCreateSale.setVisible(!isAdmin); vbCreateSale.setManaged(!isAdmin); + btnRefund.setVisible(!isAdmin); + btnRefund.setManaged(!isAdmin); lblModeNote.setText(isAdmin ? "(View only)" : "(Staff can create sales)"); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java index 20dbf6d0..76771784 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java @@ -8,16 +8,11 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; -import javafx.scene.control.TextField; +import javafx.scene.control.*; import javafx.scene.layout.VBox; import javafx.stage.Modality; import javafx.stage.Stage; import org.example.petshopdesktop.api.dto.user.UserResponse; -import org.example.petshopdesktop.api.endpoints.CustomerApi; import org.example.petshopdesktop.api.endpoints.UserApi; import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.util.ActivityLogger; @@ -32,36 +27,6 @@ public class StaffAccountsController { @FXML private VBox staffSection; - @FXML - private TableView tvCustomers; - - @FXML - private TableColumn colCustomerUsername; - - @FXML - private TableColumn colCustomerName; - - @FXML - private TableColumn colCustomerEmail; - - @FXML - private TableColumn colCustomerPhone; - - @FXML - private TableColumn colCustomerLoyaltyPoints; - - @FXML - private TableColumn colCustomerStatus; - - @FXML - private TableColumn colCustomerCreated; - - @FXML - private TextField txtSearchCustomer; - - @FXML - private Button btnEditCustomer; - @FXML private TableView tvStaff; @@ -104,32 +69,11 @@ public class StaffAccountsController { @FXML private Label lblStatus; - private final ObservableList customerAccounts = FXCollections.observableArrayList(); - private FilteredList filteredCustomers; - private final ObservableList staffAccounts = FXCollections.observableArrayList(); private FilteredList filteredStaff; @FXML public void initialize() { - colCustomerUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername())); - colCustomerName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName())); - colCustomerEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail())); - colCustomerPhone.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getPhone())); - colCustomerLoyaltyPoints.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getLoyaltyPoints())); - colCustomerStatus.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getActive() != null && data.getValue().getActive() ? "Active" : "Inactive")); - colCustomerCreated.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getCreatedAt())); - - filteredCustomers = new FilteredList<>(customerAccounts, a -> true); - TableViewSupport.bindSortedItems(tvCustomers, filteredCustomers); - TableViewSupport.installDoubleClickAction(tvCustomers, this::openEditDialog); - - tvCustomers.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> - btnEditCustomer.setDisable(newVal == null)); - btnEditCustomer.setDisable(true); - - txtSearchCustomer.textProperty().addListener((obs, o, n) -> applyCustomerFilter(n)); - colUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername())); colName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName())); colEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail())); @@ -148,29 +92,17 @@ public class StaffAccountsController { txtSearch.textProperty().addListener((obs, o, n) -> applyStaffFilter(n)); - boolean isAdmin = UserSession.getInstance().isAdmin(); - staffSection.setVisible(isAdmin); - staffSection.setManaged(isAdmin); - refresh(); } @FXML void btnRefreshClicked(ActionEvent event) { - txtSearchCustomer.clear(); txtSearch.clear(); - TableViewSupport.clearSort(tvCustomers); TableViewSupport.clearSort(tvStaff); refresh(); TableViewSupport.flashStatus(lblStatus, "Refreshed"); } - @FXML - void btnEditCustomerClicked(ActionEvent event) { - lblError.setText(""); - openEditDialog(tvCustomers.getSelectionModel().getSelectedItem()); - } - @FXML void btnCreateAccountClicked(ActionEvent event) { lblError.setText(""); @@ -214,10 +146,9 @@ public class StaffAccountsController { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml")); Stage dialog = new Stage(); - Stage owner = (tvStaff.getScene() != null) ? (Stage) tvStaff.getScene().getWindow() : (Stage) tvCustomers.getScene().getWindow(); - dialog.initOwner(owner); + dialog.initOwner(tvStaff.getScene().getWindow()); dialog.initModality(Modality.APPLICATION_MODAL); - dialog.setTitle("Edit User Account"); + dialog.setTitle("Edit Staff Account"); dialog.setScene(new Scene(loader.load())); dialog.setResizable(false); var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController(); @@ -232,7 +163,6 @@ public class StaffAccountsController { private void refresh() { lblError.setText(""); - tvCustomers.setDisable(true); tvStaff.setDisable(true); new Thread(() -> { @@ -240,60 +170,25 @@ public class StaffAccountsController { Comparator byCreated = Comparator.comparing( UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder())); - final List customers; - final List staff; - - if (UserSession.getInstance().isAdmin()) { - List allUsers = UserApi.getInstance().listUsers(null); - customers = allUsers.stream() - .filter(u -> "CUSTOMER".equalsIgnoreCase(u.getRole())) - .sorted(byCreated) - .collect(Collectors.toList()); - staff = allUsers.stream() - .filter(u -> !"CUSTOMER".equalsIgnoreCase(u.getRole())) - .sorted(byCreated) - .collect(Collectors.toList()); - } else { - customers = CustomerApi.getInstance().listCustomers(null).stream() - .sorted(byCreated) - .collect(Collectors.toList()); - staff = List.of(); - } + List staff = UserApi.getInstance().listUsers(null).stream() + .filter(u -> !"CUSTOMER".equalsIgnoreCase(u.getRole())) + .sorted(byCreated) + .collect(Collectors.toList()); Platform.runLater(() -> { - customerAccounts.setAll(customers); staffAccounts.setAll(staff); - tvCustomers.setDisable(false); tvStaff.setDisable(false); }); } catch (Exception e) { - ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading user accounts"); + ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading staff accounts"); Platform.runLater(() -> { - String message = e.getMessage(); - lblError.setText(message == null || message.isBlank() - ? "Could not load user accounts." - : "Could not load user accounts: " + message); - tvCustomers.setDisable(false); + lblError.setText("Could not load staff accounts."); tvStaff.setDisable(false); }); } }).start(); } - private void applyCustomerFilter(String text) { - String q = text == null ? "" : text.trim().toLowerCase(); - if (q.isEmpty()) { - filteredCustomers.setPredicate(a -> true); - return; - } - filteredCustomers.setPredicate(a -> - safe(a.getUsername()).contains(q) - || safe(a.getFullName()).contains(q) - || safe(a.getEmail()).contains(q) - || safe(a.getPhone()).contains(q) - ); - } - private void applyStaffFilter(String text) { String q = text == null ? "" : text.trim().toLowerCase(); if (q.isEmpty()) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java index abeb3355..6cd3bc28 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java @@ -13,6 +13,7 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.stage.Modality; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.auth.UserSession; import java.util.function.Consumer; @@ -46,6 +47,12 @@ public class SaleDetailDialogController { colQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); colUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); colLineTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + + if (btnRefund != null) { + boolean isAdmin = UserSession.getInstance().isAdmin(); + btnRefund.setVisible(!isAdmin); + btnRefund.setManaged(!isAdmin); + } } public void displaySaleDetails(SaleDetail sale) { @@ -57,7 +64,10 @@ public class SaleDetailDialogController { lblTotal.setText(currency.format(sale.getTotalAmount())); tvItems.setItems(sale.getItems()); if (btnRefund != null) { - btnRefund.setDisable(sale.isRefund()); + boolean isAdmin = UserSession.getInstance().isAdmin(); + if (!isAdmin) { + btnRefund.setDisable(sale.isRefund()); + } } } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml index a9478744..7192ea69 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml @@ -176,6 +176,14 @@ + @@ -220,7 +228,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +