From 884f56c9a7ad66a6d0aac0de5a3aeb7ab8687a44 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:25:11 -0600 Subject: [PATCH] Can now edit loyalty points for customer on andriod, and pets now have breed dropdown --- .../example/petstoremobile/api/PetApi.java | 3 ++ .../petstoremobile/dtos/CustomerDTO.java | 4 ++ .../CustomerDetailFragment.java | 24 ++++++++--- .../detailfragments/PetDetailFragment.java | 31 ++++++++++++-- .../detailfragments/StaffDetailFragment.java | 2 +- .../repositories/PetRepository.java | 4 ++ .../viewmodels/CustomerDetailViewModel.java | 13 ++++++ .../viewmodels/PetDetailViewModel.java | 42 ++++++++++++++++++- .../viewmodels/StaffDetailViewModel.java | 8 ++++ .../res/layout/fragment_customer_detail.xml | 11 +++-- .../main/res/layout/fragment_pet_detail.xml | 9 ++-- .../petshop/backend/dto/user/UserRequest.java | 10 +++++ .../petshop/backend/service/UserService.java | 6 +++ 13 files changed, 143 insertions(+), 24 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 b03ab44d..b554166b 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 @@ -50,6 +50,9 @@ public interface PetApi { @GET("api/v1/dropdowns/pet-species") Call> getPetSpeciesDropdowns(); + @GET("api/v1/dropdowns/pet-breeds") + Call> getPetBreedsDropdowns(@Query("species") String species); + // 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/CustomerDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java index 52d96a1e..c190342e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java @@ -17,6 +17,7 @@ public class CustomerDTO { private String createdAt; private String updatedAt; private String password; + private String role; public CustomerDTO() {} @@ -73,4 +74,7 @@ public class CustomerDTO { public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } + + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java index f7a8c5a3..455450cc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java @@ -14,8 +14,11 @@ import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.CustomerDetailViewModel; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.utils.Resource; +import javax.inject.Inject; + import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint @@ -24,6 +27,8 @@ public class CustomerDetailFragment extends Fragment { private FragmentCustomerDetailBinding binding; private CustomerDetailViewModel viewModel; + @Inject TokenManager tokenManager; + private final String[] STATUSES = {"Active", "Inactive"}; @Override @@ -65,7 +70,10 @@ public class CustomerDetailFragment extends Fragment { // Show loyalty points binding.tvLoyaltyPointsLabel.setVisibility(View.VISIBLE); - binding.tvCustomerLoyaltyPoints.setVisibility(View.VISIBLE); + binding.etCustomerLoyaltyPoints.setVisibility(View.VISIBLE); + + boolean isAdmin = "ADMIN".equalsIgnoreCase(tokenManager.getRole()); + binding.etCustomerLoyaltyPoints.setEnabled(isAdmin); loadCustomerData(customerId); } else { @@ -74,7 +82,7 @@ public class CustomerDetailFragment extends Fragment { binding.btnDeleteCustomer.setVisibility(View.GONE); binding.tvCustomerId.setVisibility(View.GONE); binding.tvLoyaltyPointsLabel.setVisibility(View.GONE); - binding.tvCustomerLoyaltyPoints.setVisibility(View.GONE); + binding.etCustomerLoyaltyPoints.setVisibility(View.GONE); } } @@ -91,7 +99,7 @@ public class CustomerDetailFragment extends Fragment { binding.etCustomerPhone.setText(c.getPhone() != null ? c.getPhone() : ""); binding.spinnerCustomerStatus.setSelection(Boolean.TRUE.equals(c.getActive()) ? 0 : 1); int pts = c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0; - binding.tvCustomerLoyaltyPoints.setText(String.valueOf(pts)); + binding.etCustomerLoyaltyPoints.setText(String.valueOf(pts)); } } }); @@ -121,6 +129,12 @@ public class CustomerDetailFragment extends Fragment { if (!InputValidator.isValidEmail(binding.etCustomerEmail)) return; if (!InputValidator.isValidPhone(binding.etCustomerPhone)) return; + Integer loyaltyPoints = null; + if (viewModel.isEditing()) { + if (!InputValidator.isPositiveInteger(binding.etCustomerLoyaltyPoints, "Loyalty Points")) return; + loyaltyPoints = Integer.parseInt(binding.etCustomerLoyaltyPoints.getText().toString().trim()); + } + String username = binding.etCustomerUsername.getText().toString().trim(); String password = viewModel.isEditing() ? null : binding.etCustomerPassword.getText().toString().trim(); String firstName = binding.etCustomerFirstName.getText().toString().trim(); @@ -129,9 +143,7 @@ public class CustomerDetailFragment extends Fragment { String phone = binding.etCustomerPhone.getText().toString().trim(); boolean active = binding.spinnerCustomerStatus.getSelectedItemPosition() == 0; - CustomerDTO dto = new CustomerDTO(username, password, firstName, lastName, email, phone); - dto.setFullName(firstName + " " + lastName); - dto.setActive(active); + CustomerDTO dto = viewModel.createCustomerDto(username, password, firstName, lastName, email, phone, active, loyaltyPoints); viewModel.saveCustomer(dto).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index 2d5f1575..69ff8215 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -101,6 +101,14 @@ public class PetDetailFragment extends Fragment { DropdownDTO::getLabel, "-- Select Species --", null, DropdownDTO::getId); SpinnerUtils.setSelectionByValue(binding.spinnerPetSpecies, selectedSpecies); }); + + viewModel.getBreedList().observe(getViewLifecycleOwner(), list -> { + PetDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + String selectedBreed = state != null ? state.selectedBreed : null; + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPetBreed, list, + DropdownDTO::getLabel, "-- Select Breed --", null, DropdownDTO::getId); + SpinnerUtils.setSelectionByValue(binding.spinnerPetBreed, selectedBreed); + }); } private void setLoading(boolean loading) { @@ -118,7 +126,7 @@ public class PetDetailFragment extends Fragment { private void savePet() { if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerPetSpecies, "Species")) return; - if (!InputValidator.isNotEmpty(binding.etPetBreed, "Breed")) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerPetBreed, "Breed")) return; if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return; if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return; @@ -127,7 +135,11 @@ public class PetDetailFragment extends Fragment { String species = (speciesOptions != null && binding.spinnerPetSpecies.getSelectedItemPosition() > 0) ? speciesOptions.get(binding.spinnerPetSpecies.getSelectedItemPosition() - 1).getLabel() : ""; - String breed = binding.etPetBreed.getText().toString().trim(); + + List breedOptions = viewModel.getBreedList().getValue(); + String breed = (breedOptions != null && binding.spinnerPetBreed.getSelectedItemPosition() > 0) + ? breedOptions.get(binding.spinnerPetBreed.getSelectedItemPosition() - 1).getLabel() + : ""; int age = Integer.parseInt(binding.etPetAge.getText().toString().trim()); double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim()); String status = binding.spinnerPetStatus.getSelectedItem().toString(); @@ -236,7 +248,6 @@ public class PetDetailFragment extends Fragment { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { PetDTO p = resource.data; binding.etPetName.setText(p.getPetName()); - binding.etPetBreed.setText(p.getPetBreed()); binding.etPetAge.setText(String.valueOf(p.getPetAge())); if (p.getPetPrice() != null) { binding.etPetPrice.setText(String.format(Locale.getDefault(), "%.2f", p.getPetPrice())); @@ -279,6 +290,11 @@ public class PetDetailFragment extends Fragment { viewModel.onSpeciesSelected(p); }); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPetBreed, p -> { + if (isUpdatingUI) return; + viewModel.onBreedSelected(p); + }); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerCustomer, p -> { if (isUpdatingUI) return; viewModel.onCustomerSelected(p); @@ -306,7 +322,7 @@ public class PetDetailFragment extends Fragment { binding.btnSavePet.setText(state.saveButtonText); UIUtils.setViewsEnabled(state.isSpeciesEnabled, binding.spinnerPetSpecies); - UIUtils.setViewsEnabled(state.isBreedEnabled, binding.etPetBreed); + UIUtils.setViewsEnabled(state.isBreedEnabled, binding.spinnerPetBreed); UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer); UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore); @@ -323,6 +339,13 @@ public class PetDetailFragment extends Fragment { SpinnerUtils.setSelectionByValue(binding.spinnerPetSpecies, state.selectedSpecies); } + List breeds = viewModel.getBreedList().getValue(); + if (breeds != null) { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPetBreed, breeds, + DropdownDTO::getLabel, "-- Select Breed --", null, DropdownDTO::getId); + SpinnerUtils.setSelectionByValue(binding.spinnerPetBreed, state.selectedBreed); + } + if (!state.isCustomerEnabled && binding.spinnerCustomer.getSelectedItemPosition() != 0) { binding.spinnerCustomer.setSelection(0); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java index acf171ac..94b2b307 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java @@ -161,7 +161,7 @@ public class StaffDetailFragment extends Fragment { List stores = viewModel.getStoreList().getValue(); Long storeId = stores.get(binding.spinnerStaffStore.getSelectedItemPosition() - 1).getId(); - EmployeeDTO dto = new EmployeeDTO( + EmployeeDTO dto = viewModel.createEmployeeDto( username, password.isEmpty() ? null : password, firstName, 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 f3114beb..11bb9ff1 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 @@ -58,6 +58,10 @@ public class PetRepository extends BaseRepository { return executeCall(petApi.getPetSpeciesDropdowns()); } + public LiveData>> getPetBreedsDropdowns(String species) { + return executeCall(petApi.getPetBreedsDropdowns(species)); + } + /** * Retrieves available pets for a specific store. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java index e7864b66..0d0ebfaf 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java @@ -31,6 +31,19 @@ public class CustomerDetailViewModel extends ViewModel { public long getCustomerId() { return customerId; } public boolean isEditing() { return isEditing; } + public CustomerDTO createCustomerDto(String username, String password, String firstName, + String lastName, String email, String phone, + boolean active, Integer loyaltyPoints) { + CustomerDTO dto = new CustomerDTO(username, password, firstName, lastName, email, phone); + dto.setFullName(firstName + " " + lastName); + dto.setActive(active); + dto.setRole("CUSTOMER"); + if (isEditing && loyaltyPoints != null) { + dto.setLoyaltyPoints(loyaltyPoints); + } + return dto; + } + public LiveData> loadCustomer(long id) { return repository.getCustomerById(id); } 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 25eddbbb..ddb868a7 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 @@ -34,6 +34,7 @@ public class PetDetailViewModel extends ViewModel { private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> speciesList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> breedList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); @@ -41,6 +42,7 @@ public class PetDetailViewModel extends ViewModel { private Long selectedCustomerId = null; private Long selectedStoreId = null; private String selectedSpecies = null; + private String selectedBreed = null; private boolean isOriginallyOwnedOrAdopted = false; private Long originalCustomerId = null; @@ -112,10 +114,33 @@ public class PetDetailViewModel extends ViewModel { List list = speciesList.getValue(); if (position > 0 && list != null && position <= list.size()) { selectedSpecies = list.get(position - 1).getLabel(); + loadBreeds(selectedSpecies); } else { selectedSpecies = null; + breedList.setValue(new ArrayList<>()); } - updateViewState(state -> state.selectedSpecies = selectedSpecies); + updateViewState(state -> { + state.selectedSpecies = selectedSpecies; + state.isBreedEnabled = !state.isEditing && (selectedSpecies != null); + }); + } + + public void onBreedSelected(int position) { + List list = breedList.getValue(); + if (position > 0 && list != null && position <= list.size()) { + selectedBreed = list.get(position - 1).getLabel(); + } else { + selectedBreed = null; + } + updateViewState(state -> state.selectedBreed = selectedBreed); + } + + private void loadBreeds(String species) { + observeOnce(petRepository.getPetBreedsDropdowns(species), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + breedList.setValue(resource.data); + } + }); } public void onStoreSelected(int position) { @@ -155,12 +180,15 @@ public class PetDetailViewModel extends ViewModel { selectedCustomerId = null; selectedStoreId = null; selectedSpecies = null; + selectedBreed = null; state.selectedCustomerId = null; state.selectedStoreId = null; state.selectedSpecies = null; + state.selectedBreed = null; state.selectedStatus = STATUS_AVAILABLE; state.isCustomerEnabled = false; state.isStoreEnabled = true; + state.isBreedEnabled = false; } }); } @@ -173,15 +201,22 @@ public class PetDetailViewModel extends ViewModel { selectedCustomerId = pet.getCustomerId(); selectedStoreId = pet.getStoreId(); selectedSpecies = pet.getPetSpecies(); + selectedBreed = pet.getPetBreed(); isOriginallyOwnedOrAdopted = STATUS_OWNED.equalsIgnoreCase(pet.getPetStatus()) || STATUS_ADOPTED.equalsIgnoreCase(pet.getPetStatus()); originalCustomerId = pet.getCustomerId(); + if (selectedSpecies != null) { + loadBreeds(selectedSpecies); + } + updateViewState(state -> { state.selectedCustomerId = selectedCustomerId; state.selectedStoreId = selectedStoreId; state.selectedSpecies = selectedSpecies; + state.selectedBreed = selectedBreed; state.selectedStatus = normalizeStatus(pet.getPetStatus()); + state.isBreedEnabled = !state.isEditing && (selectedSpecies != null); applyStatusRules(state, false); }); } @@ -216,6 +251,10 @@ public class PetDetailViewModel extends ViewModel { return speciesList; } + public LiveData> getBreedList() { + return breedList; + } + public LiveData getIsLoading() { return isLoading; } @@ -295,6 +334,7 @@ public class PetDetailViewModel extends ViewModel { public String[] availableStatuses = new String[]{STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED, STATUS_PENDING}; public String selectedStatus = STATUS_AVAILABLE; public String selectedSpecies = null; + public String selectedBreed = null; public Long selectedCustomerId = null; public Long selectedStoreId = null; } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java index cbee3bc3..7927465e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java @@ -60,6 +60,14 @@ public class StaffDetailViewModel extends ViewModel { return isEditing; } + public EmployeeDTO createEmployeeDto(String username, String password, String firstName, + String lastName, String email, String phone, + String role, String staffRole, boolean active, Long storeId) { + EmployeeDTO dto = new EmployeeDTO(username, password, firstName, lastName, email, phone, role, staffRole, active, storeId); + dto.setFullName(firstName + " " + lastName); + return dto; + } + public LiveData> saveEmployee(EmployeeDTO dto) { if (isEditing && employeeId > 0) { return repository.updateEmployee(employeeId, dto); diff --git a/android/app/src/main/res/layout/fragment_customer_detail.xml b/android/app/src/main/res/layout/fragment_customer_detail.xml index a2c63030..83033dc5 100644 --- a/android/app/src/main/res/layout/fragment_customer_detail.xml +++ b/android/app/src/main/res/layout/fragment_customer_detail.xml @@ -195,14 +195,13 @@ android:textSize="12sp" android:layout_marginBottom="4dp"/> - + android:hint="0" + android:inputType="number" + android:layout_marginBottom="16dp"/> diff --git a/android/app/src/main/res/layout/fragment_pet_detail.xml b/android/app/src/main/res/layout/fragment_pet_detail.xml index e9018362..e42d8e30 100644 --- a/android/app/src/main/res/layout/fragment_pet_detail.xml +++ b/android/app/src/main/res/layout/fragment_pet_detail.xml @@ -109,14 +109,11 @@ android:textSize="12sp" android:layout_marginBottom="4dp"/> - + android:layout_marginBottom="16dp"/>