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