From 33c9555564ad2e4ec80e9014f4c4af5df6c1a179 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 30 Mar 2026 09:16:52 -0600 Subject: [PATCH 1/3] 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 @@ - + - - - - - - - - + + + + + + + + From a3851871c7d3026251ed83a55a38a3e44e78d531 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 30 Mar 2026 09:17:22 -0600 Subject: [PATCH 2/3] Stabilize refunds --- .../api/dto/common/PageResponse.java | 66 ++++++- .../petshopdesktop/api/endpoints/SaleApi.java | 33 ++++ .../RefundDialogController.java | 162 ++++++++++++------ .../dialogviews/refund-dialog-view.fxml | 62 +++---- 4 files changed, 236 insertions(+), 87 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java index bbcc467c..5a27e2e9 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java @@ -9,15 +9,14 @@ import java.util.List; public class PageResponse { private List content; - @JsonProperty("number") private int pageNumber; - @JsonProperty("size") private int pageSize; private long totalElements; private int totalPages; private boolean last; + private PageMetadata page; public PageResponse() { } @@ -63,10 +62,71 @@ public class PageResponse { } public boolean isLast() { - return last; + if (last) { + return true; + } + if (page != null) { + return page.number >= Math.max(0, page.totalPages - 1); + } + return content == null || content.isEmpty(); } public void setLast(boolean last) { this.last = last; } + + public PageMetadata getPage() { + return page; + } + + public void setPage(PageMetadata page) { + this.page = page; + if (page != null) { + this.pageNumber = page.number; + this.pageSize = page.size; + this.totalElements = page.totalElements; + this.totalPages = page.totalPages; + this.last = page.number >= Math.max(0, page.totalPages - 1); + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class PageMetadata { + private int size; + private int number; + private long totalElements; + private int totalPages; + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + public long getTotalElements() { + return totalElements; + } + + public void setTotalElements(long totalElements) { + this.totalElements = totalElements; + } + + public int getTotalPages() { + return totalPages; + } + + public void setTotalPages(int totalPages) { + this.totalPages = totalPages; + } + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java index d3355012..a087fd9f 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java @@ -38,6 +38,39 @@ public class SaleApi { return pageResponse.getContent(); } + public List listAllSales(String query) throws Exception { + int page = 0; + int size = 250; + List allSales = new java.util.ArrayList<>(); + + while (true) { + String path = "/api/v1/sales?page=" + page + "&size=" + size; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from sales endpoint"); + } + + if (pageResponse.getContent() != null) { + allSales.addAll(pageResponse.getContent()); + } + + if (pageResponse.isLast()) { + break; + } + page++; + } + + return allSales; + } + public SaleResponse getSale(Long id) throws Exception { return apiClient.get("/api/v1/sales/" + id, SaleResponse.class); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java index 860a9921..bc262e30 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java @@ -4,8 +4,12 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.concurrent.Task; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.ReadOnlyStringWrapper; import javafx.scene.control.*; import javafx.scene.control.cell.PropertyValueFactory; +import javafx.application.Platform; import javafx.stage.Stage; import org.example.petshopdesktop.api.dto.sale.SaleItemRequest; import org.example.petshopdesktop.api.dto.sale.SaleItemResponse; @@ -90,6 +94,7 @@ public class RefundDialogController { private final ObservableList originalItems = FXCollections.observableArrayList(); private final ObservableList refundItems = FXCollections.observableArrayList(); private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); + private boolean refundInProgress; @FXML public void initialize() { @@ -100,17 +105,19 @@ public class RefundDialogController { } private void setupTables() { - colOriginalProduct.setCellValueFactory(new PropertyValueFactory<>("productName")); - colOriginalQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); - colOriginalUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); - colOriginalTotal.setCellValueFactory(new PropertyValueFactory<>("lineTotal")); + tvOriginalItems.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + colOriginalProduct.setCellValueFactory(cell -> new ReadOnlyStringWrapper(cell.getValue().getProductName())); + colOriginalQuantity.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getQuantity())); + colOriginalUnitPrice.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getUnitPrice())); + colOriginalTotal.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getLineTotal())); tvOriginalItems.setItems(originalItems); tvOriginalItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); - colRefundProduct.setCellValueFactory(new PropertyValueFactory<>("productName")); - colRefundQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); - colRefundUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); - colRefundTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + tvRefundItems.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + colRefundProduct.setCellValueFactory(cell -> new ReadOnlyStringWrapper(cell.getValue().getProductName())); + colRefundQuantity.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getQuantity())); + colRefundUnitPrice.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getUnitPrice())); + colRefundTotal.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getTotal())); tvRefundItems.setItems(refundItems); tvRefundItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); } @@ -125,12 +132,13 @@ public class RefundDialogController { return; } txtSaleId.setText(String.valueOf(saleId)); - loadSale(); + Platform.runLater(this::loadSale); } private void loadSale() { String saleIdText = txtSaleId.getText().trim(); if (saleIdText.isEmpty()) { + clearLoadedSale(); showError("Load Sale", "Enter a transaction ID."); return; } @@ -139,22 +147,36 @@ public class RefundDialogController { try { saleId = Long.parseLong(saleIdText); } catch (NumberFormatException e) { + clearLoadedSale(); showError("Load Sale", "Invalid transaction ID."); return; } - try { - List allSales = SaleApi.getInstance().listSales(0, 1000, null); - currentSale = SaleApi.getInstance().getSale(saleId); - if (Boolean.TRUE.equals(currentSale.getIsRefund())) { - clearLoadedSale(); - showError("Load Sale", "Select an original sale, not a refund record."); - return; - } - List previousRefunds = allSales.stream() - .filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId())) - .collect(Collectors.toList()); + setLoadingState(true, "Loading sale..."); + clearLoadedSale(); + Task task = new Task<>() { + @Override + protected LoadedSaleData call() throws Exception { + List allSales = SaleApi.getInstance().listAllSales(null); + SaleResponse sale = SaleApi.getInstance().getSale(saleId); + if (Boolean.TRUE.equals(sale.getIsRefund())) { + throw new IllegalStateException("Select an original sale, not a refund record."); + } + List previousRefunds = allSales.stream() + .filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId())) + .collect(Collectors.toList()); + List refundableItems = buildRefundableItems(sale, previousRefunds); + if (refundableItems.isEmpty()) { + throw new IllegalStateException("This sale has no remaining refundable items."); + } + return new LoadedSaleData(sale, refundableItems); + } + }; + + task.setOnSucceeded(event -> { + LoadedSaleData loaded = task.getValue(); + currentSale = loaded.sale(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); String saleInfo = String.format("Sale Date: %s | Employee: %s | Original Total: %s | Payment: %s", currentSale.getSaleDate().format(formatter), @@ -162,26 +184,25 @@ public class RefundDialogController { currency.format(currentSale.getTotalAmount()), currentSale.getPaymentMethod()); lblSaleInfo.setText(saleInfo); - - List refundableItems = buildRefundableItems(currentSale, previousRefunds); - if (refundableItems.isEmpty()) { - showError("Load Sale", "This sale has no remaining refundable items."); - return; - } - baseOriginalItems.clear(); - baseOriginalItems.addAll(copySaleItems(refundableItems)); - originalItems.setAll(copySaleItems(refundableItems)); + baseOriginalItems.addAll(copySaleItems(loaded.refundableItems())); + originalItems.setAll(copySaleItems(loaded.refundableItems())); cbPaymentMethod.getSelectionModel().select(currentSale.getPaymentMethod()); - refundItems.clear(); updateOriginalItemAvailability(); updateRefundTotal(); + setLoadingState(false, saleInfo); + }); - } catch (Exception e) { - ActivityLogger.getInstance().logException("RefundDialogController.btnLoadSaleClicked", e, "Loading sale"); + task.setOnFailed(event -> { + clearLoadedSale(); + Throwable e = task.getException(); + ActivityLogger.getInstance().logException("RefundDialogController.btnLoadSaleClicked", (Exception) e, "Loading sale"); + setLoadingState(false, ""); showError("Load Sale", e.getMessage() != null ? e.getMessage() : "Could not load sale."); - } + }); + + new Thread(task).start(); } @FXML @@ -248,6 +269,9 @@ public class RefundDialogController { @FXML void btnProcessRefundClicked(ActionEvent event) { + if (refundInProgress) { + return; + } if (currentSale == null) { showError("Process Refund", "Load a sale first."); return; @@ -280,36 +304,53 @@ public class RefundDialogController { return; } - try { - SaleRequest request = new SaleRequest(); - request.setStoreId(storeId); - request.setPaymentMethod(payment); - request.setIsRefund(true); - request.setOriginalSaleId(currentSale.getSaleId()); + SaleRequest request = new SaleRequest(); + request.setStoreId(storeId); + request.setPaymentMethod(payment); + request.setIsRefund(true); + request.setOriginalSaleId(currentSale.getSaleId()); - List items = new ArrayList<>(); - for (RefundItem item : refundItems) { - SaleItemRequest saleItem = new SaleItemRequest(); - saleItem.setProdId((long) item.getProdId()); - saleItem.setQuantity(-item.getQuantity()); - items.add(saleItem); + List items = new ArrayList<>(); + for (RefundItem item : refundItems) { + SaleItemRequest saleItem = new SaleItemRequest(); + saleItem.setProdId((long) item.getProdId()); + saleItem.setQuantity(-item.getQuantity()); + items.add(saleItem); + } + request.setItems(items); + + refundInProgress = true; + setLoadingState(true, lblSaleInfo.getText()); + + Task task = new Task<>() { + @Override + protected SaleResponse call() throws Exception { + return SaleApi.getInstance().createSale(request); } - request.setItems(items); - - SaleResponse refundResponse = SaleApi.getInstance().createSale(request); + }; + task.setOnSucceeded(evt -> { + refundInProgress = false; + setLoadingState(false, lblSaleInfo.getText()); + SaleResponse refundResponse = task.getValue(); Alert success = new Alert(Alert.AlertType.INFORMATION); success.setTitle("Refund Processed"); success.setHeaderText(null); success.setContentText("Refund ID " + refundResponse.getSaleId() + " was created successfully."); success.showAndWait(); - closeDialog(); + }); - } catch (Exception e) { - ActivityLogger.getInstance().logException("RefundDialogController.btnProcessRefundClicked", e, "Processing refund"); - showError("Process Refund", e.getMessage() != null ? e.getMessage() : "Could not process refund."); - } + task.setOnFailed(evt -> { + refundInProgress = false; + setLoadingState(false, lblSaleInfo.getText()); + Throwable e = task.getException(); + Exception ex = e instanceof Exception ? (Exception) e : new RuntimeException(e); + ActivityLogger.getInstance().logException("RefundDialogController.btnProcessRefundClicked", ex, "Processing refund"); + showError("Process Refund", e != null && e.getMessage() != null ? e.getMessage() : "Could not process refund."); + }); + + new Thread(task).start(); } @FXML @@ -326,6 +367,18 @@ public class RefundDialogController { updateRefundTotal(); } + private void setLoadingState(boolean loading, String message) { + btnLoadSale.setDisable(loading); + btnAddToRefund.setDisable(loading); + btnRemoveFromRefund.setDisable(loading); + btnProcessRefund.setDisable(loading); + txtSaleId.setDisable(loading); + cbPaymentMethod.setDisable(loading); + if (loading) { + lblSaleInfo.setText(message); + } + } + private void addOrMergeRefundItem(SaleItemResponse selected, int quantity) { for (int i = 0; i < refundItems.size(); i++) { RefundItem existing = refundItems.get(i); @@ -486,4 +539,7 @@ public class RefundDialogController { return quantity * unitPrice; } } + + private record LoadedSaleData(SaleResponse sale, List refundableItems) { + } } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml index b349a1d9..aad2f4c7 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml @@ -13,7 +13,7 @@ - + @@ -84,14 +84,14 @@ - - - - - - - - + From 4ef913dfd025a3a776bfac723f0ebe24dd0bef1a Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 30 Mar 2026 09:40:22 -0600 Subject: [PATCH 3/3] Fix refund display --- .../controllers/SaleController.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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 7e80c531..22c96dbd 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -258,18 +258,23 @@ public class SaleController { if (sale.getItems() != null && !sale.getItems().isEmpty()) { for (SaleItemResponse item : sale.getItems()) { + boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund(); double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0; - double lineTotal = unitPrice * item.getQuantity(); + int quantity = item.getQuantity() != null ? item.getQuantity() : 0; + if (isRefund && quantity > 0) { + quantity = -quantity; + } + double lineTotal = unitPrice * quantity; lineItems.add(new SaleLineItem( sale.getSaleId().intValue(), saleDate, sale.getEmployeeName(), item.getProductName(), - item.getQuantity(), + quantity, unitPrice, lineTotal, sale.getPaymentMethod(), - sale.getIsRefund() != null && sale.getIsRefund() + isRefund )); } } @@ -501,9 +506,13 @@ public class SaleController { private SaleDetail mapToSaleDetail(SaleResponse sale) { ObservableList items = FXCollections.observableArrayList(); if (sale.getItems() != null) { + boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund(); for (SaleItemResponse item : sale.getItems()) { double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0; int quantity = item.getQuantity() != null ? item.getQuantity() : 0; + if (isRefund && quantity > 0) { + quantity = -quantity; + } items.add(new SaleDetail.SaleDetailItem( item.getProdId() != null ? item.getProdId().intValue() : 0, item.getProductName(),