Used the wrong endpoint for populating species, changed to to the correct one
also added coupon option to sales
This commit is contained in:
@@ -47,6 +47,9 @@ public interface PetApi {
|
||||
@GET("api/v1/dropdowns/pets")
|
||||
Call<List<DropdownDTO>> getPetDropdowns();
|
||||
|
||||
@GET("api/v1/dropdowns/pet-species")
|
||||
Call<List<DropdownDTO>> getPetSpeciesDropdowns();
|
||||
|
||||
// Get pet by id
|
||||
@GET("api/v1/pets/{id}")
|
||||
Call<PetDTO> getPetById(@Path("id") Long id);
|
||||
|
||||
@@ -102,6 +102,10 @@ public class SaleDTO {
|
||||
return couponId;
|
||||
}
|
||||
|
||||
public void setCouponId(Long couponId) {
|
||||
this.couponId = couponId;
|
||||
}
|
||||
|
||||
public Integer getPointsEarned() {
|
||||
return pointsEarned;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -30,6 +30,10 @@ public class CouponRepository extends BaseRepository {
|
||||
return executeCall(couponApi.getCouponById(id));
|
||||
}
|
||||
|
||||
public LiveData<Resource<CouponDTO>> getCouponByCode(String code) {
|
||||
return executeCall(couponApi.getCouponByCode(code));
|
||||
}
|
||||
|
||||
public LiveData<Resource<CouponDTO>> createCoupon(CouponDTO coupon) {
|
||||
return executeCall(couponApi.createCoupon(coupon));
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@ public class PetRepository extends BaseRepository {
|
||||
return executeCall(petApi.getPetDropdowns());
|
||||
}
|
||||
|
||||
public LiveData<Resource<List<DropdownDTO>>> getPetSpeciesDropdowns() {
|
||||
return executeCall(petApi.getPetSpeciesDropdowns());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves available pets for a specific store.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<List<PetDTO>> pets = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<String>> speciesOptions = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
@Inject
|
||||
@@ -38,6 +40,7 @@ public class PetListViewModel extends ViewModel {
|
||||
|
||||
public LiveData<List<PetDTO>> getPets() { return pets; }
|
||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||
public LiveData<List<String>> getSpeciesOptions() { return speciesOptions; }
|
||||
public LiveData<Boolean> 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<String> 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) {
|
||||
|
||||
@@ -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<List<DropdownDTO>> customerList = new MutableLiveData<>(new ArrayList<>());
|
||||
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<Boolean> 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<List<SaleDTO.SaleItemDTO>> getCartItems() { return cartItems; }
|
||||
|
||||
public LiveData<Resource<CouponDTO>> lookupCoupon(String code) {
|
||||
return couponRepository.getCouponByCode(code);
|
||||
}
|
||||
|
||||
public void setAppliedCoupon(CouponDTO coupon) {
|
||||
appliedCoupon.setValue(coupon);
|
||||
}
|
||||
|
||||
public void clearCoupon() {
|
||||
appliedCoupon.setValue(null);
|
||||
}
|
||||
|
||||
public LiveData<CouponDTO> 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<SaleDTO.SaleItemDTO> items = cartItems.getValue();
|
||||
|
||||
@@ -60,6 +60,17 @@
|
||||
android:padding="16dp"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCouponId"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_light"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="italic"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<!-- Coupon Code -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -262,6 +262,66 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llCouponInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="12dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Coupon Code"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etCouponCode"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="Enter coupon code"
|
||||
android:inputType="textCapCharacters"
|
||||
android:layout_marginEnd="8dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnApplyCoupon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Apply"
|
||||
android:backgroundTint="@color/primary_medium"
|
||||
android:textColor="@color/white"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnClearCoupon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Clear"
|
||||
android:backgroundTint="@color/accent_coral"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginStart="4dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCouponInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
|
||||
Reference in New Issue
Block a user