From 3a78021b98cf765b4aab4b6ecf3a87493b61c884 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Fri, 10 Apr 2026 04:31:10 -0600 Subject: [PATCH] Added so adoption status can be missed and fixed adoption bugs for andriod --- .../petstoremobile/api/ProductApi.java | 9 + .../listfragments/ProductFragment.java | 8 +- .../AdoptionDetailFragment.java | 43 ++-- .../AppointmentDetailFragment.java | 4 +- .../InventoryDetailFragment.java | 10 +- .../ProductDetailFragment.java | 10 +- .../repositories/PetRepository.java | 7 + .../repositories/ProductRepository.java | 17 ++ .../petstoremobile/utils/DateTimeUtils.java | 25 +++ .../viewmodels/AdoptionDetailViewModel.java | 190 +++++++++++++++--- .../AppointmentDetailViewModel.java | 11 + .../viewmodels/InventoryDetailViewModel.java | 10 +- .../viewmodels/ProductDetailViewModel.java | 11 +- .../viewmodels/ProductListViewModel.java | 10 +- .../res/layout/fragment_adoption_detail.xml | 30 +-- .../backend/service/AdoptionService.java | 6 +- 16 files changed, 317 insertions(+), 84 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java index 1d46107b..8aeb596e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java @@ -1,11 +1,14 @@ package com.example.petstoremobile.api; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import okhttp3.MultipartBody; import retrofit2.Call; import retrofit2.http.*; +import java.util.List; + public interface ProductApi { String PRODUCT_IMAGE_PATH = "api/v1/products/%d/image"; @@ -35,4 +38,10 @@ public interface ProductApi { @DELETE("api/v1/products/{id}/image") Call deleteProductImage(@Path("id") Long id); + + @GET("api/v1/dropdowns/products") + Call> getProductDropdowns(); + + @GET("api/v1/dropdowns/categories") + Call> getCategoryDropdowns(); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 84f076a2..566f4ee7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -16,7 +16,7 @@ import android.view.ViewGroup; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductAdapter; import com.example.petstoremobile.databinding.FragmentProductBinding; -import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; @@ -74,7 +74,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc viewModel.getCategories().observe(getViewLifecycleOwner(), list -> { SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, list, - CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId); + DropdownDTO::getLabel, "All Categories", -1L, DropdownDTO::getId); }); viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { @@ -111,9 +111,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc if (query.isEmpty()) query = null; Long categoryId = null; - List categories = viewModel.getCategories().getValue(); + List categories = viewModel.getCategories().getValue(); if (binding.spinnerCategory.getSelectedItemPosition() > 0 && categories != null && !categories.isEmpty()) { - categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId(); + categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getId(); } viewModel.loadProducts(query, categoryId); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 3c63819d..1e109213 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -32,8 +32,7 @@ public class AdoptionDetailFragment extends Fragment { private FragmentAdoptionDetailBinding binding; private AdoptionDetailViewModel viewModel; - - private final String[] STATUSES = {"Pending", "Completed", "Cancelled"}; + private boolean isUpdatingUI = false; @Override public void onCreate(Bundle savedInstanceState) { @@ -109,9 +108,7 @@ public class AdoptionDetailFragment extends Fragment { } private void setupSpinners() { - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES); - - UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, new String[]{}); binding.spinnerAdoptionCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override @@ -130,10 +127,22 @@ public class AdoptionDetailFragment extends Fragment { @Override public void onNothingSelected(AdapterView parent) {} }); + + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAdoptionPet, p -> viewModel.onPetSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAdoptionStatus, p -> notifyDateStatusChange()); } private void setupDatePicker() { - binding.etAdoptionDate.setOnClickListener(v -> UIUtils.showDatePicker(requireContext(), binding.etAdoptionDate, null)); + binding.etAdoptionDate.setOnClickListener(v -> + UIUtils.showDatePicker(requireContext(), binding.etAdoptionDate, this::notifyDateStatusChange)); + } + + private void notifyDateStatusChange() { + if (isUpdatingUI) return; + String date = binding.etAdoptionDate.getText().toString(); + Object selected = binding.spinnerAdoptionStatus.getSelectedItem(); + String status = selected != null ? selected.toString() : ""; + viewModel.onDateChanged(date, status); } private void handleArguments() { @@ -157,14 +166,25 @@ public class AdoptionDetailFragment extends Fragment { } private void applyViewState(AdoptionDetailViewModel.ViewState state) { + isUpdatingUI = true; + binding.tvAdoptionMode.setText(state.modeTitle); binding.tvAdoptionId.setText(DateTimeUtils.formatId(viewModel.getAdoptionId())); binding.tvAdoptionId.setVisibility(state.isAdoptionIdVisible ? View.VISIBLE : View.GONE); binding.btnDeleteAdoption.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); binding.btnSaveAdoption.setText(state.saveButtonText); + binding.btnSaveAdoption.setVisibility(state.isSaveVisible ? View.VISIBLE : View.GONE); + UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerAdoptionCustomer); UIUtils.setViewsEnabled(state.isPetEnabled, binding.spinnerAdoptionPet); + UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerAdoptionStore); UIUtils.setViewsEnabled(state.isEmployeeEnabled, binding.spinnerAdoptionEmployee); + UIUtils.setViewsEnabled(state.isDateEnabled, binding.etAdoptionDate); + UIUtils.setViewsEnabled(state.isFeeEnabled, binding.etAdoptionFee); + UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAdoptionStatus); + + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, state.availableStatuses); + SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, state.selectedStatus); if (!state.adoptionDate.isEmpty()) { binding.etAdoptionDate.setText(state.adoptionDate); @@ -172,9 +192,6 @@ public class AdoptionDetailFragment extends Fragment { if (!state.adoptionFee.isEmpty()) { binding.etAdoptionFee.setText(state.adoptionFee); } - if (!state.adoptionStatus.isEmpty()) { - SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, state.adoptionStatus); - } // Re-populate spinners with updated preselected IDs List pets = viewModel.getPetList().getValue(); @@ -192,6 +209,8 @@ public class AdoptionDetailFragment extends Fragment { List employees = viewModel.getEmployeeList().getValue(); if (employees != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, employees, DropdownDTO::getLabel, "-- Select Staff --", state.selectedEmployeeId, DropdownDTO::getId); + + isUpdatingUI = false; } private void saveAdoption() { @@ -203,8 +222,7 @@ public class AdoptionDetailFragment extends Fragment { BigDecimal fee = BigDecimal.ZERO; String feeStr = binding.etAdoptionFee.getText().toString().trim(); if (!feeStr.isEmpty()) { - if (!InputValidator.isPositiveDecimal(binding.etAdoptionFee, "Adoption Fee")) return; - fee = new BigDecimal(feeStr); + try { fee = new BigDecimal(feeStr); } catch (NumberFormatException ignored) {} } DropdownDTO customer = viewModel.getCustomerList().getValue().get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); @@ -217,7 +235,8 @@ public class AdoptionDetailFragment extends Fragment { } String adoptionDate = binding.etAdoptionDate.getText().toString().trim(); - String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; + Object selectedStatus = binding.spinnerAdoptionStatus.getSelectedItem(); + String status = selectedStatus != null ? selectedStatus.toString().toUpperCase() : ""; AdoptionDTO dto = new AdoptionDTO( pet.getId(), customer.getId(), employeeId, store.getId(), adoptionDate, status, fee); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index adfea86c..6c524d75 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -177,10 +177,8 @@ public class AppointmentDetailFragment extends Fragment { UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerMinute); UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAppointmentStatus); - Object selected = binding.spinnerAppointmentStatus.getSelectedItem(); - String current = selected != null ? selected.toString() : ""; SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, state.availableStatuses); - SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, current); + SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, state.selectedStatus); // Re-populate dropdown spinners with current selected IDs from ViewState List customers = viewModel.getCustomers().getValue(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index ee52c960..75f1a6b3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -92,7 +92,7 @@ public class InventoryDetailFragment extends Fragment { if (resource == null) return; setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - viewModel.setProductList(resource.data.getContent()); + viewModel.setProductList(resource.data); } }); } @@ -105,8 +105,8 @@ public class InventoryDetailFragment extends Fragment { private void refreshProductSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, viewModel.getProductList().getValue(), - ProductDTO::getProdName, "-- Select Product --", - preselectedProductId, ProductDTO::getProdId); + DropdownDTO::getLabel, "-- Select Product --", + preselectedProductId, DropdownDTO::getId); } private void handleArguments() { @@ -156,9 +156,9 @@ public class InventoryDetailFragment extends Fragment { int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim()); DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); - ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); + DropdownDTO product = viewModel.getProductList().getValue().get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); - InventoryDTO request = new InventoryDTO(product.getProdId(), store.getId(), quantity); + InventoryDTO request = new InventoryDTO(product.getId(), store.getId(), quantity); setButtonsEnabled(false); viewModel.saveInventory(request).observe(getViewLifecycleOwner(), resource -> { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index c580905b..5d89e305 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -115,7 +115,7 @@ public class ProductDetailFragment extends Fragment { if (resource == null) return; setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - viewModel.setCategoryList(resource.data.getContent()); + viewModel.setCategoryList(resource.data); } }); } @@ -128,8 +128,8 @@ public class ProductDetailFragment extends Fragment { private void updateCategorySpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, viewModel.getCategoryList().getValue(), - CategoryDTO::getCategoryName, "-- Select Category --", - preselectedCategoryId, CategoryDTO::getCategoryId); + DropdownDTO::getLabel, "-- Select Category --", + preselectedCategoryId, DropdownDTO::getId); } @Override @@ -248,8 +248,8 @@ public class ProductDetailFragment extends Fragment { String desc = binding.etProductDesc.getText().toString().trim(); BigDecimal price = new BigDecimal(binding.etProductPrice.getText().toString().trim()); - CategoryDTO category = viewModel.getCategoryList().getValue().get(binding.spinnerProductCategory.getSelectedItemPosition() - 1); - ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); + DropdownDTO category = viewModel.getCategoryList().getValue().get(binding.spinnerProductCategory.getSelectedItemPosition() - 1); + ProductDTO dto = new ProductDTO(name, category.getId(), desc, price); viewModel.saveProduct(dto).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; 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 df7e4e5d..b1eda73f 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,13 @@ public class PetRepository extends BaseRepository { return executeCall(petApi.getPetDropdowns()); } + /** + * Retrieves available pets for a specific store. + */ + public LiveData>> getAvailablePetsByStore(Long storeId) { + return executeCall(petApi.getAllPets(0, 200, null, "available", null, storeId, null, "petName")); + } + /** * Retrieves a specific pet by its ID from the API. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java index a6d32336..636e1430 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java @@ -3,10 +3,13 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.ProductApi; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.Resource; +import java.util.List; + import javax.inject.Inject; import javax.inject.Singleton; @@ -70,4 +73,18 @@ public class ProductRepository extends BaseRepository { public LiveData> deleteProductImage(Long id) { return executeCall(productApi.deleteProductImage(id)); } + + /** + * Retrieves a list of product dropdowns from the API. + */ + public LiveData>> getProductDropdowns() { + return executeCall(productApi.getProductDropdowns()); + } + + /** + * Retrieves a list of category dropdowns from the API. + */ + public LiveData>> getCategoryDropdowns() { + return executeCall(productApi.getCategoryDropdowns()); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java index 11e8f4fd..45867cd8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java @@ -63,6 +63,31 @@ public class DateTimeUtils { } } + /** + * Checks if a given date is strictly before today (today and future return false). + * format: date = "YYYY-MM-DD" + */ + public static boolean isDateBeforeToday(String date) { + if (date == null || date.isEmpty()) return false; + try { + String[] parts = date.split("-"); + Calendar today = Calendar.getInstance(); + today.set(Calendar.HOUR_OF_DAY, 0); + today.set(Calendar.MINUTE, 0); + today.set(Calendar.SECOND, 0); + today.set(Calendar.MILLISECOND, 0); + + Calendar selected = Calendar.getInstance(); + selected.set(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]) - 1, + Integer.parseInt(parts[2]), 0, 0, 0); + selected.set(Calendar.MILLISECOND, 0); + return selected.before(today); + } catch (Exception e) { + Log.e(TAG, "Error parsing date: " + e.getMessage()); + return false; + } + } + /** * Checks if a given date and time are in the past. * format: date = "YYYY-MM-DD", time = "HH:MM" diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java index a744f92c..c15766f9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java @@ -10,10 +10,13 @@ import com.example.petstoremobile.repositories.AdoptionRepository; import com.example.petstoremobile.repositories.CustomerRepository; import com.example.petstoremobile.repositories.PetRepository; import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.Resource; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Locale; import javax.inject.Inject; @@ -68,17 +71,30 @@ public class AdoptionDetailViewModel extends ViewModel { state.saveButtonText = isEditing ? "Save" : "Add"; state.isAdoptionIdVisible = isEditing; state.isDeleteVisible = isEditing; - state.isPetEnabled = isEditing; - state.isEmployeeEnabled = false; + state.isFeeEnabled = false; // fee is always read-only + if (!isEditing) { + state.isCustomerEnabled = true; + state.isStoreEnabled = true; + state.isPetEnabled = false; // until customer selected + state.isEmployeeEnabled = false; // until store selected + state.isDateEnabled = true; + state.isStatusEnabled = true; + state.availableStatuses = new String[]{"Pending"}; + state.selectedStatus = "Pending"; + } else { + // edit: date-based logic applied after load + state.isCustomerEnabled = false; + state.isStoreEnabled = false; + state.isPetEnabled = false; + state.isEmployeeEnabled = false; + state.isDateEnabled = false; + state.isStatusEnabled = false; + } }); } public void loadInitialFormData(boolean isEditing) { - (isEditing ? petRepository.getPetDropdowns() : petRepository.getAdoptionPets()).observeForever(r -> { - if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { - petList.setValue(r.data); - } - }); + // Pets are loaded dynamically based on store selection; no pre-load needed. customerRepository.getCustomerDropdowns().observeForever(r -> { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { customerList.setValue(r.data); @@ -95,13 +111,7 @@ public class AdoptionDetailViewModel extends ViewModel { List list = customerList.getValue(); Long customerId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; - updateViewState(state -> { - state.selectedCustomerId = customerId; - state.isPetEnabled = customerId != null; - if (customerId == null && !state.isEditing) { - state.selectedPetId = null; - } - }); + updateViewState(state -> state.selectedCustomerId = customerId); } public void onStoreSelected(int position) { @@ -110,19 +120,70 @@ public class AdoptionDetailViewModel extends ViewModel { Long storeId = list.get(position - 1).getId(); updateViewState(state -> { state.selectedStoreId = storeId; - state.isEmployeeEnabled = true; + if (!state.isCancelled && !state.isEditing) { + state.isEmployeeEnabled = true; + state.isPetEnabled = true; + } }); loadEmployeesForStore(storeId); + if (!isEditing()) loadAvailablePetsByStore(storeId); } else { employeeList.setValue(new ArrayList<>()); + petList.setValue(new ArrayList<>()); updateViewState(state -> { state.selectedStoreId = null; state.selectedEmployeeId = null; + state.selectedPetId = null; state.isEmployeeEnabled = false; + state.isPetEnabled = false; }); } } + public void onPetSelected(int position) { + List list = petList.getValue(); + if (position > 0 && list != null && position <= list.size()) { + Long petId = list.get(position - 1).getId(); + updateViewState(s -> s.selectedPetId = petId); + loadPetPrice(petId); + } else { + updateViewState(s -> { + s.selectedPetId = null; + s.adoptionFee = ""; + }); + } + } + + private void loadAvailablePetsByStore(Long storeId) { + petRepository.getAvailablePetsByStore(storeId).observeForever(r -> { + if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { + List dropdowns = new ArrayList<>(); + for (com.example.petstoremobile.dtos.PetDTO pet : r.data.getContent()) { + dropdowns.add(new DropdownDTO(pet.getPetId(), pet.getPetName())); + } + petList.setValue(dropdowns); + } + }); + } + + private void loadPetPrice(Long petId) { + petRepository.getPetById(petId).observeForever(r -> { + if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { + com.example.petstoremobile.dtos.PetDTO pet = r.data; + // In edit mode, add the pet to the list so the spinner can display its name + if (isEditing()) { + List single = new ArrayList<>(); + single.add(new DropdownDTO(pet.getPetId(), pet.getPetName())); + petList.setValue(single); + } + if (pet.getPetPrice() != null) { + String price = String.format(Locale.getDefault(), "%.2f", pet.getPetPrice()); + updateViewState(s -> s.adoptionFee = price); + } + } + }); + } + private void loadEmployeesForStore(Long storeId) { storeRepository.getStoreEmployees(storeId).observeForever(r -> { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { @@ -131,26 +192,99 @@ public class AdoptionDetailViewModel extends ViewModel { }); } + /** + * Called when the date or status changes in the UI. Applies date-based field enabling. + */ + public void onDateChanged(String date, String currentStatus) { + updateViewState(s -> { + if (s.isCancelled) return; + s.availableStatuses = calculateAvailableStatuses(s.isEditing, date); + List available = Arrays.asList(s.availableStatuses); + if (!currentStatus.isEmpty() && available.contains(currentStatus)) { + s.selectedStatus = currentStatus; + } else if (!available.contains(s.selectedStatus) && s.availableStatuses.length > 0) { + s.selectedStatus = s.availableStatuses[0]; + } + + if (!s.isEditing) return; // add mode: field enabling handled separately + + boolean isPast = DateTimeUtils.isDateBeforeToday(date); + if (isPast) { + setAllEditableFieldsEnabled(s, false); + s.isStatusEnabled = true; + } else if (!date.isEmpty()) { + setAllEditableFieldsEnabled(s, false); + s.isEmployeeEnabled = true; + s.isDateEnabled = true; + s.isStatusEnabled = true; + } + }); + } + + private String[] calculateAvailableStatuses(boolean isEditing, String date) { + if (!isEditing) return new String[]{"Pending"}; + if (date == null || date.isEmpty()) return new String[]{}; + if (DateTimeUtils.isDateBeforeToday(date)) return new String[]{"Completed", "Missed"}; + return new String[]{"Pending", "Cancelled"}; + } + + /** Disables all editable fields (fee is always disabled separately). */ + private void setAllEditableFieldsEnabled(ViewState s, boolean enabled) { + s.isCustomerEnabled = enabled; + s.isStoreEnabled = enabled; + s.isPetEnabled = enabled; + s.isEmployeeEnabled = enabled; + s.isDateEnabled = enabled; + // fee never editable + } + public LiveData> loadAdoption() { MutableLiveData> result = new MutableLiveData<>(); adoptionRepository.getAdoptionById(adoptionId).observeForever(resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { AdoptionDTO a = resource.data; + String formattedStatus = DateTimeUtils.formatStatusFromBackend( + a.getAdoptionStatus() != null ? a.getAdoptionStatus() : ""); + String adoptionDate = a.getAdoptionDate() != null ? a.getAdoptionDate() : ""; + updateViewState(state -> { state.selectedPetId = a.getPetId() != null ? a.getPetId() : -1; state.selectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; state.selectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1; state.selectedEmployeeId = a.getEmployeeId() != null ? a.getEmployeeId() : -1; - state.adoptionDate = a.getAdoptionDate() != null ? a.getAdoptionDate() : ""; - state.adoptionFee = a.getAdoptionFee() != null ? a.getAdoptionFee().toString() : ""; - state.adoptionStatus = a.getAdoptionStatus() != null ? a.getAdoptionStatus() : ""; - state.isPetEnabled = state.selectedCustomerId != null && state.selectedCustomerId != -1; - state.isEmployeeEnabled = state.selectedStoreId != null && state.selectedStoreId != -1; + state.adoptionDate = adoptionDate; + state.adoptionFee = a.getAdoptionFee() != null ? a.getAdoptionFee().toPlainString() : ""; + state.selectedStatus = formattedStatus; + state.adoptionStatus = formattedStatus; + + if ("Cancelled".equalsIgnoreCase(formattedStatus)) { + state.isCancelled = true; + state.isCustomerEnabled = false; + state.isStoreEnabled = false; + state.isPetEnabled = false; + state.isEmployeeEnabled = false; + state.isStatusEnabled = false; + state.isDateEnabled = false; + state.isFeeEnabled = false; + state.isSaveVisible = false; + state.availableStatuses = new String[]{"Cancelled"}; + } else { + state.availableStatuses = calculateAvailableStatuses(true, adoptionDate); + boolean isPast = DateTimeUtils.isDateBeforeToday(adoptionDate); + if (isPast) { + setAllEditableFieldsEnabled(state, false); + state.isStatusEnabled = true; + } else if (!adoptionDate.isEmpty()) { + setAllEditableFieldsEnabled(state, false); + state.isEmployeeEnabled = true; + state.isDateEnabled = true; + state.isStatusEnabled = true; + } + } }); - if (a.getSourceStoreId() != null) { - loadEmployeesForStore(a.getSourceStoreId()); - } + if (a.getSourceStoreId() != null) loadEmployeesForStore(a.getSourceStoreId()); + if (a.getPetId() != null) loadPetPrice(a.getPetId()); } result.setValue(resource); }); @@ -173,7 +307,6 @@ public class AdoptionDetailViewModel extends ViewModel { public LiveData> getStoreList() { return storeList; } public LiveData> getEmployeeList() { return employeeList; } - // Kept for backward-compatibility with any remaining direct calls public void setEmployeeList(List list) { employeeList.setValue(list); } private void updateViewState(Action action) { @@ -192,8 +325,17 @@ public class AdoptionDetailViewModel extends ViewModel { public boolean isEditing = false; public boolean isAdoptionIdVisible = false; public boolean isDeleteVisible = false; + public boolean isCancelled = false; public boolean isPetEnabled = false; public boolean isEmployeeEnabled = false; + public boolean isCustomerEnabled = true; + public boolean isStoreEnabled = true; + public boolean isStatusEnabled = true; + public boolean isDateEnabled = true; + public boolean isFeeEnabled = false; // always read-only + public boolean isSaveVisible = true; + public String[] availableStatuses = new String[]{}; + public String selectedStatus = ""; public String modeTitle = "Add Adoption"; public String saveButtonText = "Add"; public Long selectedPetId = null; diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java index e05d261a..18c20e24 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java @@ -237,12 +237,14 @@ public class AppointmentDetailViewModel extends ViewModel { currentServiceId = a.getServiceId(); currentStaffId = a.getEmployeeId(); + String formattedStatus = DateTimeUtils.formatStatusFromBackend(a.getAppointmentStatus()); updateViewState(s -> { s.selectedCustomerId = currentCustomerId; s.selectedStoreId = currentStoreId; s.selectedPetId = currentPetId; s.selectedServiceId = currentServiceId; s.selectedStaffId = currentStaffId; + s.selectedStatus = formattedStatus; }); if (currentCustomerId != null) loadPetsForCustomer(currentCustomerId); @@ -280,6 +282,13 @@ public class AppointmentDetailViewModel extends ViewModel { public void onDateOrTimeChanged(String date, String time, String currentStatus) { updateViewState(s -> { s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus); + // Keep selectedStatus if still valid; prefer explicit currentStatus from UI if valid + java.util.List available = java.util.Arrays.asList(s.availableStatuses); + if (!currentStatus.isEmpty() && available.contains(currentStatus)) { + s.selectedStatus = currentStatus; + } else if (!available.contains(s.selectedStatus) && s.availableStatuses.length > 0) { + s.selectedStatus = s.availableStatuses[0]; + } boolean isPast = DateTimeUtils.isDateTimeInPast(date, time); if (isOriginallyCancel) { @@ -349,6 +358,7 @@ public class AppointmentDetailViewModel extends ViewModel { s.isPetEnabled = false; // until customer selected s.isStaffEnabled = false; // until store selected s.availableStatuses = new String[]{"Booked"}; + s.selectedStatus = "Booked"; } }); } @@ -392,6 +402,7 @@ public class AppointmentDetailViewModel extends ViewModel { public boolean isTimeEnabled = true; public boolean isStatusEnabled = true; public String[] availableStatuses = new String[]{}; + public String selectedStatus = ""; // Selected IDs public Long selectedCustomerId = null; diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java index a76785af..c872ea49 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java @@ -30,7 +30,7 @@ public class InventoryDetailViewModel extends ViewModel { private boolean isEditing = false; private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); - private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); @Inject public InventoryDetailViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository, ProductRepository productRepository) { @@ -55,8 +55,8 @@ public class InventoryDetailViewModel extends ViewModel { return storeRepository.getStoreDropdowns(); } - public LiveData>> loadProducts() { - return productRepository.getAllProducts(null, null, 0, 500, "prodName"); + public LiveData>> loadProducts() { + return productRepository.getProductDropdowns(); } public LiveData> saveInventory(InventoryDTO dto) { @@ -74,6 +74,6 @@ public class InventoryDetailViewModel extends ViewModel { public void setStoreList(List list) { storeList.setValue(list); } public LiveData> getStoreList() { return storeList; } - public void setProductList(List list) { productList.setValue(list); } - public LiveData> getProductList() { return productList; } + public void setProductList(List list) { productList.setValue(list); } + public LiveData> getProductList() { return productList; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java index 54ff65b5..c1ac5fdd 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.repositories.CategoryRepository; @@ -24,7 +25,7 @@ public class ProductDetailViewModel extends ViewModel { private final ProductRepository productRepository; private final CategoryRepository categoryRepository; - private final MutableLiveData> categoryList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> categoryList = new MutableLiveData<>(new ArrayList<>()); private long prodId = -1; private boolean isEditing = false; @@ -47,8 +48,8 @@ public class ProductDetailViewModel extends ViewModel { return isEditing; } - public LiveData>> loadCategories() { - return categoryRepository.getAllCategories(0, 100); + public LiveData>> loadCategories() { + return productRepository.getCategoryDropdowns(); } public LiveData> loadProduct() { @@ -75,11 +76,11 @@ public class ProductDetailViewModel extends ViewModel { return productRepository.deleteProductImage(prodId); } - public void setCategoryList(List list) { + public void setCategoryList(List list) { categoryList.setValue(list); } - public LiveData> getCategoryList() { + public LiveData> getCategoryList() { return categoryList; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java index ecd2d238..6d89ab6f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java @@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; -import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.repositories.CategoryRepository; import com.example.petstoremobile.repositories.ProductRepository; @@ -23,7 +23,7 @@ public class ProductListViewModel extends ViewModel { private final CategoryRepository categoryRepository; private final MutableLiveData> products = new MutableLiveData<>(new ArrayList<>()); - private final MutableLiveData> categories = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> categories = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); @Inject @@ -33,7 +33,7 @@ public class ProductListViewModel extends ViewModel { } public LiveData> getProducts() { return products; } - public LiveData> getCategories() { return categories; } + public LiveData> getCategories() { return categories; } public LiveData getIsLoading() { return isLoading; } public void loadProducts(String query, Long categoryId) { @@ -51,9 +51,9 @@ public class ProductListViewModel extends ViewModel { } public void loadCategories() { - categoryRepository.getAllCategories(0, 100).observeForever(resource -> { + productRepository.getCategoryDropdowns().observeForever(resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - categories.setValue(resource.data.getContent()); + categories.setValue(resource.data); } }); } diff --git a/android/app/src/main/res/layout/fragment_adoption_detail.xml b/android/app/src/main/res/layout/fragment_adoption_detail.xml index a5170113..0bbacddc 100644 --- a/android/app/src/main/res/layout/fragment_adoption_detail.xml +++ b/android/app/src/main/res/layout/fragment_adoption_detail.xml @@ -84,21 +84,6 @@ android:layout_height="wrap_content" android:layout_marginBottom="16dp"/> - - - - - + + + + +