From 07cd4f6c0e0f63779408d7639ec388cd432c7175 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 30 Mar 2026 09:16:52 -0600 Subject: [PATCH] Polish sales tables --- .../controllers/AdoptionController.java | 3 + .../controllers/AppointmentController.java | 3 + .../controllers/PurchaseOrderController.java | 4 +- .../controllers/SaleController.java | 216 ++++++++++++++---- .../controllers/StaffAccountsController.java | 2 + .../SaleDetailDialogController.java | 54 +++++ .../dialogviews/sale-detail-dialog-view.fxml | 61 +++++ .../petshopdesktop/modelviews/sale-view.fxml | 22 +- 8 files changed, 313 insertions(+), 52 deletions(-) create mode 100644 desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java create mode 100644 desktop/src/main/resources/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java index 65edcebd..564e6205 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java @@ -18,6 +18,7 @@ import org.example.petshopdesktop.models.Adoption; import org.example.petshopdesktop.util.ActivityLogger; import java.io.IOException; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -168,6 +169,7 @@ public class AdoptionController { List adoptions = AdoptionApi.getInstance().listAdoptions(filter); List adoptionList = adoptions.stream() .map(this::mapToAdoption) + .sorted(Comparator.comparing(Adoption::getAdoptionDate).reversed()) .collect(Collectors.toList()); Platform.runLater(() -> { @@ -193,6 +195,7 @@ public class AdoptionController { List adoptions = AdoptionApi.getInstance().listAdoptions(null); List adoptionList = adoptions.stream() .map(this::mapToAdoption) + .sorted(Comparator.comparing(Adoption::getAdoptionDate).reversed()) .collect(Collectors.toList()); Platform.runLater(() -> { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index 221140b0..bd5dc392 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -20,6 +20,7 @@ import org.example.petshopdesktop.controllers.dialogcontrollers.AppointmentDialo import org.example.petshopdesktop.util.ActivityLogger; import java.util.List; +import java.util.Comparator; import java.util.stream.Collectors; public class AppointmentController { @@ -81,6 +82,7 @@ public class AppointmentController { List responses = AppointmentApi.getInstance().listAppointments(null); List appointmentDTOs = responses.stream() .map(this::mapToAppointmentDTO) + .sorted(Comparator.comparing((AppointmentDTO a) -> a.getAppointmentDate() + "T" + a.getAppointmentTime()).reversed()) .collect(Collectors.toList()); Platform.runLater(() -> { @@ -105,6 +107,7 @@ public class AppointmentController { List responses = AppointmentApi.getInstance().listAppointments(query); List appointmentDTOs = responses.stream() .map(this::mapToAppointmentDTO) + .sorted(Comparator.comparing((AppointmentDTO a) -> a.getAppointmentDate() + "T" + a.getAppointmentTime()).reversed()) .collect(Collectors.toList()); Platform.runLater(() -> { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java index 71ec81ec..d8ea54b6 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java @@ -13,6 +13,7 @@ import org.example.petshopdesktop.api.endpoints.PurchaseOrderApi; import org.example.petshopdesktop.util.ActivityLogger; import java.util.List; +import java.util.Comparator; import java.util.stream.Collectors; public class PurchaseOrderController { @@ -63,6 +64,7 @@ public class PurchaseOrderController { List responses = PurchaseOrderApi.getInstance().listPurchaseOrders(null); List dtos = responses.stream() .map(this::mapToPurchaseOrderDTO) + .sorted(Comparator.comparing(PurchaseOrderDTO::getOrderDate).reversed()) .collect(Collectors.toList()); Platform.runLater(() -> { @@ -118,4 +120,4 @@ public class PurchaseOrderController { response.getOrderStatus() ); } -} \ No newline at end of file +} 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 c3866b1e..7e80c531 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -6,6 +6,8 @@ import javafx.collections.transformation.FilteredList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; +import javafx.application.Platform; +import javafx.scene.input.MouseButton; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Button; @@ -32,6 +34,7 @@ import org.example.petshopdesktop.api.dto.sale.SaleRequest; import org.example.petshopdesktop.api.dto.sale.SaleResponse; import org.example.petshopdesktop.models.Product; import org.example.petshopdesktop.models.SaleCartItem; +import org.example.petshopdesktop.models.SaleDetail; import org.example.petshopdesktop.models.SaleLineItem; import org.example.petshopdesktop.util.ActivityLogger; @@ -39,6 +42,7 @@ import java.math.BigDecimal; import java.text.NumberFormat; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Locale; @@ -128,6 +132,7 @@ public class SaleController { private final ObservableList cartItems = FXCollections.observableArrayList(); private final ObservableList saleItems = FXCollections.observableArrayList(); private FilteredList filteredSales; + private boolean saleSaveInProgress; private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); @@ -165,6 +170,15 @@ public class SaleController { filteredSales = new FilteredList<>(saleItems, s -> true); tvSales.setItems(filteredSales); + tvSales.setOnMouseClicked(event -> { + if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { + SaleLineItem selected = tvSales.getSelectionModel().getSelectedItem(); + if (selected != null) { + openSaleDetailDialog(selected.getSaleId()); + } + } + }); + txtSearch.textProperty().addListener((obs, oldVal, newVal) -> applySalesFilter(newVal)); } @@ -177,22 +191,43 @@ public class SaleController { updateCartTotal(); - try { - List productResponses = ProductApi.getInstance().listProducts(null); - ObservableList products = FXCollections.observableArrayList(); - for (ProductResponse pr : productResponses) { - products.add(new Product( - pr.getProdId().intValue(), - pr.getProdName(), - pr.getProdPrice().doubleValue(), - 0, - pr.getProdDesc() - )); + setCreateSaleControlsDisabled(true); + + Task> task = new Task<>() { + @Override + protected ObservableList call() throws Exception { + List productResponses = ProductApi.getInstance().listProducts(null); + ObservableList products = FXCollections.observableArrayList(); + for (ProductResponse pr : productResponses) { + products.add(new Product( + pr.getProdId().intValue(), + pr.getProdName(), + pr.getProdPrice().doubleValue(), + 0, + pr.getProdDesc() + )); + } + return products; } - cbProduct.setItems(products); - } catch (Exception e) { - ActivityLogger.getInstance().logException("SaleController.setupCreateSale", e, "Loading products"); - } + }; + + task.setOnSucceeded(event -> { + cbProduct.setItems(task.getValue()); + setCreateSaleControlsDisabled(false); + }); + + task.setOnFailed(event -> { + Throwable e = task.getException(); + ActivityLogger.getInstance().logException( + "SaleController.setupCreateSale", + e instanceof Exception ? (Exception) e : new RuntimeException(e), + "Loading products" + ); + setCreateSaleControlsDisabled(false); + showError("Sales", "Could not load products. Check the backend connection and refresh the view."); + }); + + new Thread(task).start(); } private void applyRoleMode() { @@ -207,10 +242,13 @@ public class SaleController { } private void refreshSales(boolean showErrorDialog) { + btnRefresh.setDisable(true); Task> task = new Task>() { @Override protected List call() throws Exception { - List sales = SaleApi.getInstance().listSales(0, 1000, null); + List sales = SaleApi.getInstance().listAllSales(null); + sales.sort(Comparator.comparing(SaleResponse::getSaleDate, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(SaleResponse::getSaleId, Comparator.nullsLast(Comparator.reverseOrder()))); List lineItems = new ArrayList<>(); for (SaleResponse sale : sales) { @@ -242,14 +280,14 @@ public class SaleController { task.setOnSucceeded(event -> { saleItems.setAll(task.getValue()); + btnRefresh.setDisable(false); }); task.setOnFailed(event -> { Throwable e = task.getException(); ActivityLogger.getInstance().logException("SaleController.refreshSales", (Exception) e, "Loading sales"); - if (showErrorDialog) { - showError("Sales", "Could not load sales: " + e.getMessage()); - } + btnRefresh.setDisable(false); + showError("Sales", "Could not load sales: " + e.getMessage()); }); new Thread(task).start(); @@ -310,6 +348,9 @@ public class SaleController { @FXML void btnSaveSale(ActionEvent event) { + if (saleSaveInProgress) { + return; + } if (UserSession.getInstance().isAdmin()) { showError("Create Sale", "This action is restricted to staff."); return; @@ -332,36 +373,57 @@ public class SaleController { return; } - try { - SaleRequest request = new SaleRequest(); - request.setStoreId(storeId); - request.setPaymentMethod(payment); + SaleRequest request = new SaleRequest(); + request.setStoreId(storeId); + request.setPaymentMethod(payment); - List itemRequests = new ArrayList<>(); - for (SaleCartItem cartItem : cartItems) { - SaleItemRequest itemRequest = new SaleItemRequest(); - itemRequest.setProdId((long) cartItem.getProdId()); - itemRequest.setQuantity(cartItem.getQuantity()); - itemRequests.add(itemRequest); + List itemRequests = new ArrayList<>(); + for (SaleCartItem cartItem : cartItems) { + SaleItemRequest itemRequest = new SaleItemRequest(); + itemRequest.setProdId((long) cartItem.getProdId()); + itemRequest.setQuantity(cartItem.getQuantity()); + itemRequests.add(itemRequest); + } + request.setItems(itemRequests); + + saleSaveInProgress = true; + setCreateSaleControlsDisabled(true); + btnRefund.setDisable(true); + + Task task = new Task<>() { + @Override + protected SaleResponse call() throws Exception { + return SaleApi.getInstance().createSale(request); } - request.setItems(itemRequests); + }; - SaleResponse response = SaleApi.getInstance().createSale(request); + task.setOnSucceeded(evt -> { + saleSaveInProgress = false; + setCreateSaleControlsDisabled(false); + btnRefund.setDisable(false); + SaleResponse response = task.getValue(); showInfo("Sale saved", "Sale ID " + response.getSaleId() + " was created."); - cartItems.clear(); updateCartTotal(); - refreshSales(true); - } catch (Exception e) { - ActivityLogger.getInstance().logException("SaleController.btnSaveSale", e, "Creating sale"); - String errorMsg = e.getMessage(); + }); + + task.setOnFailed(evt -> { + saleSaveInProgress = false; + setCreateSaleControlsDisabled(false); + btnRefund.setDisable(false); + Throwable e = task.getException(); + Exception ex = e instanceof Exception ? (Exception) e : new RuntimeException(e); + ActivityLogger.getInstance().logException("SaleController.btnSaveSale", ex, "Creating sale"); + String errorMsg = e != null ? e.getMessage() : null; if (errorMsg != null && errorMsg.contains("Insufficient inventory")) { showError("Create Sale", "Insufficient stock for one or more items."); } else { showError("Create Sale", errorMsg != null ? errorMsg : "Could not save the sale."); } - } + }); + + new Thread(task).start(); } @FXML @@ -384,11 +446,13 @@ public class SaleController { dialog.initModality(Modality.APPLICATION_MODAL); dialog.setTitle("Process Refund"); dialog.setScene(new Scene(loader.load())); + var controller = loader.getController(); if (selectedSale != null) { - loader.getController() - .prefillSale((long) selectedSale.getSaleId()); + controller.prefillSale((long) selectedSale.getSaleId()); } - dialog.setResizable(false); + dialog.setMinWidth(860); + dialog.setMinHeight(680); + dialog.setResizable(true); dialog.showAndWait(); refreshSales(true); @@ -397,11 +461,83 @@ public class SaleController { } } + private void openSaleDetailDialog(int saleId) { + Task task = new Task<>() { + @Override + protected SaleResponse call() throws Exception { + return SaleApi.getInstance().getSale((long) saleId); + } + }; + + task.setOnSucceeded(event -> { + try { + SaleResponse sale = task.getValue(); + FXMLLoader loader = new FXMLLoader(getClass().getResource( + "/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml")); + Stage dialog = new Stage(); + dialog.initOwner(tvSales.getScene().getWindow()); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Sale Details"); + dialog.setScene(new Scene(loader.load())); + var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.SaleDetailDialogController) loader.getController(); + controller.displaySaleDetails(mapToSaleDetail(sale)); + dialog.setResizable(false); + dialog.showAndWait(); + } catch (Exception e) { + ActivityLogger.getInstance().logException("SaleController.openSaleDetailDialog", e, "Opening sale detail dialog"); + showError("Sale Details", "Could not open the sale details."); + } + }); + + task.setOnFailed(event -> { + Throwable e = task.getException(); + ActivityLogger.getInstance().logException("SaleController.openSaleDetailDialog", (Exception) e, "Loading sale detail"); + showError("Sale Details", "Could not open the sale details."); + }); + + new Thread(task).start(); + } + + private SaleDetail mapToSaleDetail(SaleResponse sale) { + ObservableList items = FXCollections.observableArrayList(); + if (sale.getItems() != null) { + for (SaleItemResponse item : sale.getItems()) { + double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0; + int quantity = item.getQuantity() != null ? item.getQuantity() : 0; + items.add(new SaleDetail.SaleDetailItem( + item.getProdId() != null ? item.getProdId().intValue() : 0, + item.getProductName(), + quantity, + unitPrice, + unitPrice * quantity + )); + } + } + return new SaleDetail( + sale.getSaleId().intValue(), + sale.getSaleDate(), + sale.getTotalAmount() != null ? sale.getTotalAmount().doubleValue() : 0.0, + sale.getPaymentMethod(), + sale.getEmployeeName(), + items + ); + } + private void updateCartTotal() { double total = cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum(); lblCartTotal.setText(currency.format(total)); } + private void setCreateSaleControlsDisabled(boolean disabled) { + cbProduct.setDisable(disabled); + spQuantity.setDisable(disabled); + btnAddToCart.setDisable(disabled); + btnRemoveSelected.setDisable(disabled); + cbPaymentMethod.setDisable(disabled); + btnClearCart.setDisable(disabled); + btnSaveSale.setDisable(disabled); + } + private void applySalesFilter(String filter) { String f = filter == null ? "" : filter.trim().toLowerCase(); if (f.isEmpty()) { 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 a02674e4..603b85fa 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java @@ -25,6 +25,7 @@ import org.example.petshopdesktop.util.ActivityLogger; import java.sql.Timestamp; import java.time.ZoneId; import java.util.List; +import java.util.Comparator; import java.util.stream.Collectors; public class StaffAccountsController { @@ -161,6 +162,7 @@ public class StaffAccountsController { List employees = EmployeeApi.getInstance().listEmployees(null); List accounts = employees.stream() .map(this::mapToStaffAccount) + .sorted(Comparator.comparing(StaffAccount::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder()))) .collect(Collectors.toList()); Platform.runLater(() -> { 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 new file mode 100644 index 00000000..3a0c67cc --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java @@ -0,0 +1,54 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Stage; +import org.example.petshopdesktop.models.SaleDetail; + +import java.text.NumberFormat; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +public class SaleDetailDialogController { + + @FXML private Label lblSaleId; + @FXML private Label lblSaleDate; + @FXML private Label lblEmployee; + @FXML private Label lblPayment; + @FXML private Label lblTotal; + @FXML private TableView tvItems; + @FXML private TableColumn colProduct; + @FXML private TableColumn colQuantity; + @FXML private TableColumn colUnitPrice; + @FXML private TableColumn colLineTotal; + + private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + @FXML + public void initialize() { + tvItems.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + colProduct.setCellValueFactory(new PropertyValueFactory<>("productName")); + colQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); + colUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); + colLineTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + } + + public void displaySaleDetails(SaleDetail sale) { + lblSaleId.setText(String.valueOf(sale.getSaleId())); + lblSaleDate.setText(sale.getSaleDate() != null ? sale.getSaleDate().format(DATE_FORMATTER) : ""); + lblEmployee.setText(sale.getEmployeeName() != null ? sale.getEmployeeName() : ""); + lblPayment.setText(sale.getPaymentMethod() != null ? sale.getPaymentMethod() : ""); + lblTotal.setText(currency.format(sale.getTotalAmount())); + tvItems.setItems(sale.getItems()); + } + + @FXML + void btnCloseClicked() { + Stage stage = (Stage) tvItems.getScene().getWindow(); + stage.close(); + } +} diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml new file mode 100644 index 00000000..a0964b27 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -151,16 +151,16 @@ - + - - - - - - - - + + + + + + + +