From 9dad410c5427dc7016da371c17cc39be703c6a20 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:52:07 -0600 Subject: [PATCH] added loyaltypoint usage to sales unfinished still needs to work with the backend --- .../example/petstoremobile/dtos/SaleDTO.java | 18 ++++ .../detailfragments/SaleDetailFragment.java | 85 +++++++++++++++---- .../viewmodels/SaleDetailViewModel.java | 54 ++++++++++++ .../main/res/layout/fragment_sale_detail.xml | 46 +++++++++- 4 files changed, 184 insertions(+), 19 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java index f309978f..8b2849da 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java @@ -17,6 +17,8 @@ public class SaleDTO { private BigDecimal subtotalAmount; private BigDecimal couponDiscountAmount; private BigDecimal employeeDiscountAmount; + private BigDecimal loyaltyDiscountAmount; + private Integer pointsUsed; private String paymentMethod; private String channel; private Boolean isRefund; @@ -78,6 +80,22 @@ public class SaleDTO { return employeeDiscountAmount; } + public BigDecimal getLoyaltyDiscountAmount() { + return loyaltyDiscountAmount; + } + + public void setLoyaltyDiscountAmount(BigDecimal loyaltyDiscountAmount) { + this.loyaltyDiscountAmount = loyaltyDiscountAmount; + } + + public Integer getPointsUsed() { + return pointsUsed; + } + + public void setPointsUsed(Integer pointsUsed) { + this.pointsUsed = pointsUsed; + } + public String getPaymentMethod() { return paymentMethod; } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java index 9b94a168..3acde9a4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java @@ -14,6 +14,7 @@ import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.viewmodels.SaleDetailViewModel; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.UIUtils; @@ -81,7 +82,7 @@ public class SaleDetailFragment extends Fragment { if (isStaff()) { UIUtils.setViewsEnabled(false, binding.spinnerSaleStore); if (primaryStoreId == null) { - Toast.makeText(requireContext(), "No store assigned to your account. Contact an admin.", Toast.LENGTH_LONG).show(); + UIUtils.showToast(requireContext(), "No store assigned to your account. Contact an admin."); } } }); @@ -100,6 +101,17 @@ public class SaleDetailFragment extends Fragment { renderCartItems(); updateTotal(); }); + + viewModel.getSelectedCustomerData().observe(getViewLifecycleOwner(), customer -> { + if (customer != null && !viewModel.isViewOnly()) { + binding.llLoyaltyPoints.setVisibility(View.VISIBLE); + binding.tvAvailablePoints.setText("(Available: " + customer.getLoyaltyPoints() + ")"); + binding.cbUseLoyaltyPoints.setEnabled(customer.getLoyaltyPoints() >= 20); + } else { + binding.llLoyaltyPoints.setVisibility(View.GONE); + binding.cbUseLoyaltyPoints.setChecked(false); + } + }); } private void handleArguments() { @@ -109,8 +121,8 @@ public class SaleDetailFragment extends Fragment { boolean viewOnly = a.getBoolean("viewOnly", false); viewModel.setSaleId(saleId, viewOnly); - binding.tvSaleMode.setText("Sale #" + saleId); - binding.tvSaleDetailId.setText("ID: " + saleId); + binding.tvSaleMode.setText("Sale #" + DateTimeUtils.formatId(saleId)); + binding.tvSaleDetailId.setText("ID: " + DateTimeUtils.formatId(saleId)); boolean isRefund = a.getBoolean("isRefund", false); if (isRefund || isAdmin()) { @@ -176,23 +188,30 @@ public class SaleDetailFragment extends Fragment { setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { SaleDTO sale = resource.data; - binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount()); - binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount())); + binding.tvSaleDetailTotal.setText("Total: $" + String.format(Locale.getDefault(), "%.2f", sale.getTotalAmount())); + binding.tvSaleSubtotal.setText("$" + String.format(Locale.getDefault(), "%.2f", (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount()))); if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { binding.llCouponDiscount.setVisibility(View.VISIBLE); - binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount()); + binding.tvSaleCouponDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getCouponDiscountAmount())); } else { binding.llCouponDiscount.setVisibility(View.GONE); } if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { binding.llEmployeeDiscount.setVisibility(View.VISIBLE); - binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount()); + binding.tvSaleEmployeeDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getEmployeeDiscountAmount())); } else { binding.llEmployeeDiscount.setVisibility(View.GONE); } + if (sale.getLoyaltyDiscountAmount() != null && sale.getLoyaltyDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + binding.llLoyaltyDiscount.setVisibility(View.VISIBLE); + binding.tvSaleLoyaltyDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getLoyaltyDiscountAmount())); + } else { + binding.llLoyaltyDiscount.setVisibility(View.GONE); + } + binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "—"); binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0)); binding.tvSaleStore.setText(sale.getStoreName() != null ? sale.getStoreName() : "—"); @@ -219,7 +238,7 @@ public class SaleDetailFragment extends Fragment { binding.btnApplyCoupon.setOnClickListener(v -> { String code = binding.etCouponCode.getText().toString().trim(); if (code.isEmpty()) { - Toast.makeText(getContext(), "Enter a coupon code", Toast.LENGTH_SHORT).show(); + UIUtils.showToast(getContext(), "Enter a coupon code"); return; } setLoading(true); @@ -289,7 +308,7 @@ public class SaleDetailFragment extends Fragment { for (SaleDTO.SaleItemDTO existing : viewModel.getCartItems().getValue()) { if (existing.getProdId().equals(product.getProdId())) { - Toast.makeText(getContext(), "Product already added", Toast.LENGTH_SHORT).show(); + UIUtils.showToast(getContext(), "Product already added"); return; } } @@ -297,6 +316,19 @@ public class SaleDetailFragment extends Fragment { viewModel.addToCart(new SaleDTO.SaleItemDTO(product.getProdId(), qty)); binding.etSaleQuantity.setText(""); }); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerSaleCustomer, p -> { + if (p > 0) { + Long id = viewModel.getCustomerList().getValue().get(p - 1).getId(); + viewModel.selectCustomer(id); + } else { + viewModel.selectCustomer(null); + } + }); + + binding.cbUseLoyaltyPoints.setOnCheckedChangeListener((v, checked) -> { + viewModel.setUseLoyaltyPoints(checked); + updateTotal(); + }); } private void renderCartItems() { @@ -336,7 +368,7 @@ public class SaleDetailFragment extends Fragment { TextView tvPrice = new TextView(getContext()); tvPrice.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); - tvPrice.setText(price != null ? "$" + price : ""); + tvPrice.setText(price != null ? "$" + String.format(Locale.getDefault(), "%.2f", price) : ""); row.addView(tvName); row.addView(tvQty); @@ -360,23 +392,35 @@ public class SaleDetailFragment extends Fragment { private void updateTotal() { BigDecimal subtotal = viewModel.calculateSubtotal(); - BigDecimal discount = viewModel.calculateDiscount(); - BigDecimal total = subtotal.subtract(discount); - binding.tvSaleSubtotal.setText("$" + subtotal); - if (discount.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal couponDiscount = viewModel.calculateCouponDiscount(); + BigDecimal loyaltyDiscount = viewModel.calculateLoyaltyDiscount(); + BigDecimal total = subtotal.subtract(couponDiscount).subtract(loyaltyDiscount); + + binding.tvSaleSubtotal.setText("$" + String.format(Locale.getDefault(), "%.2f", subtotal)); + + if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) { binding.llCouponDiscount.setVisibility(View.VISIBLE); - binding.tvSaleCouponDiscount.setText("-$" + discount); + binding.tvSaleCouponDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", couponDiscount)); } else { binding.llCouponDiscount.setVisibility(View.GONE); } - binding.tvSaleDetailTotal.setText("Total: $" + total); + + if (loyaltyDiscount.compareTo(BigDecimal.ZERO) > 0) { + binding.llLoyaltyDiscount.setVisibility(View.VISIBLE); + binding.tvLoyaltyDiscountLabel.setText("Loyalty Discount (" + viewModel.calculatePointsToUse() + " pts):"); + binding.tvSaleLoyaltyDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", loyaltyDiscount)); + } else { + binding.llLoyaltyDiscount.setVisibility(View.GONE); + } + + binding.tvSaleDetailTotal.setText("Total: $" + String.format(Locale.getDefault(), "%.2f", total)); } private void saveSale() { if (!InputValidator.isSpinnerSelected(binding.spinnerSaleStore, "Store")) return; if (viewModel.getCartItems().getValue() == null || viewModel.getCartItems().getValue().isEmpty()) { - Toast.makeText(getContext(), "Add at least one item", Toast.LENGTH_SHORT).show(); + UIUtils.showToast(getContext(), "Add at least one item"); return; } @@ -390,12 +434,17 @@ public class SaleDetailFragment extends Fragment { SaleDTO dto = new SaleDTO(store.getId(), payment, viewModel.getCartItems().getValue(), false, null, customerId); dto.setCouponId(viewModel.getAppliedCouponId()); + + if (Boolean.TRUE.equals(viewModel.getUseLoyaltyPoints().getValue())) { + dto.setPointsUsed(viewModel.calculatePointsToUse()); + dto.setLoyaltyDiscountAmount(viewModel.calculateLoyaltyDiscount()); + } viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); + UIUtils.showToast(getContext(), "Sale saved!"); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java index 658264c9..68369cce 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.SaleDTO; @@ -39,6 +40,8 @@ public class SaleDetailViewModel extends ViewModel { private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> cartItems = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData appliedCoupon = new MutableLiveData<>(null); + private final MutableLiveData selectedCustomerData = new MutableLiveData<>(null); + private final MutableLiveData useLoyaltyPoints = new MutableLiveData<>(false); private final MutableLiveData isLoading = new MutableLiveData<>(false); @Inject @@ -111,6 +114,34 @@ public class SaleDetailViewModel extends ViewModel { appliedCoupon.setValue(coupon); } + public void setUseLoyaltyPoints(boolean use) { + useLoyaltyPoints.setValue(use); + } + + public LiveData getUseLoyaltyPoints() { + return useLoyaltyPoints; + } + + public LiveData getSelectedCustomerData() { + return selectedCustomerData; + } + + public void selectCustomer(Long customerId) { + if (customerId == null) { + selectedCustomerData.setValue(null); + useLoyaltyPoints.setValue(false); + return; + } + customerRepository.getCustomerById(customerId).observeForever(new androidx.lifecycle.Observer>() { + @Override + public void onChanged(Resource resource) { + if (resource != null && resource.status == Resource.Status.SUCCESS) { + selectedCustomerData.setValue(resource.data); + } + } + }); + } + public void clearCoupon() { appliedCoupon.setValue(null); } @@ -125,6 +156,10 @@ public class SaleDetailViewModel extends ViewModel { } public BigDecimal calculateDiscount() { + return calculateCouponDiscount().add(calculateLoyaltyDiscount()); + } + + public BigDecimal calculateCouponDiscount() { CouponDTO coupon = appliedCoupon.getValue(); if (coupon == null || coupon.getDiscountValue() == null) return BigDecimal.ZERO; BigDecimal subtotal = calculateSubtotal(); @@ -135,6 +170,25 @@ public class SaleDetailViewModel extends ViewModel { } } + public BigDecimal calculateLoyaltyDiscount() { + if (Boolean.FALSE.equals(useLoyaltyPoints.getValue())) return BigDecimal.ZERO; + CustomerDTO customer = selectedCustomerData.getValue(); + if (customer == null || customer.getLoyaltyPoints() == null || customer.getLoyaltyPoints() < 20) { + return BigDecimal.ZERO; + } + + BigDecimal subtotalAfterCoupon = calculateSubtotal().subtract(calculateCouponDiscount()); + int maxPointsNeeded = subtotalAfterCoupon.multiply(BigDecimal.valueOf(20)).intValue(); + int pointsToUse = Math.min(customer.getLoyaltyPoints(), maxPointsNeeded); + + return BigDecimal.valueOf(pointsToUse).multiply(BigDecimal.valueOf(0.05)).setScale(2, java.math.RoundingMode.HALF_UP); + } + + public int calculatePointsToUse() { + BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(); + return loyaltyDiscount.divide(BigDecimal.valueOf(0.05), 0, java.math.RoundingMode.HALF_UP).intValue(); + } + public BigDecimal calculateSubtotal() { BigDecimal total = BigDecimal.ZERO; List items = cartItems.getValue(); diff --git a/android/app/src/main/res/layout/fragment_sale_detail.xml b/android/app/src/main/res/layout/fragment_sale_detail.xml index 3e5825e2..e92f83d4 100644 --- a/android/app/src/main/res/layout/fragment_sale_detail.xml +++ b/android/app/src/main/res/layout/fragment_sale_detail.xml @@ -128,7 +128,30 @@ android:id="@+id/spinnerSaleCustomer" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="16dp"/> + android:layout_marginBottom="8dp"/> + + + + + + + + + +