Used the wrong endpoint for populating species, changed to to the correct one

also added coupon option to sales
This commit is contained in:
Alex
2026-04-11 23:50:22 -06:00
parent 8ae47ef056
commit 0311887185
12 changed files with 231 additions and 6 deletions

View File

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

View File

@@ -102,6 +102,10 @@ public class SaleDTO {
return couponId;
}
public void setCouponId(Long couponId) {
this.couponId = couponId;
}
public Integer getPointsEarned() {
return pointsEarned;
}

View File

@@ -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() {

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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.
*/

View File

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

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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"

View File

@@ -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"