Merge pull request #58 from RecentRunner/refund-layout-spacing
Refund polish
This commit is contained in:
@@ -9,15 +9,14 @@ import java.util.List;
|
|||||||
public class PageResponse<T> {
|
public class PageResponse<T> {
|
||||||
private List<T> content;
|
private List<T> content;
|
||||||
|
|
||||||
@JsonProperty("number")
|
|
||||||
private int pageNumber;
|
private int pageNumber;
|
||||||
|
|
||||||
@JsonProperty("size")
|
|
||||||
private int pageSize;
|
private int pageSize;
|
||||||
|
|
||||||
private long totalElements;
|
private long totalElements;
|
||||||
private int totalPages;
|
private int totalPages;
|
||||||
private boolean last;
|
private boolean last;
|
||||||
|
private PageMetadata page;
|
||||||
|
|
||||||
public PageResponse() {
|
public PageResponse() {
|
||||||
}
|
}
|
||||||
@@ -63,10 +62,71 @@ public class PageResponse<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLast() {
|
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) {
|
public void setLast(boolean last) {
|
||||||
this.last = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,39 @@ public class SaleApi {
|
|||||||
return pageResponse.getContent();
|
return pageResponse.getContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<SaleResponse> listAllSales(String query) throws Exception {
|
||||||
|
int page = 0;
|
||||||
|
int size = 250;
|
||||||
|
List<SaleResponse> 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<SaleResponse> pageResponse = apiClient.getObjectMapper().readValue(
|
||||||
|
response,
|
||||||
|
new TypeReference<PageResponse<SaleResponse>>() {}
|
||||||
|
);
|
||||||
|
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 {
|
public SaleResponse getSale(Long id) throws Exception {
|
||||||
return apiClient.get("/api/v1/sales/" + id, SaleResponse.class);
|
return apiClient.get("/api/v1/sales/" + id, SaleResponse.class);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.example.petshopdesktop.models.Adoption;
|
|||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -168,6 +169,7 @@ public class AdoptionController {
|
|||||||
List<AdoptionResponse> adoptions = AdoptionApi.getInstance().listAdoptions(filter);
|
List<AdoptionResponse> adoptions = AdoptionApi.getInstance().listAdoptions(filter);
|
||||||
List<Adoption> adoptionList = adoptions.stream()
|
List<Adoption> adoptionList = adoptions.stream()
|
||||||
.map(this::mapToAdoption)
|
.map(this::mapToAdoption)
|
||||||
|
.sorted(Comparator.comparing(Adoption::getAdoptionDate).reversed())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
@@ -193,6 +195,7 @@ public class AdoptionController {
|
|||||||
List<AdoptionResponse> adoptions = AdoptionApi.getInstance().listAdoptions(null);
|
List<AdoptionResponse> adoptions = AdoptionApi.getInstance().listAdoptions(null);
|
||||||
List<Adoption> adoptionList = adoptions.stream()
|
List<Adoption> adoptionList = adoptions.stream()
|
||||||
.map(this::mapToAdoption)
|
.map(this::mapToAdoption)
|
||||||
|
.sorted(Comparator.comparing(Adoption::getAdoptionDate).reversed())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import org.example.petshopdesktop.controllers.dialogcontrollers.AppointmentDialo
|
|||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class AppointmentController {
|
public class AppointmentController {
|
||||||
@@ -81,6 +82,7 @@ public class AppointmentController {
|
|||||||
List<AppointmentResponse> responses = AppointmentApi.getInstance().listAppointments(null);
|
List<AppointmentResponse> responses = AppointmentApi.getInstance().listAppointments(null);
|
||||||
List<AppointmentDTO> appointmentDTOs = responses.stream()
|
List<AppointmentDTO> appointmentDTOs = responses.stream()
|
||||||
.map(this::mapToAppointmentDTO)
|
.map(this::mapToAppointmentDTO)
|
||||||
|
.sorted(Comparator.comparing((AppointmentDTO a) -> a.getAppointmentDate() + "T" + a.getAppointmentTime()).reversed())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
@@ -105,6 +107,7 @@ public class AppointmentController {
|
|||||||
List<AppointmentResponse> responses = AppointmentApi.getInstance().listAppointments(query);
|
List<AppointmentResponse> responses = AppointmentApi.getInstance().listAppointments(query);
|
||||||
List<AppointmentDTO> appointmentDTOs = responses.stream()
|
List<AppointmentDTO> appointmentDTOs = responses.stream()
|
||||||
.map(this::mapToAppointmentDTO)
|
.map(this::mapToAppointmentDTO)
|
||||||
|
.sorted(Comparator.comparing((AppointmentDTO a) -> a.getAppointmentDate() + "T" + a.getAppointmentTime()).reversed())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.example.petshopdesktop.api.endpoints.PurchaseOrderApi;
|
|||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class PurchaseOrderController {
|
public class PurchaseOrderController {
|
||||||
@@ -63,6 +64,7 @@ public class PurchaseOrderController {
|
|||||||
List<PurchaseOrderResponse> responses = PurchaseOrderApi.getInstance().listPurchaseOrders(null);
|
List<PurchaseOrderResponse> responses = PurchaseOrderApi.getInstance().listPurchaseOrders(null);
|
||||||
List<PurchaseOrderDTO> dtos = responses.stream()
|
List<PurchaseOrderDTO> dtos = responses.stream()
|
||||||
.map(this::mapToPurchaseOrderDTO)
|
.map(this::mapToPurchaseOrderDTO)
|
||||||
|
.sorted(Comparator.comparing(PurchaseOrderDTO::getOrderDate).reversed())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
@@ -118,4 +120,4 @@ public class PurchaseOrderController {
|
|||||||
response.getOrderStatus()
|
response.getOrderStatus()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import javafx.collections.transformation.FilteredList;
|
|||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Button;
|
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.api.dto.sale.SaleResponse;
|
||||||
import org.example.petshopdesktop.models.Product;
|
import org.example.petshopdesktop.models.Product;
|
||||||
import org.example.petshopdesktop.models.SaleCartItem;
|
import org.example.petshopdesktop.models.SaleCartItem;
|
||||||
|
import org.example.petshopdesktop.models.SaleDetail;
|
||||||
import org.example.petshopdesktop.models.SaleLineItem;
|
import org.example.petshopdesktop.models.SaleLineItem;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
|
||||||
@@ -39,6 +42,7 @@ import java.math.BigDecimal;
|
|||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
@@ -128,6 +132,7 @@ public class SaleController {
|
|||||||
private final ObservableList<SaleCartItem> cartItems = FXCollections.observableArrayList();
|
private final ObservableList<SaleCartItem> cartItems = FXCollections.observableArrayList();
|
||||||
private final ObservableList<SaleLineItem> saleItems = FXCollections.observableArrayList();
|
private final ObservableList<SaleLineItem> saleItems = FXCollections.observableArrayList();
|
||||||
private FilteredList<SaleLineItem> filteredSales;
|
private FilteredList<SaleLineItem> filteredSales;
|
||||||
|
private boolean saleSaveInProgress;
|
||||||
|
|
||||||
private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA);
|
private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA);
|
||||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
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);
|
filteredSales = new FilteredList<>(saleItems, s -> true);
|
||||||
tvSales.setItems(filteredSales);
|
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));
|
txtSearch.textProperty().addListener((obs, oldVal, newVal) -> applySalesFilter(newVal));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,22 +191,43 @@ public class SaleController {
|
|||||||
|
|
||||||
updateCartTotal();
|
updateCartTotal();
|
||||||
|
|
||||||
try {
|
setCreateSaleControlsDisabled(true);
|
||||||
List<ProductResponse> productResponses = ProductApi.getInstance().listProducts(null);
|
|
||||||
ObservableList<Product> products = FXCollections.observableArrayList();
|
Task<ObservableList<Product>> task = new Task<>() {
|
||||||
for (ProductResponse pr : productResponses) {
|
@Override
|
||||||
products.add(new Product(
|
protected ObservableList<Product> call() throws Exception {
|
||||||
pr.getProdId().intValue(),
|
List<ProductResponse> productResponses = ProductApi.getInstance().listProducts(null);
|
||||||
pr.getProdName(),
|
ObservableList<Product> products = FXCollections.observableArrayList();
|
||||||
pr.getProdPrice().doubleValue(),
|
for (ProductResponse pr : productResponses) {
|
||||||
0,
|
products.add(new Product(
|
||||||
pr.getProdDesc()
|
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() {
|
private void applyRoleMode() {
|
||||||
@@ -207,10 +242,13 @@ public class SaleController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void refreshSales(boolean showErrorDialog) {
|
private void refreshSales(boolean showErrorDialog) {
|
||||||
|
btnRefresh.setDisable(true);
|
||||||
Task<List<SaleLineItem>> task = new Task<List<SaleLineItem>>() {
|
Task<List<SaleLineItem>> task = new Task<List<SaleLineItem>>() {
|
||||||
@Override
|
@Override
|
||||||
protected List<SaleLineItem> call() throws Exception {
|
protected List<SaleLineItem> call() throws Exception {
|
||||||
List<SaleResponse> sales = SaleApi.getInstance().listSales(0, 1000, null);
|
List<SaleResponse> sales = SaleApi.getInstance().listAllSales(null);
|
||||||
|
sales.sort(Comparator.comparing(SaleResponse::getSaleDate, Comparator.nullsLast(Comparator.reverseOrder()))
|
||||||
|
.thenComparing(SaleResponse::getSaleId, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||||
List<SaleLineItem> lineItems = new ArrayList<>();
|
List<SaleLineItem> lineItems = new ArrayList<>();
|
||||||
|
|
||||||
for (SaleResponse sale : sales) {
|
for (SaleResponse sale : sales) {
|
||||||
@@ -220,18 +258,23 @@ public class SaleController {
|
|||||||
|
|
||||||
if (sale.getItems() != null && !sale.getItems().isEmpty()) {
|
if (sale.getItems() != null && !sale.getItems().isEmpty()) {
|
||||||
for (SaleItemResponse item : sale.getItems()) {
|
for (SaleItemResponse item : sale.getItems()) {
|
||||||
|
boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund();
|
||||||
double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0;
|
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(
|
lineItems.add(new SaleLineItem(
|
||||||
sale.getSaleId().intValue(),
|
sale.getSaleId().intValue(),
|
||||||
saleDate,
|
saleDate,
|
||||||
sale.getEmployeeName(),
|
sale.getEmployeeName(),
|
||||||
item.getProductName(),
|
item.getProductName(),
|
||||||
item.getQuantity(),
|
quantity,
|
||||||
unitPrice,
|
unitPrice,
|
||||||
lineTotal,
|
lineTotal,
|
||||||
sale.getPaymentMethod(),
|
sale.getPaymentMethod(),
|
||||||
sale.getIsRefund() != null && sale.getIsRefund()
|
isRefund
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,14 +285,14 @@ public class SaleController {
|
|||||||
|
|
||||||
task.setOnSucceeded(event -> {
|
task.setOnSucceeded(event -> {
|
||||||
saleItems.setAll(task.getValue());
|
saleItems.setAll(task.getValue());
|
||||||
|
btnRefresh.setDisable(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
task.setOnFailed(event -> {
|
task.setOnFailed(event -> {
|
||||||
Throwable e = task.getException();
|
Throwable e = task.getException();
|
||||||
ActivityLogger.getInstance().logException("SaleController.refreshSales", (Exception) e, "Loading sales");
|
ActivityLogger.getInstance().logException("SaleController.refreshSales", (Exception) e, "Loading sales");
|
||||||
if (showErrorDialog) {
|
btnRefresh.setDisable(false);
|
||||||
showError("Sales", "Could not load sales: " + e.getMessage());
|
showError("Sales", "Could not load sales: " + e.getMessage());
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
new Thread(task).start();
|
new Thread(task).start();
|
||||||
@@ -310,6 +353,9 @@ public class SaleController {
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void btnSaveSale(ActionEvent event) {
|
void btnSaveSale(ActionEvent event) {
|
||||||
|
if (saleSaveInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (UserSession.getInstance().isAdmin()) {
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
showError("Create Sale", "This action is restricted to staff.");
|
showError("Create Sale", "This action is restricted to staff.");
|
||||||
return;
|
return;
|
||||||
@@ -332,36 +378,57 @@ public class SaleController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
SaleRequest request = new SaleRequest();
|
||||||
SaleRequest request = new SaleRequest();
|
request.setStoreId(storeId);
|
||||||
request.setStoreId(storeId);
|
request.setPaymentMethod(payment);
|
||||||
request.setPaymentMethod(payment);
|
|
||||||
|
|
||||||
List<SaleItemRequest> itemRequests = new ArrayList<>();
|
List<SaleItemRequest> itemRequests = new ArrayList<>();
|
||||||
for (SaleCartItem cartItem : cartItems) {
|
for (SaleCartItem cartItem : cartItems) {
|
||||||
SaleItemRequest itemRequest = new SaleItemRequest();
|
SaleItemRequest itemRequest = new SaleItemRequest();
|
||||||
itemRequest.setProdId((long) cartItem.getProdId());
|
itemRequest.setProdId((long) cartItem.getProdId());
|
||||||
itemRequest.setQuantity(cartItem.getQuantity());
|
itemRequest.setQuantity(cartItem.getQuantity());
|
||||||
itemRequests.add(itemRequest);
|
itemRequests.add(itemRequest);
|
||||||
|
}
|
||||||
|
request.setItems(itemRequests);
|
||||||
|
|
||||||
|
saleSaveInProgress = true;
|
||||||
|
setCreateSaleControlsDisabled(true);
|
||||||
|
btnRefund.setDisable(true);
|
||||||
|
|
||||||
|
Task<SaleResponse> 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.");
|
showInfo("Sale saved", "Sale ID " + response.getSaleId() + " was created.");
|
||||||
|
|
||||||
cartItems.clear();
|
cartItems.clear();
|
||||||
updateCartTotal();
|
updateCartTotal();
|
||||||
|
|
||||||
refreshSales(true);
|
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")) {
|
if (errorMsg != null && errorMsg.contains("Insufficient inventory")) {
|
||||||
showError("Create Sale", "Insufficient stock for one or more items.");
|
showError("Create Sale", "Insufficient stock for one or more items.");
|
||||||
} else {
|
} else {
|
||||||
showError("Create Sale", errorMsg != null ? errorMsg : "Could not save the sale.");
|
showError("Create Sale", errorMsg != null ? errorMsg : "Could not save the sale.");
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
new Thread(task).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -384,11 +451,13 @@ public class SaleController {
|
|||||||
dialog.initModality(Modality.APPLICATION_MODAL);
|
dialog.initModality(Modality.APPLICATION_MODAL);
|
||||||
dialog.setTitle("Process Refund");
|
dialog.setTitle("Process Refund");
|
||||||
dialog.setScene(new Scene(loader.load()));
|
dialog.setScene(new Scene(loader.load()));
|
||||||
|
var controller = loader.<org.example.petshopdesktop.controllers.dialogcontrollers.RefundDialogController>getController();
|
||||||
if (selectedSale != null) {
|
if (selectedSale != null) {
|
||||||
loader.<org.example.petshopdesktop.controllers.dialogcontrollers.RefundDialogController>getController()
|
controller.prefillSale((long) selectedSale.getSaleId());
|
||||||
.prefillSale((long) selectedSale.getSaleId());
|
|
||||||
}
|
}
|
||||||
dialog.setResizable(false);
|
dialog.setMinWidth(860);
|
||||||
|
dialog.setMinHeight(680);
|
||||||
|
dialog.setResizable(true);
|
||||||
dialog.showAndWait();
|
dialog.showAndWait();
|
||||||
|
|
||||||
refreshSales(true);
|
refreshSales(true);
|
||||||
@@ -397,11 +466,87 @@ public class SaleController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openSaleDetailDialog(int saleId) {
|
||||||
|
Task<SaleResponse> 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<SaleDetail.SaleDetailItem> 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(),
|
||||||
|
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() {
|
private void updateCartTotal() {
|
||||||
double total = cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum();
|
double total = cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum();
|
||||||
lblCartTotal.setText(currency.format(total));
|
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) {
|
private void applySalesFilter(String filter) {
|
||||||
String f = filter == null ? "" : filter.trim().toLowerCase();
|
String f = filter == null ? "" : filter.trim().toLowerCase();
|
||||||
if (f.isEmpty()) {
|
if (f.isEmpty()) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import org.example.petshopdesktop.util.ActivityLogger;
|
|||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class StaffAccountsController {
|
public class StaffAccountsController {
|
||||||
@@ -161,6 +162,7 @@ public class StaffAccountsController {
|
|||||||
List<EmployeeResponse> employees = EmployeeApi.getInstance().listEmployees(null);
|
List<EmployeeResponse> employees = EmployeeApi.getInstance().listEmployees(null);
|
||||||
List<StaffAccount> accounts = employees.stream()
|
List<StaffAccount> accounts = employees.stream()
|
||||||
.map(this::mapToStaffAccount)
|
.map(this::mapToStaffAccount)
|
||||||
|
.sorted(Comparator.comparing(StaffAccount::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder())))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ import javafx.collections.FXCollections;
|
|||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
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.*;
|
||||||
import javafx.scene.control.cell.PropertyValueFactory;
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.api.dto.sale.SaleItemRequest;
|
import org.example.petshopdesktop.api.dto.sale.SaleItemRequest;
|
||||||
import org.example.petshopdesktop.api.dto.sale.SaleItemResponse;
|
import org.example.petshopdesktop.api.dto.sale.SaleItemResponse;
|
||||||
@@ -90,6 +94,7 @@ public class RefundDialogController {
|
|||||||
private final ObservableList<SaleItemResponse> originalItems = FXCollections.observableArrayList();
|
private final ObservableList<SaleItemResponse> originalItems = FXCollections.observableArrayList();
|
||||||
private final ObservableList<RefundItem> refundItems = FXCollections.observableArrayList();
|
private final ObservableList<RefundItem> refundItems = FXCollections.observableArrayList();
|
||||||
private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA);
|
private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA);
|
||||||
|
private boolean refundInProgress;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
@@ -100,17 +105,19 @@ public class RefundDialogController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupTables() {
|
private void setupTables() {
|
||||||
colOriginalProduct.setCellValueFactory(new PropertyValueFactory<>("productName"));
|
tvOriginalItems.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||||
colOriginalQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
|
colOriginalProduct.setCellValueFactory(cell -> new ReadOnlyStringWrapper(cell.getValue().getProductName()));
|
||||||
colOriginalUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
|
colOriginalQuantity.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getQuantity()));
|
||||||
colOriginalTotal.setCellValueFactory(new PropertyValueFactory<>("lineTotal"));
|
colOriginalUnitPrice.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getUnitPrice()));
|
||||||
|
colOriginalTotal.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getLineTotal()));
|
||||||
tvOriginalItems.setItems(originalItems);
|
tvOriginalItems.setItems(originalItems);
|
||||||
tvOriginalItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
tvOriginalItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||||
|
|
||||||
colRefundProduct.setCellValueFactory(new PropertyValueFactory<>("productName"));
|
tvRefundItems.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||||
colRefundQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
|
colRefundProduct.setCellValueFactory(cell -> new ReadOnlyStringWrapper(cell.getValue().getProductName()));
|
||||||
colRefundUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
|
colRefundQuantity.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getQuantity()));
|
||||||
colRefundTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
|
colRefundUnitPrice.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getUnitPrice()));
|
||||||
|
colRefundTotal.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getTotal()));
|
||||||
tvRefundItems.setItems(refundItems);
|
tvRefundItems.setItems(refundItems);
|
||||||
tvRefundItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
tvRefundItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||||
}
|
}
|
||||||
@@ -125,12 +132,13 @@ public class RefundDialogController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
txtSaleId.setText(String.valueOf(saleId));
|
txtSaleId.setText(String.valueOf(saleId));
|
||||||
loadSale();
|
Platform.runLater(this::loadSale);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadSale() {
|
private void loadSale() {
|
||||||
String saleIdText = txtSaleId.getText().trim();
|
String saleIdText = txtSaleId.getText().trim();
|
||||||
if (saleIdText.isEmpty()) {
|
if (saleIdText.isEmpty()) {
|
||||||
|
clearLoadedSale();
|
||||||
showError("Load Sale", "Enter a transaction ID.");
|
showError("Load Sale", "Enter a transaction ID.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -139,22 +147,36 @@ public class RefundDialogController {
|
|||||||
try {
|
try {
|
||||||
saleId = Long.parseLong(saleIdText);
|
saleId = Long.parseLong(saleIdText);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
|
clearLoadedSale();
|
||||||
showError("Load Sale", "Invalid transaction ID.");
|
showError("Load Sale", "Invalid transaction ID.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
setLoadingState(true, "Loading sale...");
|
||||||
List<SaleResponse> allSales = SaleApi.getInstance().listSales(0, 1000, null);
|
clearLoadedSale();
|
||||||
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<SaleResponse> previousRefunds = allSales.stream()
|
|
||||||
.filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
|
Task<LoadedSaleData> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected LoadedSaleData call() throws Exception {
|
||||||
|
List<SaleResponse> 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<SaleResponse> previousRefunds = allSales.stream()
|
||||||
|
.filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<SaleItemResponse> 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");
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||||
String saleInfo = String.format("Sale Date: %s | Employee: %s | Original Total: %s | Payment: %s",
|
String saleInfo = String.format("Sale Date: %s | Employee: %s | Original Total: %s | Payment: %s",
|
||||||
currentSale.getSaleDate().format(formatter),
|
currentSale.getSaleDate().format(formatter),
|
||||||
@@ -162,26 +184,25 @@ public class RefundDialogController {
|
|||||||
currency.format(currentSale.getTotalAmount()),
|
currency.format(currentSale.getTotalAmount()),
|
||||||
currentSale.getPaymentMethod());
|
currentSale.getPaymentMethod());
|
||||||
lblSaleInfo.setText(saleInfo);
|
lblSaleInfo.setText(saleInfo);
|
||||||
|
|
||||||
List<SaleItemResponse> refundableItems = buildRefundableItems(currentSale, previousRefunds);
|
|
||||||
if (refundableItems.isEmpty()) {
|
|
||||||
showError("Load Sale", "This sale has no remaining refundable items.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
baseOriginalItems.clear();
|
baseOriginalItems.clear();
|
||||||
baseOriginalItems.addAll(copySaleItems(refundableItems));
|
baseOriginalItems.addAll(copySaleItems(loaded.refundableItems()));
|
||||||
originalItems.setAll(copySaleItems(refundableItems));
|
originalItems.setAll(copySaleItems(loaded.refundableItems()));
|
||||||
cbPaymentMethod.getSelectionModel().select(currentSale.getPaymentMethod());
|
cbPaymentMethod.getSelectionModel().select(currentSale.getPaymentMethod());
|
||||||
|
|
||||||
refundItems.clear();
|
refundItems.clear();
|
||||||
updateOriginalItemAvailability();
|
updateOriginalItemAvailability();
|
||||||
updateRefundTotal();
|
updateRefundTotal();
|
||||||
|
setLoadingState(false, saleInfo);
|
||||||
|
});
|
||||||
|
|
||||||
} catch (Exception e) {
|
task.setOnFailed(event -> {
|
||||||
ActivityLogger.getInstance().logException("RefundDialogController.btnLoadSaleClicked", e, "Loading sale");
|
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.");
|
showError("Load Sale", e.getMessage() != null ? e.getMessage() : "Could not load sale.");
|
||||||
}
|
});
|
||||||
|
|
||||||
|
new Thread(task).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -248,6 +269,9 @@ public class RefundDialogController {
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void btnProcessRefundClicked(ActionEvent event) {
|
void btnProcessRefundClicked(ActionEvent event) {
|
||||||
|
if (refundInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (currentSale == null) {
|
if (currentSale == null) {
|
||||||
showError("Process Refund", "Load a sale first.");
|
showError("Process Refund", "Load a sale first.");
|
||||||
return;
|
return;
|
||||||
@@ -280,36 +304,53 @@ public class RefundDialogController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
SaleRequest request = new SaleRequest();
|
||||||
SaleRequest request = new SaleRequest();
|
request.setStoreId(storeId);
|
||||||
request.setStoreId(storeId);
|
request.setPaymentMethod(payment);
|
||||||
request.setPaymentMethod(payment);
|
request.setIsRefund(true);
|
||||||
request.setIsRefund(true);
|
request.setOriginalSaleId(currentSale.getSaleId());
|
||||||
request.setOriginalSaleId(currentSale.getSaleId());
|
|
||||||
|
|
||||||
List<SaleItemRequest> items = new ArrayList<>();
|
List<SaleItemRequest> items = new ArrayList<>();
|
||||||
for (RefundItem item : refundItems) {
|
for (RefundItem item : refundItems) {
|
||||||
SaleItemRequest saleItem = new SaleItemRequest();
|
SaleItemRequest saleItem = new SaleItemRequest();
|
||||||
saleItem.setProdId((long) item.getProdId());
|
saleItem.setProdId((long) item.getProdId());
|
||||||
saleItem.setQuantity(-item.getQuantity());
|
saleItem.setQuantity(-item.getQuantity());
|
||||||
items.add(saleItem);
|
items.add(saleItem);
|
||||||
|
}
|
||||||
|
request.setItems(items);
|
||||||
|
|
||||||
|
refundInProgress = true;
|
||||||
|
setLoadingState(true, lblSaleInfo.getText());
|
||||||
|
|
||||||
|
Task<SaleResponse> 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);
|
Alert success = new Alert(Alert.AlertType.INFORMATION);
|
||||||
success.setTitle("Refund Processed");
|
success.setTitle("Refund Processed");
|
||||||
success.setHeaderText(null);
|
success.setHeaderText(null);
|
||||||
success.setContentText("Refund ID " + refundResponse.getSaleId() + " was created successfully.");
|
success.setContentText("Refund ID " + refundResponse.getSaleId() + " was created successfully.");
|
||||||
success.showAndWait();
|
success.showAndWait();
|
||||||
|
|
||||||
closeDialog();
|
closeDialog();
|
||||||
|
});
|
||||||
|
|
||||||
} catch (Exception e) {
|
task.setOnFailed(evt -> {
|
||||||
ActivityLogger.getInstance().logException("RefundDialogController.btnProcessRefundClicked", e, "Processing refund");
|
refundInProgress = false;
|
||||||
showError("Process Refund", e.getMessage() != null ? e.getMessage() : "Could not process refund.");
|
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
|
@FXML
|
||||||
@@ -326,6 +367,18 @@ public class RefundDialogController {
|
|||||||
updateRefundTotal();
|
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) {
|
private void addOrMergeRefundItem(SaleItemResponse selected, int quantity) {
|
||||||
for (int i = 0; i < refundItems.size(); i++) {
|
for (int i = 0; i < refundItems.size(); i++) {
|
||||||
RefundItem existing = refundItems.get(i);
|
RefundItem existing = refundItems.get(i);
|
||||||
@@ -486,4 +539,7 @@ public class RefundDialogController {
|
|||||||
return quantity * unitPrice;
|
return quantity * unitPrice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record LoadedSaleData(SaleResponse sale, List<SaleItemResponse> refundableItems) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<SaleDetail.SaleDetailItem> tvItems;
|
||||||
|
@FXML private TableColumn<SaleDetail.SaleDetailItem, String> colProduct;
|
||||||
|
@FXML private TableColumn<SaleDetail.SaleDetailItem, Integer> colQuantity;
|
||||||
|
@FXML private TableColumn<SaleDetail.SaleDetailItem, Double> colUnitPrice;
|
||||||
|
@FXML private TableColumn<SaleDetail.SaleDetailItem, Double> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.text.Font?>
|
<?import javafx.scene.text.Font?>
|
||||||
|
|
||||||
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="700.0" prefWidth="900.0" spacing="20.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.dialogcontrollers.RefundDialogController">
|
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="720.0" prefWidth="920.0" spacing="22.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.dialogcontrollers.RefundDialogController">
|
||||||
<padding>
|
<padding>
|
||||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
||||||
</padding>
|
</padding>
|
||||||
@@ -84,14 +84,14 @@
|
|||||||
</children>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|
||||||
<VBox spacing="10.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-color: #FF6b6b; -fx-border-radius: 14;" VBox.vgrow="ALWAYS">
|
<VBox spacing="18.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-color: #FF6b6b; -fx-border-radius: 14;" VBox.vgrow="ALWAYS">
|
||||||
<padding>
|
<padding>
|
||||||
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
|
<Insets bottom="18.0" left="18.0" right="18.0" top="18.0" />
|
||||||
</padding>
|
</padding>
|
||||||
<children>
|
<children>
|
||||||
<HBox alignment="CENTER_LEFT" spacing="10.0">
|
<HBox alignment="CENTER_LEFT" spacing="14.0">
|
||||||
<children>
|
<children>
|
||||||
<Label text="Original Items" textFill="#2c3e50">
|
<Label text="Original Items" textFill="#2c3e50">
|
||||||
<font>
|
<font>
|
||||||
<Font name="System Bold" size="16.0" />
|
<Font name="System Bold" size="16.0" />
|
||||||
</font>
|
</font>
|
||||||
@@ -107,20 +107,20 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</children>
|
</children>
|
||||||
</HBox>
|
</HBox>
|
||||||
<TableView fx:id="tvOriginalItems" prefHeight="150.0" style="-fx-background-color: white; -fx-background-radius: 10;">
|
<TableView fx:id="tvOriginalItems" prefHeight="190.0" style="-fx-background-color: white; -fx-background-radius: 10; -fx-border-color: #E8EBED; -fx-border-radius: 10;">
|
||||||
<columns>
|
<columns>
|
||||||
<TableColumn fx:id="colOriginalProduct" prefWidth="350.0" text="Product Name" />
|
<TableColumn fx:id="colOriginalProduct" prefWidth="430.0" text="Product Name" />
|
||||||
<TableColumn fx:id="colOriginalQuantity" prefWidth="120.0" text="Quantity" />
|
<TableColumn fx:id="colOriginalQuantity" prefWidth="140.0" text="Quantity" />
|
||||||
<TableColumn fx:id="colOriginalUnitPrice" prefWidth="150.0" text="Unit Price" />
|
<TableColumn fx:id="colOriginalUnitPrice" prefWidth="170.0" text="Unit Price" />
|
||||||
<TableColumn fx:id="colOriginalTotal" prefWidth="150.0" text="Total" />
|
<TableColumn fx:id="colOriginalTotal" prefWidth="170.0" text="Total" />
|
||||||
</columns>
|
</columns>
|
||||||
</TableView>
|
</TableView>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<HBox alignment="CENTER_LEFT" spacing="10.0">
|
<HBox alignment="CENTER_LEFT" spacing="14.0">
|
||||||
<children>
|
<children>
|
||||||
<Label text="Refund Items" textFill="#2c3e50">
|
<Label text="Refund Items" textFill="#2c3e50">
|
||||||
<font>
|
<font>
|
||||||
<Font name="System Bold" size="16.0" />
|
<Font name="System Bold" size="16.0" />
|
||||||
</font>
|
</font>
|
||||||
@@ -136,16 +136,16 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</children>
|
</children>
|
||||||
</HBox>
|
</HBox>
|
||||||
<TableView fx:id="tvRefundItems" prefHeight="150.0" style="-fx-background-color: white; -fx-background-radius: 10;">
|
<TableView fx:id="tvRefundItems" prefHeight="190.0" style="-fx-background-color: white; -fx-background-radius: 10; -fx-border-color: #E8EBED; -fx-border-radius: 10;" VBox.vgrow="ALWAYS">
|
||||||
<columns>
|
<columns>
|
||||||
<TableColumn fx:id="colRefundProduct" prefWidth="350.0" text="Product Name" />
|
<TableColumn fx:id="colRefundProduct" prefWidth="430.0" text="Product Name" />
|
||||||
<TableColumn fx:id="colRefundQuantity" prefWidth="120.0" text="Quantity" />
|
<TableColumn fx:id="colRefundQuantity" prefWidth="140.0" text="Quantity" />
|
||||||
<TableColumn fx:id="colRefundUnitPrice" prefWidth="150.0" text="Unit Price" />
|
<TableColumn fx:id="colRefundUnitPrice" prefWidth="170.0" text="Unit Price" />
|
||||||
<TableColumn fx:id="colRefundTotal" prefWidth="150.0" text="Total" />
|
<TableColumn fx:id="colRefundTotal" prefWidth="170.0" text="Total" />
|
||||||
</columns>
|
</columns>
|
||||||
</TableView>
|
</TableView>
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|
||||||
<VBox spacing="12.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-color: #FF6b6b; -fx-border-radius: 14;">
|
<VBox spacing="12.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-color: #FF6b6b; -fx-border-radius: 14;">
|
||||||
<padding>
|
<padding>
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.TableColumn?>
|
||||||
|
<?import javafx.scene.control.TableView?>
|
||||||
|
<?import javafx.scene.layout.GridPane?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.Region?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import javafx.scene.text.Font?>
|
||||||
|
|
||||||
|
<VBox prefHeight="520.0" prefWidth="760.0" spacing="18.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.dialogcontrollers.SaleDetailDialogController">
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="18.0" left="18.0" right="18.0" top="18.0" />
|
||||||
|
</padding>
|
||||||
|
<children>
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="12.0" style="-fx-background-color: #4ECDC4; -fx-background-radius: 12;">
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="14.0" left="16.0" right="16.0" top="14.0" />
|
||||||
|
</padding>
|
||||||
|
<children>
|
||||||
|
<Label text="Sale Details" textFill="WHITE">
|
||||||
|
<font>
|
||||||
|
<Font name="System Bold" size="24.0" />
|
||||||
|
</font>
|
||||||
|
</Label>
|
||||||
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
|
<Button mnemonicParsing="false" onAction="#btnCloseClicked" style="-fx-background-color: white; -fx-text-fill: #2c3e50; -fx-background-radius: 8;" text="Close" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<GridPane hgap="16.0" vgap="12.0" style="-fx-background-color: white; -fx-background-radius: 12; -fx-border-color: #e6e6e6; -fx-border-radius: 12; -fx-border-width: 1;">
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="16.0" left="16.0" right="16.0" top="16.0" />
|
||||||
|
</padding>
|
||||||
|
<children>
|
||||||
|
<Label text="Sale ID" GridPane.columnIndex="0" GridPane.rowIndex="0" />
|
||||||
|
<Label fx:id="lblSaleId" GridPane.columnIndex="1" GridPane.rowIndex="0" />
|
||||||
|
<Label text="Date" GridPane.columnIndex="2" GridPane.rowIndex="0" />
|
||||||
|
<Label fx:id="lblSaleDate" GridPane.columnIndex="3" GridPane.rowIndex="0" />
|
||||||
|
<Label text="Employee" GridPane.columnIndex="0" GridPane.rowIndex="1" />
|
||||||
|
<Label fx:id="lblEmployee" GridPane.columnIndex="1" GridPane.rowIndex="1" />
|
||||||
|
<Label text="Payment" GridPane.columnIndex="2" GridPane.rowIndex="1" />
|
||||||
|
<Label fx:id="lblPayment" GridPane.columnIndex="3" GridPane.rowIndex="1" />
|
||||||
|
<Label text="Total" GridPane.columnIndex="0" GridPane.rowIndex="2" />
|
||||||
|
<Label fx:id="lblTotal" GridPane.columnIndex="1" GridPane.rowIndex="2" />
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
|
||||||
|
<TableView fx:id="tvItems" prefHeight="320.0" VBox.vgrow="ALWAYS">
|
||||||
|
<columns>
|
||||||
|
<TableColumn fx:id="colProduct" text="Product" prefWidth="330.0" />
|
||||||
|
<TableColumn fx:id="colQuantity" text="Qty" prefWidth="90.0" />
|
||||||
|
<TableColumn fx:id="colUnitPrice" text="Unit Price" prefWidth="130.0" />
|
||||||
|
<TableColumn fx:id="colLineTotal" text="Total" prefWidth="130.0" />
|
||||||
|
</columns>
|
||||||
|
</TableView>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<Insets top="10.0" />
|
<Insets top="10.0" />
|
||||||
</padding>
|
</padding>
|
||||||
</Label>
|
</Label>
|
||||||
<Region prefHeight="200.0" prefWidth="200.0" HBox.hgrow="ALWAYS" />
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
<Button fx:id="btnRefund" mnemonicParsing="false" onAction="#btnRefund" prefHeight="44.0" style="-fx-background-color: #FF6b6b; -fx-cursor: hand; -fx-background-radius: 8;" text="Process Refund" textFill="WHITE">
|
<Button fx:id="btnRefund" mnemonicParsing="false" onAction="#btnRefund" prefHeight="44.0" style="-fx-background-color: #FF6b6b; -fx-cursor: hand; -fx-background-radius: 8;" text="Process Refund" textFill="WHITE">
|
||||||
<font>
|
<font>
|
||||||
<Font name="System Bold" size="14.0" />
|
<Font name="System Bold" size="14.0" />
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</children>
|
</children>
|
||||||
</HBox>
|
</HBox>
|
||||||
<TableView fx:id="tvCart" prefHeight="170.0" style="-fx-background-color: white; -fx-background-radius: 10;" VBox.vgrow="NEVER">
|
<TableView fx:id="tvCart" prefHeight="170.0" style="-fx-background-color: white; -fx-background-radius: 10;" VBox.vgrow="NEVER">
|
||||||
<columns>
|
<columns>
|
||||||
<TableColumn fx:id="colCartProduct" prefWidth="310.0" text="Product" />
|
<TableColumn fx:id="colCartProduct" prefWidth="310.0" text="Product" />
|
||||||
<TableColumn fx:id="colCartQty" prefWidth="90.0" text="Qty" />
|
<TableColumn fx:id="colCartQty" prefWidth="90.0" text="Qty" />
|
||||||
@@ -151,16 +151,16 @@
|
|||||||
</TextField>
|
</TextField>
|
||||||
</children>
|
</children>
|
||||||
</HBox>
|
</HBox>
|
||||||
<TableView fx:id="tvSales" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12; -fx-padding: 6;" VBox.vgrow="ALWAYS">
|
<TableView fx:id="tvSales" prefHeight="362.0" style="-fx-background-color: white; -fx-background-radius: 12; -fx-padding: 6;" VBox.vgrow="ALWAYS">
|
||||||
<columns>
|
<columns>
|
||||||
<TableColumn fx:id="colSaleId" minWidth="56.0" prefWidth="72.0" text="ID" />
|
<TableColumn fx:id="colSaleId" minWidth="54.0" prefWidth="62.0" text="ID" />
|
||||||
<TableColumn fx:id="colSaleDate" minWidth="150.0" prefWidth="180.0" text="Date" />
|
<TableColumn fx:id="colSaleDate" minWidth="145.0" prefWidth="165.0" text="Date" />
|
||||||
<TableColumn fx:id="colEmployeeName" minWidth="150.0" prefWidth="180.0" text="Employee" />
|
<TableColumn fx:id="colEmployeeName" minWidth="130.0" prefWidth="155.0" text="Employee" />
|
||||||
<TableColumn fx:id="colServiceProduct" minWidth="190.0" prefWidth="240.0" text="Product" />
|
<TableColumn fx:id="colServiceProduct" minWidth="180.0" prefWidth="240.0" text="Product" />
|
||||||
<TableColumn fx:id="colSaleQuantity" minWidth="70.0" prefWidth="80.0" text="Qty" />
|
<TableColumn fx:id="colSaleQuantity" minWidth="62.0" prefWidth="72.0" text="Qty" />
|
||||||
<TableColumn fx:id="colSaleUnitPrice" minWidth="110.0" prefWidth="130.0" text="Unit Price" />
|
<TableColumn fx:id="colSaleUnitPrice" minWidth="95.0" prefWidth="115.0" text="Unit Price" />
|
||||||
<TableColumn fx:id="colSaleTotal" minWidth="100.0" prefWidth="120.0" text="Total" />
|
<TableColumn fx:id="colSaleTotal" minWidth="90.0" prefWidth="108.0" text="Total" />
|
||||||
<TableColumn fx:id="colSalePaymentType" minWidth="100.0" prefWidth="120.0" text="Payment" />
|
<TableColumn fx:id="colSalePaymentType" minWidth="88.0" prefWidth="100.0" text="Payment" />
|
||||||
</columns>
|
</columns>
|
||||||
</TableView>
|
</TableView>
|
||||||
</children>
|
</children>
|
||||||
|
|||||||
Reference in New Issue
Block a user