From 03118871858e0aac5260190bb528659167f2402e Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sat, 11 Apr 2026 23:50:22 -0600 Subject: [PATCH] Used the wrong endpoint for populating species, changed to to the correct one also added coupon option to sales --- .../example/petstoremobile/api/PetApi.java | 3 + .../example/petstoremobile/dtos/SaleDTO.java | 4 + .../fragments/listfragments/PetFragment.java | 10 ++- .../detailfragments/CouponDetailFragment.java | 4 + .../detailfragments/SaleDetailFragment.java | 79 ++++++++++++++++++- .../repositories/CouponRepository.java | 4 + .../repositories/PetRepository.java | 4 + .../viewmodels/PetDetailViewModel.java | 2 +- .../viewmodels/PetListViewModel.java | 16 ++++ .../viewmodels/SaleDetailViewModel.java | 40 +++++++++- .../res/layout/fragment_coupon_detail.xml | 11 +++ .../main/res/layout/fragment_sale_detail.xml | 60 ++++++++++++++ 12 files changed, 231 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java index d13a5ed4..97423002 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -47,6 +47,9 @@ public interface PetApi { @GET("api/v1/dropdowns/pets") Call> getPetDropdowns(); + @GET("api/v1/dropdowns/pet-species") + Call> getPetSpeciesDropdowns(); + // Get pet by id @GET("api/v1/pets/{id}") Call getPetById(@Path("id") Long id); 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 edb2a132..f309978f 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 @@ -102,6 +102,10 @@ public class SaleDTO { return couponId; } + public void setCouponId(Long couponId) { + this.couponId = couponId; + } + public Integer getPointsEarned() { return pointsEarned; } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index 5c94113b..a9d153e4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -88,6 +88,11 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { binding.swipeRefreshPet.setRefreshing(loading); }); + + viewModel.getSpeciesOptions().observe(getViewLifecycleOwner(), options -> { + String[] arr = options.toArray(new String[0]); + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, arr, this::loadPetData); + }); } private void setupBulkDelete() { @@ -107,6 +112,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen public void onResume() { super.onResume(); loadPetData(); + viewModel.loadSpecies(); if (!isStaff()) viewModel.loadStores(); } @@ -135,8 +141,8 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } private void setupSpeciesFilter() { - String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"}; - SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, species, this::loadPetData); + String[] initial = {"All Species"}; + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, initial, this::loadPetData); } private void setupStoreFilter() { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java index 7646358f..ff172bdc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java @@ -16,6 +16,7 @@ import androidx.navigation.Navigation; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentCouponDetailBinding; import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.UIUtils; @@ -95,6 +96,9 @@ public class CouponDetailFragment extends Fragment { } private void loadCouponDetails() { + binding.tvCouponId.setText(DateTimeUtils.formatId(couponId)); + binding.tvCouponId.setVisibility(View.VISIBLE); + viewModel.loadCoupon(couponId).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { CouponDTO coupon = resource.data; 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 11e8b9f3..5208720f 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 @@ -48,11 +48,13 @@ public class SaleDetailFragment extends Fragment { if (viewModel.isViewOnly()) { binding.llAddItemRow.setVisibility(View.GONE); + binding.llCouponInput.setVisibility(View.GONE); binding.btnSaveSale.setVisibility(View.GONE); UIUtils.setViewsEnabled(false, binding.spinnerSaleStore, binding.spinnerSaleCustomer, binding.spinnerPaymentMethod); } else { loadData(); setupAddItem(); + setupCoupon(); } binding.btnSaleBack.setOnClickListener(v -> navigateBack()); @@ -206,6 +208,70 @@ public class SaleDetailFragment extends Fragment { }); } + private void setupCoupon() { + 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(); + return; + } + setLoading(true); + viewModel.lookupCoupon(code).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + CouponDTO coupon = resource.data; + if (Boolean.FALSE.equals(coupon.getActive())) { + showCouponError("This coupon is no longer active."); + return; + } + if (coupon.getMinOrderAmount() != null && + viewModel.calculateSubtotal().compareTo(coupon.getMinOrderAmount()) < 0) { + showCouponError("Minimum order of $" + coupon.getMinOrderAmount() + " required."); + return; + } + viewModel.setAppliedCoupon(coupon); + applyAppliedCouponUI(coupon); + updateTotal(); + } else if (resource.status == Resource.Status.ERROR) { + showCouponError("Invalid coupon code."); + } + }); + }); + + binding.btnClearCoupon.setOnClickListener(v -> { + viewModel.clearCoupon(); + binding.etCouponCode.setText(""); + binding.tvCouponInfo.setVisibility(View.GONE); + binding.btnClearCoupon.setVisibility(View.GONE); + binding.btnApplyCoupon.setVisibility(View.VISIBLE); + binding.etCouponCode.setEnabled(true); + binding.llCouponDiscount.setVisibility(View.GONE); + updateTotal(); + }); + } + + private void applyAppliedCouponUI(CouponDTO coupon) { + String info; + if ("PERCENTAGE".equalsIgnoreCase(coupon.getDiscountType())) { + info = coupon.getDiscountValue().stripTrailingZeros().toPlainString() + "% off applied"; + } else { + info = "$" + coupon.getDiscountValue() + " off applied"; + } + binding.tvCouponInfo.setText(info); + binding.tvCouponInfo.setTextColor(0xFF4CAF50); + binding.tvCouponInfo.setVisibility(View.VISIBLE); + binding.btnClearCoupon.setVisibility(View.VISIBLE); + binding.btnApplyCoupon.setVisibility(View.GONE); + binding.etCouponCode.setEnabled(false); + } + + private void showCouponError(String message) { + binding.tvCouponInfo.setText(message); + binding.tvCouponInfo.setTextColor(0xFFE53935); + binding.tvCouponInfo.setVisibility(View.VISIBLE); + } + private void setupAddItem() { binding.btnAddItem.setOnClickListener(v -> { if (!InputValidator.isSpinnerSelected(binding.spinnerSaleProduct, "Product")) return; @@ -271,8 +337,16 @@ public class SaleDetailFragment extends Fragment { } private void updateTotal() { - BigDecimal total = viewModel.calculateSubtotal(); - binding.tvSaleSubtotal.setText("$" + total); + BigDecimal subtotal = viewModel.calculateSubtotal(); + BigDecimal discount = viewModel.calculateDiscount(); + BigDecimal total = subtotal.subtract(discount); + binding.tvSaleSubtotal.setText("$" + subtotal); + if (discount.compareTo(BigDecimal.ZERO) > 0) { + binding.llCouponDiscount.setVisibility(View.VISIBLE); + binding.tvSaleCouponDiscount.setText("-$" + discount); + } else { + binding.llCouponDiscount.setVisibility(View.GONE); + } binding.tvSaleDetailTotal.setText("Total: $" + total); } @@ -293,6 +367,7 @@ public class SaleDetailFragment extends Fragment { } SaleDTO dto = new SaleDTO(store.getId(), payment, viewModel.getCartItems().getValue(), false, null, customerId); + dto.setCouponId(viewModel.getAppliedCouponId()); viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java index 338043be..f97d1ca0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java @@ -30,6 +30,10 @@ public class CouponRepository extends BaseRepository { return executeCall(couponApi.getCouponById(id)); } + public LiveData> getCouponByCode(String code) { + return executeCall(couponApi.getCouponByCode(code)); + } + public LiveData> createCoupon(CouponDTO coupon) { return executeCall(couponApi.createCoupon(coupon)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index b1eda73f..a186cc7e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -54,6 +54,10 @@ public class PetRepository extends BaseRepository { return executeCall(petApi.getPetDropdowns()); } + public LiveData>> getPetSpeciesDropdowns() { + return executeCall(petApi.getPetSpeciesDropdowns()); + } + /** * Retrieves available pets for a specific store. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java index dd0d302a..ace8fa2b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java @@ -61,7 +61,7 @@ public class PetDetailViewModel extends ViewModel { } }); - observeOnce(petRepository.getPetDropdowns(), resource -> { + observeOnce(petRepository.getPetSpeciesDropdowns(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { speciesList.setValue(resource.data); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java index 2c1132ae..af6770f8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.StoreDTO; @@ -28,6 +29,7 @@ public class PetListViewModel extends ViewModel { private final MutableLiveData> pets = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> speciesOptions = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); @Inject @@ -38,6 +40,7 @@ public class PetListViewModel extends ViewModel { public LiveData> getPets() { return pets; } public LiveData> getStores() { return stores; } + public LiveData> getSpeciesOptions() { return speciesOptions; } public LiveData getIsLoading() { return isLoading; } public void loadPets(String query, String status, String species, Long storeId) { @@ -57,6 +60,19 @@ public class PetListViewModel extends ViewModel { }); } + public void loadSpecies() { + observeOnce(petRepository.getPetSpeciesDropdowns(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + List labels = new ArrayList<>(); + labels.add("All Species"); + for (DropdownDTO dto : resource.data) { + labels.add(dto.getLabel()); + } + speciesOptions.setValue(labels); + } + }); + } + public void loadStores() { observeOnce(storeRepository.getAllStores(0, 100), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { 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 201fae9e..cb090feb 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 @@ -4,9 +4,11 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import com.example.petstoremobile.dtos.CouponDTO; import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.repositories.CouponRepository; import com.example.petstoremobile.repositories.CustomerRepository; import com.example.petstoremobile.repositories.ProductRepository; import com.example.petstoremobile.repositories.SaleRepository; @@ -27,6 +29,7 @@ public class SaleDetailViewModel extends ViewModel { private final StoreRepository storeRepository; private final CustomerRepository customerRepository; private final ProductRepository productRepository; + private final CouponRepository couponRepository; private long saleId = -1; private boolean viewOnly = false; @@ -35,15 +38,18 @@ public class SaleDetailViewModel extends ViewModel { private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); 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 isLoading = new MutableLiveData<>(false); @Inject public SaleDetailViewModel(SaleRepository saleRepository, StoreRepository storeRepository, - CustomerRepository customerRepository, ProductRepository productRepository) { + CustomerRepository customerRepository, ProductRepository productRepository, + CouponRepository couponRepository) { this.saleRepository = saleRepository; this.storeRepository = storeRepository; this.customerRepository = customerRepository; this.productRepository = productRepository; + this.couponRepository = couponRepository; } public void setSaleId(long id, boolean viewOnly) { @@ -91,6 +97,38 @@ public class SaleDetailViewModel extends ViewModel { public LiveData> getCartItems() { return cartItems; } + public LiveData> lookupCoupon(String code) { + return couponRepository.getCouponByCode(code); + } + + public void setAppliedCoupon(CouponDTO coupon) { + appliedCoupon.setValue(coupon); + } + + public void clearCoupon() { + appliedCoupon.setValue(null); + } + + public LiveData getAppliedCoupon() { + return appliedCoupon; + } + + public Long getAppliedCouponId() { + CouponDTO coupon = appliedCoupon.getValue(); + return coupon != null ? coupon.getCouponId() : null; + } + + public BigDecimal calculateDiscount() { + CouponDTO coupon = appliedCoupon.getValue(); + if (coupon == null || coupon.getDiscountValue() == null) return BigDecimal.ZERO; + BigDecimal subtotal = calculateSubtotal(); + if ("PERCENTAGE".equalsIgnoreCase(coupon.getDiscountType())) { + return subtotal.multiply(coupon.getDiscountValue()).divide(BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP); + } else { + return coupon.getDiscountValue().min(subtotal); + } + } + public BigDecimal calculateSubtotal() { BigDecimal total = BigDecimal.ZERO; List items = cartItems.getValue(); diff --git a/android/app/src/main/res/layout/fragment_coupon_detail.xml b/android/app/src/main/res/layout/fragment_coupon_detail.xml index 64bf1f8a..29a45ae4 100644 --- a/android/app/src/main/res/layout/fragment_coupon_detail.xml +++ b/android/app/src/main/res/layout/fragment_coupon_detail.xml @@ -60,6 +60,17 @@ android:padding="16dp" android:layout_marginBottom="16dp"> + + + + + + + + + + +