added loyaltypoint usage to sales unfinished still needs to work with the backend

This commit is contained in:
Alex
2026-04-13 18:52:07 -06:00
parent 884f56c9a7
commit 6efa440bbc
4 changed files with 184 additions and 19 deletions

View File

@@ -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;
}

View File

@@ -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;
}
@@ -391,11 +435,16 @@ 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);

View File

@@ -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<List<ProductDTO>> productList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<SaleDTO.SaleItemDTO>> cartItems = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<CouponDTO> appliedCoupon = new MutableLiveData<>(null);
private final MutableLiveData<CustomerDTO> selectedCustomerData = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> useLoyaltyPoints = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> 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<Boolean> getUseLoyaltyPoints() {
return useLoyaltyPoints;
}
public LiveData<CustomerDTO> 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<Resource<CustomerDTO>>() {
@Override
public void onChanged(Resource<CustomerDTO> 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<SaleDTO.SaleItemDTO> items = cartItems.getValue();

View File

@@ -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"/>
<LinearLayout
android:id="@+id/llLoyaltyPoints"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp"
android:visibility="gone">
<CheckBox
android:id="@+id/cbUseLoyaltyPoints"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Use Loyalty Points"/>
<TextView
android:id="@+id/tvAvailablePoints"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/text_light"
android:textSize="12sp"
android:text="(Available: 0)"/>
</LinearLayout>
<!-- Payment Method -->
<TextView
@@ -390,6 +413,27 @@
android:textColor="@color/status_adopted"/>
</LinearLayout>
<LinearLayout
android:id="@+id/llLoyaltyDiscount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:visibility="gone">
<TextView
android:id="@+id/tvLoyaltyDiscountLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Loyalty Discount:"/>
<TextView
android:id="@+id/tvSaleLoyaltyDiscount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="-$0.00"
android:textColor="@color/status_adopted"/>
</LinearLayout>
<TextView
android:id="@+id/tvSaleDetailTotal"
android:layout_width="wrap_content"