From 6f11f4ebbb3f667915f6495a7daf07311c63e753 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:53:42 -0600 Subject: [PATCH 01/33] making appointment userfrendly part1 andriod --- .../example/petstoremobile/api/PetApi.java | 1 + .../example/petstoremobile/api/StoreApi.java | 7 ++ .../petstoremobile/dtos/DropdownDTO.java | 29 +++++++ .../listfragments/AdoptionFragment.java | 18 ---- .../listfragments/AppointmentFragment.java | 20 ----- .../fragments/listfragments/PetFragment.java | 2 +- .../AdoptionDetailFragment.java | 2 +- .../AppointmentDetailFragment.java | 87 ++++++++++++++++--- .../repositories/PetRepository.java | 4 +- .../repositories/StoreRepository.java | 10 +++ .../example/petstoremobile/utils/UIUtils.java | 9 +- .../viewmodels/PetViewModel.java | 4 +- .../viewmodels/StoreViewModel.java | 10 +++ .../layout/fragment_appointment_detail.xml | 6 +- .../backend/controller/PetController.java | 3 +- .../backend/repository/PetRepository.java | 5 +- .../petshop/backend/service/PetService.java | 4 +- 17 files changed, 151 insertions(+), 70 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/DropdownDTO.java 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 acae1b51..f594b8f9 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 @@ -31,6 +31,7 @@ public interface PetApi { @Query("status") String status, @Query("species") String species, @Query("storeId") Long storeId, + @Query("customerId") Long customerId, @Query("sort") String sort ); diff --git a/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java b/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java index 8fe7f9c1..9819df22 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java @@ -1,10 +1,14 @@ package com.example.petstoremobile.api; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.StoreDTO; +import java.util.List; + import retrofit2.Call; import retrofit2.http.GET; +import retrofit2.http.Path; import retrofit2.http.Query; public interface StoreApi { @@ -13,4 +17,7 @@ public interface StoreApi { Call> getAllStores( @Query("page") int page, @Query("size") int size); + + @GET("api/v1/dropdowns/stores/{storeId}/employees") + Call> getStoreEmployees(@Path("storeId") Long storeId); } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/DropdownDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/DropdownDTO.java new file mode 100644 index 00000000..3174dae5 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/DropdownDTO.java @@ -0,0 +1,29 @@ +package com.example.petstoremobile.dtos; + +public class DropdownDTO { + private Long id; + private String label; + + public DropdownDTO() {} + + public DropdownDTO(Long id, String label) { + this.id = id; + this.label = label; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index 577d58b1..f2a01b5d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -133,24 +133,6 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption, binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption); - - binding.btnToggleFilterAdoption.setOnClickListener(v -> { - boolean isVisible = binding.layoutFilterAdoption.getVisibility() == View.VISIBLE; - binding.layoutFilterAdoption.setVisibility(isVisible ? View.GONE : View.VISIBLE); - - binding.btnToggleFilterAdoption.setImageResource(isVisible ? - android.R.drawable.ic_menu_search : - android.R.drawable.ic_menu_close_clear_cancel); - - if (isVisible) { - binding.etSearchAdoption.setText(""); - binding.spinnerStatusAdoption.setSelection(0); - binding.spinnerStoreAdoption.setSelection(0); - selectedCalendarDay = null; - binding.calendarViewAdoption.clearSelection(); - loadAdoptions(); - } - }); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index 3d4d6cd6..b921d867 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -163,26 +163,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment, binding.spinnerStatus, binding.spinnerStore); - - // Add additional reset logic for elements specific to this fragment - binding.btnToggleFilter.setOnClickListener(v -> { - boolean isVisible = binding.layoutFilter.getVisibility() == View.VISIBLE; - binding.layoutFilter.setVisibility(isVisible ? View.GONE : View.VISIBLE); - - binding.btnToggleFilter.setImageResource(isVisible ? - android.R.drawable.ic_menu_search : - android.R.drawable.ic_menu_close_clear_cancel); - - if (isVisible) { - binding.etSearchAppointment.setText(""); - binding.spinnerStatus.setSelection(0); - binding.spinnerStore.setSelection(0); - binding.btnMyAppointments.setChecked(false); - selectedCalendarDay = null; - binding.calendarView.clearSelection(); - loadAppointmentData(); - } - }); } /** 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 e7e36eae..87a1a54c 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 @@ -215,7 +215,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen if (status.equals("All Statuses")) status = null; if (species.equals("All Species")) species = null; - viewModel.getAllPets(0, 100, query, status, species, storeId, "petName").observe(getViewLifecycleOwner(), resource -> { + viewModel.getAllPets(0, 100, query, status, species, storeId, null, "petName").observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; switch (resource.status) { 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 7098b795..687a6a7c 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 @@ -126,7 +126,7 @@ public class AdoptionDetailFragment extends Fragment { * Loads the list of pets from the API. */ private void loadPets() { - petViewModel.getAllPets(0, 200, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> { + petViewModel.getAllPets(0, 200, null, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { petList = resource.data.getContent(); refreshPetSpinner(); 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 1081462a..285f0d60 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 @@ -47,7 +47,7 @@ public class AppointmentDetailFragment extends Fragment { private List serviceList = new ArrayList<>(); private List customerList = new ArrayList<>(); private List storeList = new ArrayList<>(); - private List staffList = new ArrayList<>(); + private List staffList = new ArrayList<>(); private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; private final Integer[] MINUTES = {0,15,30,45}; @@ -107,6 +107,40 @@ public class AppointmentDetailFragment extends Fragment { hours[i] = String.format("%02d:00", HOURS[i]); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00","15","30","45"}); + + // Listener to load pets based on selected customer + binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (position > 0 && position <= customerList.size()) { + CustomerDTO selectedCustomer = customerList.get(position - 1); + loadPets(selectedCustomer.getCustomerId()); + } else { + petList.clear(); + refreshPetSpinner(); + } + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); + + // Listener to load staff based on selected store + binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (position > 0 && position <= storeList.size()) { + StoreDTO selectedStore = storeList.get(position - 1); + loadStaff(selectedStore.getStoreId()); + } else { + staffList.clear(); + refreshStaffSpinner(); + } + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); } /** @@ -129,18 +163,16 @@ public class AppointmentDetailFragment extends Fragment { * Fetches all required data for spinners from the backend. */ private void loadSpinnersData() { - loadPets(); loadServices(); loadCustomers(); loadStores(); - loadStaff(); } /** - * Loads the list of pets from the ViewModel. + * Loads the list of pets from the ViewModel, filtered by customerId. */ - private void loadPets() { - petViewModel.getAllPets(0, 200, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> { + private void loadPets(Long customerId) { + petViewModel.getAllPets(0, 200, null, null, null, null, customerId, "petName").observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { petList = resource.data.getContent(); refreshPetSpinner(); @@ -222,12 +254,12 @@ public class AppointmentDetailFragment extends Fragment { } /** - * Loads the list of staff from the API. + * Loads the list of staff for a specific store. */ - private void loadStaff() { - userViewModel.getUsers("STAFF", 0, 100).observe(getViewLifecycleOwner(), resource -> { + private void loadStaff(Long storeId) { + storeViewModel.getStoreEmployees(storeId).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - staffList = resource.data.getContent(); + staffList = resource.data; refreshStaffSpinner(); } }); @@ -238,8 +270,8 @@ public class AppointmentDetailFragment extends Fragment { */ private void refreshStaffSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, staffList, - UserDTO::getFullName, "-- Select Staff --", - preselectedStaffId, UserDTO::getId); + DropdownDTO::getLabel, "-- Select Staff --", + preselectedStaffId, DropdownDTO::getId); } /** @@ -254,11 +286,42 @@ public class AppointmentDetailFragment extends Fragment { binding.tvAppointmentId.setText("ID: " + appointmentId); binding.tvAppointmentId.setVisibility(View.VISIBLE); binding.btnDeleteAppointment.setVisibility(View.VISIBLE); + + // Disable and fade fields in edit mode + binding.spinnerCustomer.setEnabled(false); + binding.spinnerStore.setEnabled(false); + binding.spinnerPet.setEnabled(false); + binding.spinnerService.setEnabled(false); + binding.spinnerCustomer.setAlpha(0.5f); + binding.spinnerStore.setAlpha(0.5f); + binding.spinnerPet.setAlpha(0.5f); + binding.spinnerService.setAlpha(0.5f); + + binding.tvLabelCustomer.setAlpha(0.5f); + binding.tvLabelStore.setAlpha(0.5f); + binding.tvLabelPet.setAlpha(0.5f); + binding.tvLabelService.setAlpha(0.5f); + loadAppointmentData(); } else { binding.tvApptMode.setText("Add Appointment"); binding.btnDeleteAppointment.setVisibility(View.GONE); binding.tvAppointmentId.setVisibility(View.GONE); + + // enable fields in add mode + binding.spinnerCustomer.setEnabled(true); + binding.spinnerStore.setEnabled(true); + binding.spinnerPet.setEnabled(true); + binding.spinnerService.setEnabled(true); + binding.spinnerCustomer.setAlpha(1.0f); + binding.spinnerStore.setAlpha(1.0f); + binding.spinnerPet.setAlpha(1.0f); + binding.spinnerService.setAlpha(1.0f); + + binding.tvLabelCustomer.setAlpha(1.0f); + binding.tvLabelStore.setAlpha(1.0f); + binding.tvLabelPet.setAlpha(1.0f); + binding.tvLabelService.setAlpha(1.0f); } } 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 019b5884..d49ec00f 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 @@ -26,8 +26,8 @@ public class PetRepository extends BaseRepository { /** * Retrieves a paginated list of pets from the API with optional filters. */ - public LiveData>> getAllPets(int page, int size, String query, String status, String species, Long storeId, String sort) { - return executeCall(petApi.getAllPets(page, size, query, status, species, storeId, sort)); + public LiveData>> getAllPets(int page, int size, String query, String status, String species, Long storeId, Long customerId, String sort) { + return executeCall(petApi.getAllPets(page, size, query, status, species, storeId, customerId, sort)); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java index 44781a32..9aa4aa1e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java @@ -3,10 +3,13 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.StoreApi; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.Resource; +import java.util.List; + import javax.inject.Inject; import javax.inject.Singleton; @@ -26,4 +29,11 @@ public class StoreRepository extends BaseRepository { public LiveData>> getAllStores(int page, int size) { return executeCall(storeApi.getAllStores(page, size)); } + + /** + * Retrieves a list of employees for a specific store from the dropdowns API. + */ + public LiveData>> getStoreEmployees(Long storeId) { + return executeCall(storeApi.getStoreEmployees(storeId)); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java index 399cabc4..05f6ac18 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java @@ -25,7 +25,7 @@ public class UIUtils { } /** - * Sets up a toggle for a filter layout, including icon changes and field resets. + * Sets up a toggle for a filter layout, including icon changes. */ public static void setupFilterToggle(ImageButton btnToggle, View layoutFilter, EditText etSearch, Spinner... spinners) { btnToggle.setOnClickListener(v -> { @@ -36,13 +36,6 @@ public class UIUtils { btnToggle.setImageResource(isVisible ? android.R.drawable.ic_menu_search : android.R.drawable.ic_menu_close_clear_cancel); - - if (isVisible) { - if (etSearch != null) etSearch.setText(""); - for (Spinner spinner : spinners) { - if (spinner != null) spinner.setSelection(0); - } - } }); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java index c75926a7..6b5f6025 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java @@ -28,8 +28,8 @@ public class PetViewModel extends ViewModel { /** * Fetches a paginated list of pets with filters. */ - public LiveData>> getAllPets(int page, int size, String query, String status, String species, Long storeId, String sort) { - return repository.getAllPets(page, size, query, status, species, storeId, sort); + public LiveData>> getAllPets(int page, int size, String query, String status, String species, Long storeId, Long customerId, String sort) { + return repository.getAllPets(page, size, query, status, species, storeId, customerId, sort); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java index 83f4c3b3..5fac8de4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java @@ -3,11 +3,14 @@ package com.example.petstoremobile.viewmodels; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.utils.Resource; +import java.util.List; + import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; @@ -27,4 +30,11 @@ public class StoreViewModel extends ViewModel { public LiveData>> getAllStores(int page, int size) { return repository.getAllStores(page, size); } + + /** + * Fetches a list of employees for a specific store. + */ + public LiveData>> getStoreEmployees(Long storeId) { + return repository.getStoreEmployees(storeId); + } } diff --git a/android/app/src/main/res/layout/fragment_appointment_detail.xml b/android/app/src/main/res/layout/fragment_appointment_detail.xml index 1f31bc54..f8a87fa0 100644 --- a/android/app/src/main/res/layout/fragment_appointment_detail.xml +++ b/android/app/src/main/res/layout/fragment_appointment_detail.xml @@ -69,6 +69,7 @@ { "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status)) AND " + - "(:storeId IS NULL OR p.store.storeId = :storeId)") - Page searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, @Param("storeId") Long storeId, Pageable pageable); + "(:storeId IS NULL OR p.store.storeId = :storeId) AND " + + "(:customerId IS NULL OR p.owner.id = :customerId)") + Page searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, @Param("storeId") Long storeId, @Param("customerId") Long customerId, Pageable pageable); @Query("SELECT p FROM Pet p WHERE LOWER(p.petStatus) = 'available' AND " + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index e35deb7d..0051d893 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -48,7 +48,7 @@ public class PetService { } @Transactional(readOnly = true) - public Page getAllPets(String query, String species, String status, Long storeId, Pageable pageable) { + public Page getAllPets(String query, String species, String status, Long storeId, Long customerId, Pageable pageable) { String normalizedQuery = normalizeFilter(query); String normalizedSpecies = normalizeFilter(species); String normalizedStatus = normalizeFilter(status); @@ -61,7 +61,7 @@ public class PetService { } pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, storeId, pageable); } else if (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN) { - pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, storeId, pageable); + pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, storeId, customerId, pageable); } else if (viewer.role() == User.Role.CUSTOMER) { if (!isAllowedCustomerStatus(normalizedStatus)) { return new PageImpl<>(java.util.List.of(), pageable, 0); From 2bc0ffd47a8c24c88103999ba6079c68b5c70680 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:34:33 -0600 Subject: [PATCH 02/33] update dropdowns to use backend dropdown endpoints part 1 --- .../petstoremobile/api/CustomerApi.java | 4 ++ .../example/petstoremobile/api/PetApi.java | 9 ++++ .../example/petstoremobile/api/StoreApi.java | 3 ++ .../AdoptionDetailFragment.java | 42 ++++++++-------- .../AppointmentDetailFragment.java | 50 +++++++++---------- .../InventoryDetailFragment.java | 15 +++--- .../detailfragments/PetDetailFragment.java | 29 ++++++----- .../detailfragments/SaleDetailFragment.java | 28 ++++------- .../repositories/CustomerRepository.java | 12 ++++- .../repositories/PetRepository.java | 17 +++++++ .../repositories/StoreRepository.java | 7 +++ .../viewmodels/CustomerViewModel.java | 12 ++++- .../viewmodels/PetViewModel.java | 15 ++++++ .../viewmodels/StoreViewModel.java | 7 +++ 14 files changed, 163 insertions(+), 87 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java index 02700075..855ba5fa 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java @@ -1,6 +1,7 @@ package com.example.petstoremobile.api; import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import java.util.List; @@ -18,4 +19,7 @@ public interface CustomerApi { @GET("api/v1/customers/{customerId}") Call getCustomerById(@Path("customerId") Long customerId); + + @GET("api/v1/dropdowns/customers") + Call> getCustomerDropdowns(); } \ No newline at end of file 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 f594b8f9..24250c4c 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 @@ -1,9 +1,12 @@ package com.example.petstoremobile.api; 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 java.util.List; + import okhttp3.MultipartBody; import retrofit2.Call; import retrofit2.http.Body; @@ -35,6 +38,12 @@ public interface PetApi { @Query("sort") String sort ); + @GET("api/v1/dropdowns/customers/{customerId}/pets") + Call> getCustomerPets(@Path("customerId") Long customerId); + + @GET("api/v1/dropdowns/adoption-pets") + Call> getAdoptionPets(); + // 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/api/StoreApi.java b/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java index 9819df22..f71b92b6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java @@ -18,6 +18,9 @@ public interface StoreApi { @Query("page") int page, @Query("size") int size); + @GET("api/v1/dropdowns/stores") + Call> getStoreDropdowns(); + @GET("api/v1/dropdowns/stores/{storeId}/employees") Call> getStoreEmployees(@Path("storeId") Long storeId); } 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 687a6a7c..0beeaa7e 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 @@ -41,9 +41,9 @@ public class AdoptionDetailFragment extends Fragment { private long preselectedStoreId = -1; private long preselectedEmployeeId = -1; - private List petList = new ArrayList<>(); - private List customerList = new ArrayList<>(); - private List storeList = new ArrayList<>(); + private List petList = new ArrayList<>(); + private List customerList = new ArrayList<>(); + private List storeList = new ArrayList<>(); private List employeeList = new ArrayList<>(); private final String[] STATUSES = {"Pending", "Completed", "Cancelled"}; @@ -126,9 +126,9 @@ public class AdoptionDetailFragment extends Fragment { * Loads the list of pets from the API. */ private void loadPets() { - petViewModel.getAllPets(0, 200, null, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> { + petViewModel.getAdoptionPets().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - petList = resource.data.getContent(); + petList = resource.data; refreshPetSpinner(); } }); @@ -139,17 +139,17 @@ public class AdoptionDetailFragment extends Fragment { */ private void refreshPetSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList, - PetDTO::getPetName, "-- Select Pet --", - preselectedPetId, PetDTO::getPetId); + DropdownDTO::getLabel, "-- Select Pet --", + preselectedPetId, DropdownDTO::getId); } /** * Loads the list of customers from the API. */ private void loadCustomers() { - customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { + customerViewModel.getCustomerDropdowns().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data.getContent(); + customerList = resource.data; refreshCustomerSpinner(); } }); @@ -160,18 +160,18 @@ public class AdoptionDetailFragment extends Fragment { */ private void refreshCustomerSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList, - item -> item.getFirstName() + " " + item.getLastName(), + DropdownDTO::getLabel, "-- Select Customer --", - preselectedCustomerId, CustomerDTO::getCustomerId); + preselectedCustomerId, DropdownDTO::getId); } /** * Loads the list of stores from the API. */ private void loadStores() { - storeViewModel.getAllStores(0, 200).observe(getViewLifecycleOwner(), resource -> { + storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); + storeList = resource.data; refreshStoreSpinner(); } }); @@ -182,8 +182,8 @@ public class AdoptionDetailFragment extends Fragment { */ private void refreshStoreSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", - preselectedStoreId, StoreDTO::getStoreId); + DropdownDTO::getLabel, "-- Select Store --", + preselectedStoreId, DropdownDTO::getId); } /** @@ -283,9 +283,9 @@ public class AdoptionDetailFragment extends Fragment { } } - CustomerDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); - PetDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); - StoreDTO store = storeList.get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); + DropdownDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); + DropdownDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); + DropdownDTO store = storeList.get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); Long employeeId = null; if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0) { @@ -295,10 +295,10 @@ public class AdoptionDetailFragment extends Fragment { String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; AdoptionDTO dto = new AdoptionDTO( - pet.getPetId(), - customer.getCustomerId(), + pet.getId(), + customer.getId(), employeeId, - store.getStoreId(), + store.getId(), date, 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 285f0d60..1d25207a 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 @@ -43,10 +43,10 @@ public class AppointmentDetailFragment extends Fragment { private long preselectedStoreId = -1; private long preselectedStaffId = -1; - private List petList = new ArrayList<>(); + private List petList = new ArrayList<>(); private List serviceList = new ArrayList<>(); - private List customerList = new ArrayList<>(); - private List storeList = new ArrayList<>(); + private List customerList = new ArrayList<>(); + private List storeList = new ArrayList<>(); private List staffList = new ArrayList<>(); private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; @@ -113,8 +113,8 @@ public class AppointmentDetailFragment extends Fragment { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { if (position > 0 && position <= customerList.size()) { - CustomerDTO selectedCustomer = customerList.get(position - 1); - loadPets(selectedCustomer.getCustomerId()); + DropdownDTO selectedCustomer = customerList.get(position - 1); + loadPets(selectedCustomer.getId()); } else { petList.clear(); refreshPetSpinner(); @@ -130,8 +130,8 @@ public class AppointmentDetailFragment extends Fragment { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { if (position > 0 && position <= storeList.size()) { - StoreDTO selectedStore = storeList.get(position - 1); - loadStaff(selectedStore.getStoreId()); + DropdownDTO selectedStore = storeList.get(position - 1); + loadStaff(selectedStore.getId()); } else { staffList.clear(); refreshStaffSpinner(); @@ -172,9 +172,9 @@ public class AppointmentDetailFragment extends Fragment { * Loads the list of pets from the ViewModel, filtered by customerId. */ private void loadPets(Long customerId) { - petViewModel.getAllPets(0, 200, null, null, null, null, customerId, "petName").observe(getViewLifecycleOwner(), resource -> { + petViewModel.getCustomerPets(customerId).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - petList = resource.data.getContent(); + petList = resource.data; refreshPetSpinner(); } }); @@ -185,8 +185,8 @@ public class AppointmentDetailFragment extends Fragment { */ private void refreshPetSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList, - PetDTO::getPetName, "-- Select Pet --", - preselectedPetId, PetDTO::getPetId); + DropdownDTO::getLabel, "-- Select Pet --", + preselectedPetId, DropdownDTO::getId); } /** @@ -214,9 +214,9 @@ public class AppointmentDetailFragment extends Fragment { * Loads the list of customers from the API. */ private void loadCustomers() { - customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { + customerViewModel.getCustomerDropdowns().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data.getContent(); + customerList = resource.data; refreshCustomerSpinner(); } }); @@ -227,18 +227,18 @@ public class AppointmentDetailFragment extends Fragment { */ private void refreshCustomerSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, customerList, - item -> item.getFirstName() + " " + item.getLastName(), + DropdownDTO::getLabel, "-- Select Customer --", - preselectedCustomerId, CustomerDTO::getCustomerId); + preselectedCustomerId, DropdownDTO::getId); } /** * Loads the list of stores from the API. */ private void loadStores() { - storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> { + storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); + storeList = resource.data; refreshStoreSpinner(); } }); @@ -249,8 +249,8 @@ public class AppointmentDetailFragment extends Fragment { */ private void refreshStoreSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", - preselectedStoreId, StoreDTO::getStoreId); + DropdownDTO::getLabel, "-- Select Store --", + preselectedStoreId, DropdownDTO::getId); } /** @@ -395,9 +395,9 @@ public class AppointmentDetailFragment extends Fragment { Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; } - CustomerDTO customer = customerList.get(binding.spinnerCustomer.getSelectedItemPosition() - 1); - StoreDTO store = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1); - PetDTO pet = petList.get(binding.spinnerPet.getSelectedItemPosition() - 1); + DropdownDTO customer = customerList.get(binding.spinnerCustomer.getSelectedItemPosition() - 1); + DropdownDTO store = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1); + DropdownDTO pet = petList.get(binding.spinnerPet.getSelectedItemPosition() - 1); ServiceDTO service = serviceList.get(binding.spinnerService.getSelectedItemPosition() - 1); Long employeeId = null; @@ -440,14 +440,14 @@ public class AppointmentDetailFragment extends Fragment { // Build DTO with all required IDs AppointmentDTO dto = new AppointmentDTO( - customer.getCustomerId(), - store.getStoreId(), + customer.getId(), + store.getId(), service.getServiceId(), employeeId, date, time, status, - pet.getPetId() + pet.getId() ); androidx.lifecycle.Observer> observer = resource -> { 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 7a729b94..640b99b9 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 @@ -14,6 +14,7 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.StoreDTO; @@ -46,7 +47,7 @@ public class InventoryDetailFragment extends Fragment { private long preselectedStoreId = -1; private long preselectedProductId = -1; - private List storeList = new ArrayList<>(); + private List storeList = new ArrayList<>(); private List productList = new ArrayList<>(); /** @@ -103,9 +104,9 @@ public class InventoryDetailFragment extends Fragment { * Loads the list of stores for the spinner. */ private void loadStores() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { + storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); + storeList = resource.data; refreshStoreSpinner(); } }); @@ -113,8 +114,8 @@ public class InventoryDetailFragment extends Fragment { private void refreshStoreSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", - preselectedStoreId, StoreDTO::getStoreId); + DropdownDTO::getLabel, "-- Select Store --", + preselectedStoreId, DropdownDTO::getId); } /** @@ -199,10 +200,10 @@ public class InventoryDetailFragment extends Fragment { } int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim()); - StoreDTO store = storeList.get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); + DropdownDTO store = storeList.get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); ProductDTO product = productList.get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); - InventoryDTO request = new InventoryDTO(product.getProdId(), store.getStoreId(), quantity); + InventoryDTO request = new InventoryDTO(product.getProdId(), store.getId(), quantity); setButtonsEnabled(false); if (isEditing) { 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 ea08d16d..e45a93b2 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 @@ -18,9 +18,8 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentPetDetailBinding; -import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PetDTO; -import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; @@ -49,8 +48,8 @@ public class PetDetailFragment extends Fragment { private PetViewModel viewModel; private CustomerViewModel customerViewModel; private StoreViewModel storeViewModel; - private List customerList = new ArrayList<>(); - private List storeList = new ArrayList<>(); + private List customerList = new ArrayList<>(); + private List storeList = new ArrayList<>(); private Long selectedCustomerId = null; private Long selectedStoreId = null; @@ -113,14 +112,14 @@ public class PetDetailFragment extends Fragment { Long customerId = null; int customerPos = binding.spinnerCustomer.getSelectedItemPosition(); if (customerPos > 0) { // 0 means no customer for pet - customerId = customerList.get(customerPos - 1).getCustomerId(); + customerId = customerList.get(customerPos - 1).getId(); } // Get selected store Long storeId = null; int storePos = binding.spinnerStore.getSelectedItemPosition(); if (storePos > 0) { - storeId = storeList.get(storePos - 1).getStoreId(); + storeId = storeList.get(storePos - 1).getId(); } // Validation: If status is Available, a store must be selected @@ -277,9 +276,9 @@ public class PetDetailFragment extends Fragment { * Fetches the list of customers and populates the spinner. */ private void loadCustomers() { - customerViewModel.getAllCustomers(0, 1000).observe(getViewLifecycleOwner(), resource -> { + customerViewModel.getCustomerDropdowns().observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data.getContent(); + customerList = resource.data; updateCustomerSpinnerSelection(); } }); @@ -289,9 +288,9 @@ public class PetDetailFragment extends Fragment { * Fetches the list of stores and populates the spinner. */ private void loadStores() { - storeViewModel.getAllStores(0, 1000).observe(getViewLifecycleOwner(), resource -> { + storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); + storeList = resource.data; updateStoreSpinnerSelection(); } }); @@ -305,10 +304,10 @@ public class PetDetailFragment extends Fragment { requireContext(), binding.spinnerCustomer, customerList, - CustomerDTO::getFullName, + DropdownDTO::getLabel, "No Owner", selectedCustomerId, - CustomerDTO::getCustomerId + DropdownDTO::getId ); } @@ -320,10 +319,10 @@ public class PetDetailFragment extends Fragment { requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, + DropdownDTO::getLabel, "None", selectedStoreId, - StoreDTO::getStoreId + DropdownDTO::getId ); } @@ -379,4 +378,4 @@ public class PetDetailFragment extends Fragment { ((TextView) selectedView).setError(null); } } -} +} \ No newline at end of file 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 40c7896b..0be0120c 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 @@ -31,8 +31,8 @@ public class SaleDetailFragment extends Fragment { private boolean viewOnly = false; private long saleId = -1; - private List storeList = new ArrayList<>(); - private List customerList = new ArrayList<>(); + private List storeList = new ArrayList<>(); + private List customerList = new ArrayList<>(); private List productList = new ArrayList<>(); private List cartItems = new ArrayList<>(); @@ -104,30 +104,24 @@ public class SaleDetailFragment extends Fragment { } private void loadStores() { - storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> { + storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); + storeList = resource.data; if (binding != null) { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", -1L, StoreDTO::getStoreId); - } - } else if (storeList.isEmpty()) { - storeList = Collections.singletonList(new StoreDTO(1L, "Downtown Branch")); - if (binding != null) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", -1L, StoreDTO::getStoreId); + DropdownDTO::getLabel, "-- Select Store --", -1L, DropdownDTO::getId); } } }); } private void loadCustomers() { - customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { + customerViewModel.getCustomerDropdowns().observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data.getContent(); + customerList = resource.data; if (binding != null) { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleCustomer, customerList, - CustomerDTO::getFullName, "-- No Customer --", -1L, CustomerDTO::getCustomerId); + DropdownDTO::getLabel, "-- No Customer --", -1L, DropdownDTO::getId); } } }); @@ -275,18 +269,18 @@ public class SaleDetailFragment extends Fragment { return; } - StoreDTO store = storeList.get(binding.spinnerSaleStore.getSelectedItemPosition() - 1); + DropdownDTO store = storeList.get(binding.spinnerSaleStore.getSelectedItemPosition() - 1); String payment = PAYMENT_METHODS[binding.spinnerPaymentMethod.getSelectedItemPosition()]; // Optional customer Long customerId = null; if (binding.spinnerSaleCustomer.getSelectedItemPosition() > 0) { customerId = customerList.get(binding.spinnerSaleCustomer.getSelectedItemPosition() - 1) - .getCustomerId(); + .getId(); } SaleDTO dto = new SaleDTO( - store.getStoreId(), + store.getId(), payment, cartItems, false, diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java index 4006ae69..9fa9d2e6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java @@ -4,9 +4,12 @@ import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.CustomerApi; import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; +import java.util.List; + import javax.inject.Inject; import javax.inject.Singleton; @@ -33,4 +36,11 @@ public class CustomerRepository extends BaseRepository { public LiveData> getCustomerById(Long id) { return executeCall(customerApi.getCustomerById(id)); } -} + + /** + * Retrieves a list of customer dropdowns from the API. + */ + public LiveData>> getCustomerDropdowns() { + return executeCall(customerApi.getCustomerDropdowns()); + } +} \ No newline at end of file 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 d49ec00f..623a4daa 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 @@ -4,10 +4,13 @@ import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.PetApi; 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.utils.Resource; +import java.util.List; + import javax.inject.Inject; import javax.inject.Singleton; @@ -30,6 +33,20 @@ public class PetRepository extends BaseRepository { return executeCall(petApi.getAllPets(page, size, query, status, species, storeId, customerId, sort)); } + /** + * Retrieves a list of pets for a specific customer from the dropdowns API. + */ + public LiveData>> getCustomerPets(Long customerId) { + return executeCall(petApi.getCustomerPets(customerId)); + } + + /** + * Retrieves a list of pets available for adoption from the dropdowns API. + */ + public LiveData>> getAdoptionPets() { + return executeCall(petApi.getAdoptionPets()); + } + /** * Retrieves a specific pet by its ID from the API. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java index 9aa4aa1e..0df93ab1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java @@ -30,6 +30,13 @@ public class StoreRepository extends BaseRepository { return executeCall(storeApi.getAllStores(page, size)); } + /** + * Retrieves a list of store dropdowns from the API. + */ + public LiveData>> getStoreDropdowns() { + return executeCall(storeApi.getStoreDropdowns()); + } + /** * Retrieves a list of employees for a specific store from the dropdowns API. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java index 5ad7cc76..d916ff75 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java @@ -4,10 +4,13 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.repositories.CustomerRepository; import com.example.petstoremobile.utils.Resource; +import java.util.List; + import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; @@ -34,4 +37,11 @@ public class CustomerViewModel extends ViewModel { public LiveData> getCustomerById(Long id) { return repository.getCustomerById(id); } -} + + /** + * Retrieves a list of customer dropdowns from the repository. + */ + public LiveData>> getCustomerDropdowns() { + return repository.getCustomerDropdowns(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java index 6b5f6025..76770392 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData; 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.repositories.PetRepository; @@ -32,6 +33,20 @@ public class PetViewModel extends ViewModel { return repository.getAllPets(page, size, query, status, species, storeId, customerId, sort); } + /** + * Retrieves a list of pets for a specific customer from the repository. + */ + public LiveData>> getCustomerPets(Long customerId) { + return repository.getCustomerPets(customerId); + } + + /** + * Retrieves a list of pets available for adoption from the repository. + */ + public LiveData>> getAdoptionPets() { + return repository.getAdoptionPets(); + } + /** * Retrieves a single pet by its ID. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java index 5fac8de4..0388c646 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java @@ -31,6 +31,13 @@ public class StoreViewModel extends ViewModel { return repository.getAllStores(page, size); } + /** + * Fetches a list of store dropdowns from the repository. + */ + public LiveData>> getStoreDropdowns() { + return repository.getStoreDropdowns(); + } + /** * Fetches a list of employees for a specific store. */ From 271314f99064787a351026a64395df5be816f28b Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:10:18 -0600 Subject: [PATCH 03/33] fixing dropdowns --- .../AdoptionDetailFragment.java | 69 ++++++++++++++++--- .../AppointmentDetailFragment.java | 21 ++++-- .../controller/DropdownController.java | 11 +++ 3 files changed, 87 insertions(+), 14 deletions(-) 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 0beeaa7e..61dfbcce 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 @@ -44,7 +44,7 @@ public class AdoptionDetailFragment extends Fragment { private List petList = new ArrayList<>(); private List customerList = new ArrayList<>(); private List storeList = new ArrayList<>(); - private List employeeList = new ArrayList<>(); + private List employeeList = new ArrayList<>(); private final String[] STATUSES = {"Pending", "Completed", "Cancelled"}; @@ -95,6 +95,46 @@ public class AdoptionDetailFragment extends Fragment { */ private void setupSpinners() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES); + + // Pet spinner disabled by default until customer is selected + binding.spinnerAdoptionPet.setEnabled(false); + binding.spinnerAdoptionPet.setAlpha(0.5f); + + // Listener to enable pet spinner based on customer selection + binding.spinnerAdoptionCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (position > 0) { + binding.spinnerAdoptionPet.setEnabled(true); + binding.spinnerAdoptionPet.setAlpha(1.0f); + } else { + if (!isEditing) { + binding.spinnerAdoptionPet.setSelection(0); + binding.spinnerAdoptionPet.setEnabled(false); + binding.spinnerAdoptionPet.setAlpha(0.5f); + } + } + } + @Override + public void onNothingSelected(AdapterView parent) {} + }); + + // Listener to load employees based on selected store + binding.spinnerAdoptionStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (position > 0 && position <= storeList.size()) { + DropdownDTO selectedStore = storeList.get(position - 1); + loadEmployees(selectedStore.getId()); + } else { + employeeList.clear(); + refreshEmployeeSpinner(); + } + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); } /** @@ -119,7 +159,6 @@ public class AdoptionDetailFragment extends Fragment { loadPets(); loadCustomers(); loadStores(); - loadEmployees(); } /** @@ -187,12 +226,12 @@ public class AdoptionDetailFragment extends Fragment { } /** - * Loads the list of employees from the API. + * Loads the list of employees for a specific store. */ - private void loadEmployees() { - userViewModel.getUsers("STAFF", 0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - employeeList = resource.data.getContent(); + private void loadEmployees(Long storeId) { + storeViewModel.getStoreEmployees(storeId).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + employeeList = resource.data; refreshEmployeeSpinner(); } }); @@ -203,8 +242,8 @@ public class AdoptionDetailFragment extends Fragment { */ private void refreshEmployeeSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, employeeList, - UserDTO::getFullName, "-- Select Staff --", - preselectedEmployeeId, UserDTO::getId); + DropdownDTO::getLabel, "-- Select Staff --", + preselectedEmployeeId, DropdownDTO::getId); } /** @@ -221,9 +260,14 @@ public class AdoptionDetailFragment extends Fragment { binding.btnDeleteAdoption.setVisibility(View.VISIBLE); loadAdoptionData(); } else { + isEditing = false; binding.tvAdoptionMode.setText("Add Adoption"); binding.btnDeleteAdoption.setVisibility(View.GONE); binding.tvAdoptionId.setVisibility(View.GONE); + + // Explicitly disable in add mode + binding.spinnerAdoptionPet.setEnabled(false); + binding.spinnerAdoptionPet.setAlpha(0.5f); } } @@ -247,7 +291,12 @@ public class AdoptionDetailFragment extends Fragment { refreshPetSpinner(); refreshCustomerSpinner(); refreshStoreSpinner(); - refreshEmployeeSpinner(); + + // In edit mode, if a customer is already set, ensure pet spinner is enabled + if (preselectedCustomerId != -1) { + binding.spinnerAdoptionPet.setEnabled(true); + binding.spinnerAdoptionPet.setAlpha(1.0f); + } } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show(); } 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 1d25207a..86da3ae7 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 @@ -108,6 +108,10 @@ public class AppointmentDetailFragment extends Fragment { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00","15","30","45"}); + // Pet spinner disabled by default + binding.spinnerPet.setEnabled(false); + binding.spinnerPet.setAlpha(0.5f); + // Listener to load pets based on selected customer binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override @@ -115,9 +119,18 @@ public class AppointmentDetailFragment extends Fragment { if (position > 0 && position <= customerList.size()) { DropdownDTO selectedCustomer = customerList.get(position - 1); loadPets(selectedCustomer.getId()); + if (!isEditing) { + binding.spinnerPet.setEnabled(true); + binding.spinnerPet.setAlpha(1.0f); + } } else { petList.clear(); refreshPetSpinner(); + if (!isEditing) { + binding.spinnerPet.setSelection(0); + binding.spinnerPet.setEnabled(false); + binding.spinnerPet.setAlpha(0.5f); + } } } @@ -280,6 +293,7 @@ public class AppointmentDetailFragment extends Fragment { private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("appointmentId")) { + //edit mode isEditing = true; appointmentId = a.getLong("appointmentId"); binding.tvApptMode.setText("Edit Appointment"); @@ -287,7 +301,6 @@ public class AppointmentDetailFragment extends Fragment { binding.tvAppointmentId.setVisibility(View.VISIBLE); binding.btnDeleteAppointment.setVisibility(View.VISIBLE); - // Disable and fade fields in edit mode binding.spinnerCustomer.setEnabled(false); binding.spinnerStore.setEnabled(false); binding.spinnerPet.setEnabled(false); @@ -304,18 +317,18 @@ public class AppointmentDetailFragment extends Fragment { loadAppointmentData(); } else { + //add mode binding.tvApptMode.setText("Add Appointment"); binding.btnDeleteAppointment.setVisibility(View.GONE); binding.tvAppointmentId.setVisibility(View.GONE); - // enable fields in add mode binding.spinnerCustomer.setEnabled(true); binding.spinnerStore.setEnabled(true); - binding.spinnerPet.setEnabled(true); + binding.spinnerPet.setEnabled(false); + binding.spinnerPet.setAlpha(0.5f); binding.spinnerService.setEnabled(true); binding.spinnerCustomer.setAlpha(1.0f); binding.spinnerStore.setAlpha(1.0f); - binding.spinnerPet.setAlpha(1.0f); binding.spinnerService.setAlpha(1.0f); binding.tvLabelCustomer.setAlpha(1.0f); diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index acf77b52..261a56a3 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -44,6 +45,7 @@ public class DropdownController { return ResponseEntity.ok( petRepository.findAll().stream() .map(p -> new DropdownOption(p.getPetId(), p.getPetName())) + .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); } @@ -64,6 +66,7 @@ public class DropdownController { return ResponseEntity.ok( userRepository.findByRoleAndActiveTrue(User.Role.CUSTOMER).stream() .map(u -> new DropdownOption(u.getId(), u.getFirstName() + " " + u.getLastName())) + .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); } @@ -74,6 +77,7 @@ public class DropdownController { return ResponseEntity.ok( userRepository.findByRoleAndActiveTrue(User.Role.CUSTOMER).stream() .map(u -> new DropdownOption(u.getId(), u.getFirstName() + " " + u.getLastName())) + .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); } @@ -83,6 +87,7 @@ public class DropdownController { return ResponseEntity.ok( serviceRepository.findAll().stream() .map(s -> new DropdownOption(s.getServiceId(), s.getServiceName())) + .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); } @@ -92,6 +97,7 @@ public class DropdownController { return ResponseEntity.ok( productRepository.findAll().stream() .map(p -> new DropdownOption(p.getProdId(), p.getProdName())) + .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); } @@ -101,6 +107,7 @@ public class DropdownController { return ResponseEntity.ok( categoryRepository.findAll().stream() .map(c -> new DropdownOption(c.getCategoryId(), c.getCategoryName())) + .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); } @@ -111,6 +118,7 @@ public class DropdownController { categoryRepository.findAll().stream() .filter(c -> "product".equalsIgnoreCase(c.getCategoryType())) .map(c -> new DropdownOption(c.getCategoryId(), c.getCategoryName())) + .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); } @@ -133,6 +141,7 @@ public class DropdownController { return ResponseEntity.ok( storeRepository.findAll().stream() .map(s -> new DropdownOption(s.getStoreId(), s.getStoreName())) + .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); } @@ -149,6 +158,7 @@ public class DropdownController { return ResponseEntity.ok( employees.stream() .map(u -> new DropdownOption(u.getId(), u.getFirstName() + " " + u.getLastName())) + .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); } @@ -169,6 +179,7 @@ public class DropdownController { return ResponseEntity.ok( supplierRepository.findAll().stream() .map(s -> new DropdownOption(s.getSupId(), s.getSupCompany())) + .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); } From 5f9d7a848c4321f28a9fc0302799f3ecbf3a2b45 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:16:18 -0600 Subject: [PATCH 04/33] updated backend so booked appointment automatically changes to completed --- .../AppointmentDetailFragment.java | 90 +++++++++++++++++++ .../layout/fragment_appointment_detail.xml | 3 + .../petshop/backend/BackendApplication.java | 2 + .../config/ApplicationStartupListener.java | 22 +++++ .../repository/AppointmentRepository.java | 3 + .../backend/service/AppointmentService.java | 15 ++++ 6 files changed, 135 insertions(+) create mode 100644 backend/src/main/java/com/petshop/backend/config/ApplicationStartupListener.java 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 86da3ae7..169e0532 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 @@ -37,6 +37,7 @@ public class AppointmentDetailFragment extends Fragment { private long appointmentId = -1; private boolean isEditing = false; + private boolean isPastAppointment = false; private long preselectedPetId = -1; private long preselectedServiceId = -1; private long preselectedCustomerId = -1; @@ -161,6 +162,7 @@ public class AppointmentDetailFragment extends Fragment { */ private void setupDatePicker() { binding.etAppointmentDate.setOnClickListener(v -> { + if (isPastAppointment) return; Calendar c = Calendar.getInstance(); DatePickerDialog d = new DatePickerDialog(requireContext(), (dp,y,m,d1) -> binding.etAppointmentDate.setText( @@ -376,6 +378,8 @@ public class AppointmentDetailFragment extends Fragment { SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, formattedStatus); } + checkIfPastAndDisable(a.getAppointmentDate(), time); + refreshPetSpinner(); refreshServiceSpinner(); refreshCustomerSpinner(); @@ -387,6 +391,92 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Checks if the appointment is in the past and disables fields. + */ + private void checkIfPastAndDisable(String date, String time) { + if (date == null || time == null) return; + try { + Calendar selected = Calendar.getInstance(); + String[] dateParts = date.split("-"); + String[] timeParts = time.split(":"); + selected.set( + Integer.parseInt(dateParts[0]), + Integer.parseInt(dateParts[1]) - 1, + Integer.parseInt(dateParts[2]), + Integer.parseInt(timeParts[0]), + Integer.parseInt(timeParts[1]), + 0 + ); + + Object selectedItem = binding.spinnerAppointmentStatus.getSelectedItem(); + String currentStatus = selectedItem != null ? selectedItem.toString() : ""; + + // If the appointment is already Cancelled, disable all fields + if ("Cancelled".equalsIgnoreCase(currentStatus)) { + isPastAppointment = true; + disableAllExceptStatus(); + binding.spinnerAppointmentStatus.setEnabled(false); + binding.spinnerAppointmentStatus.setAlpha(0.5f); + binding.btnSaveAppointment.setVisibility(View.GONE); + return; + } + + // If the appointment date/time is in the past + if (selected.before(Calendar.getInstance())) { + isPastAppointment = true; + disableAllExceptStatus(); + + // Make status spinner only have Completed or Missed + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, + new String[]{"Completed", "Missed"}); + + // Restore selection if it's already one of the valid options + if (currentStatus.equals("Completed") || currentStatus.equals("Missed")) { + SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, currentStatus); + } + } + } catch (Exception e) { + Log.e("APPT_DETAIL", "Error parsing date/time for past check: " + e.getMessage()); + } + } + + /** + * Disables all input fields except the status spinner + */ + private void disableAllExceptStatus() { + binding.spinnerCustomer.setEnabled(false); + binding.spinnerStore.setEnabled(false); + binding.spinnerPet.setEnabled(false); + binding.spinnerService.setEnabled(false); + binding.spinnerStaff.setEnabled(false); + binding.etAppointmentDate.setEnabled(false); + binding.spinnerHour.setEnabled(false); + binding.spinnerMinute.setEnabled(false); + + float disabledAlpha = 0.5f; + binding.spinnerCustomer.setAlpha(disabledAlpha); + binding.spinnerStore.setAlpha(disabledAlpha); + binding.spinnerPet.setAlpha(disabledAlpha); + binding.spinnerService.setAlpha(disabledAlpha); + binding.spinnerStaff.setAlpha(disabledAlpha); + binding.etAppointmentDate.setAlpha(disabledAlpha); + binding.spinnerHour.setAlpha(disabledAlpha); + binding.spinnerMinute.setAlpha(disabledAlpha); + + binding.tvLabelCustomer.setAlpha(disabledAlpha); + binding.tvLabelStore.setAlpha(disabledAlpha); + binding.tvLabelPet.setAlpha(disabledAlpha); + binding.tvLabelService.setAlpha(disabledAlpha); + binding.tvLabelStaff.setAlpha(disabledAlpha); + binding.tvLabelDate.setAlpha(disabledAlpha); + binding.tvLabelTime.setAlpha(disabledAlpha); + + // Keep status enabled + binding.spinnerAppointmentStatus.setEnabled(true); + binding.spinnerAppointmentStatus.setAlpha(1.0f); + } + /** * Validates input and saves the appointment to the backend. */ diff --git a/android/app/src/main/res/layout/fragment_appointment_detail.xml b/android/app/src/main/res/layout/fragment_appointment_detail.xml index f8a87fa0..4248311c 100644 --- a/android/app/src/main/res/layout/fragment_appointment_detail.xml +++ b/android/app/src/main/res/layout/fragment_appointment_detail.xml @@ -136,6 +136,7 @@ { + + private final AppointmentService appointmentService; + + public ApplicationStartupListener(AppointmentService appointmentService) { + this.appointmentService = appointmentService; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + //update booked appointments to complete on startup + appointmentService.updatePastAppointmentsStatus(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index dc7d40ef..9da6efdd 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -47,4 +47,7 @@ public interface AppointmentRepository extends JpaRepository @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.id IN :employeeIds AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByEmployeeIdInAndAppointmentDate(@Param("employeeIds") List employeeIds, @Param("date") LocalDate date); + + @Query("SELECT a FROM Appointment a WHERE (a.appointmentDate < :currentDate OR (a.appointmentDate = :currentDate AND a.appointmentTime < :currentTime)) AND LOWER(a.appointmentStatus) = 'booked'") + List findPastBookedAppointments(@Param("currentDate") LocalDate currentDate, @Param("currentTime") LocalTime currentTime); } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 1a32d2aa..26e0a512 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -16,6 +16,7 @@ import com.petshop.backend.repository.UserRepository; import com.petshop.backend.util.AuthenticationHelper; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -209,6 +210,20 @@ public class AppointmentService { return availableSlots; } + //Update booked status to completed at every midnight + @Scheduled(cron = "0 0 0 * * ?") + @Transactional + public void updatePastAppointmentsStatus() { + LocalDate currentDate = LocalDate.now(); + LocalTime currentTime = LocalTime.now(); + List pastBookedAppointments = appointmentRepository.findPastBookedAppointments(currentDate, currentTime); + + for (Appointment appointment : pastBookedAppointments) { + appointment.setAppointmentStatus("COMPLETED"); + appointmentRepository.save(appointment); + } + } + private String normalizeFilter(String value) { if (value == null) { return null; From b6bee250df78f74f0ddd5fb89d446af346c40fce Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:57:04 -0600 Subject: [PATCH 05/33] helper class added to enable and disable fields --- .../AdoptionDetailFragment.java | 16 ++-- .../AppointmentDetailFragment.java | 91 ++++++------------- .../detailfragments/PetDetailFragment.java | 25 ++--- .../detailfragments/SaleDetailFragment.java | 8 +- .../example/petstoremobile/utils/UIUtils.java | 24 +++++ 5 files changed, 71 insertions(+), 93 deletions(-) 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 61dfbcce..ece652fd 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 @@ -15,6 +15,7 @@ import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.AdoptionViewModel; import com.example.petstoremobile.viewmodels.CustomerViewModel; import com.example.petstoremobile.viewmodels.PetViewModel; @@ -97,21 +98,18 @@ public class AdoptionDetailFragment extends Fragment { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES); // Pet spinner disabled by default until customer is selected - binding.spinnerAdoptionPet.setEnabled(false); - binding.spinnerAdoptionPet.setAlpha(0.5f); + UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); // Listener to enable pet spinner based on customer selection binding.spinnerAdoptionCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { if (position > 0) { - binding.spinnerAdoptionPet.setEnabled(true); - binding.spinnerAdoptionPet.setAlpha(1.0f); + UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet); } else { if (!isEditing) { binding.spinnerAdoptionPet.setSelection(0); - binding.spinnerAdoptionPet.setEnabled(false); - binding.spinnerAdoptionPet.setAlpha(0.5f); + UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); } } } @@ -266,8 +264,7 @@ public class AdoptionDetailFragment extends Fragment { binding.tvAdoptionId.setVisibility(View.GONE); // Explicitly disable in add mode - binding.spinnerAdoptionPet.setEnabled(false); - binding.spinnerAdoptionPet.setAlpha(0.5f); + UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); } } @@ -294,8 +291,7 @@ public class AdoptionDetailFragment extends Fragment { // In edit mode, if a customer is already set, ensure pet spinner is enabled if (preselectedCustomerId != -1) { - binding.spinnerAdoptionPet.setEnabled(true); - binding.spinnerAdoptionPet.setAlpha(1.0f); + UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet); } } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show(); 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 169e0532..72d291e8 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 @@ -16,6 +16,7 @@ import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.AppointmentViewModel; import com.example.petstoremobile.viewmodels.CustomerViewModel; import com.example.petstoremobile.viewmodels.PetViewModel; @@ -109,9 +110,8 @@ public class AppointmentDetailFragment extends Fragment { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00","15","30","45"}); - // Pet spinner disabled by default - binding.spinnerPet.setEnabled(false); - binding.spinnerPet.setAlpha(0.5f); + // Pet and Staff spinners disabled by until parent selection + UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff); // Listener to load pets based on selected customer binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @@ -121,16 +121,14 @@ public class AppointmentDetailFragment extends Fragment { DropdownDTO selectedCustomer = customerList.get(position - 1); loadPets(selectedCustomer.getId()); if (!isEditing) { - binding.spinnerPet.setEnabled(true); - binding.spinnerPet.setAlpha(1.0f); + UIUtils.setViewsEnabled(true, binding.spinnerPet); } } else { petList.clear(); refreshPetSpinner(); if (!isEditing) { binding.spinnerPet.setSelection(0); - binding.spinnerPet.setEnabled(false); - binding.spinnerPet.setAlpha(0.5f); + UIUtils.setViewsEnabled(false, binding.spinnerPet); } } } @@ -146,9 +144,16 @@ public class AppointmentDetailFragment extends Fragment { if (position > 0 && position <= storeList.size()) { DropdownDTO selectedStore = storeList.get(position - 1); loadStaff(selectedStore.getId()); + if (!isPastAppointment) { + UIUtils.setViewsEnabled(true, binding.spinnerStaff); + } } else { staffList.clear(); refreshStaffSpinner(); + if (!isEditing) { + binding.spinnerStaff.setSelection(0); + UIUtils.setViewsEnabled(false, binding.spinnerStaff); + } } } @@ -303,19 +308,8 @@ public class AppointmentDetailFragment extends Fragment { binding.tvAppointmentId.setVisibility(View.VISIBLE); binding.btnDeleteAppointment.setVisibility(View.VISIBLE); - binding.spinnerCustomer.setEnabled(false); - binding.spinnerStore.setEnabled(false); - binding.spinnerPet.setEnabled(false); - binding.spinnerService.setEnabled(false); - binding.spinnerCustomer.setAlpha(0.5f); - binding.spinnerStore.setAlpha(0.5f); - binding.spinnerPet.setAlpha(0.5f); - binding.spinnerService.setAlpha(0.5f); - - binding.tvLabelCustomer.setAlpha(0.5f); - binding.tvLabelStore.setAlpha(0.5f); - binding.tvLabelPet.setAlpha(0.5f); - binding.tvLabelService.setAlpha(0.5f); + UIUtils.setViewsEnabled(false, binding.spinnerCustomer, binding.spinnerStore, binding.spinnerPet, binding.spinnerService); + UIUtils.setViewsAlpha(0.5f, binding.tvLabelCustomer, binding.tvLabelStore, binding.tvLabelPet, binding.tvLabelService); loadAppointmentData(); } else { @@ -324,19 +318,9 @@ public class AppointmentDetailFragment extends Fragment { binding.btnDeleteAppointment.setVisibility(View.GONE); binding.tvAppointmentId.setVisibility(View.GONE); - binding.spinnerCustomer.setEnabled(true); - binding.spinnerStore.setEnabled(true); - binding.spinnerPet.setEnabled(false); - binding.spinnerPet.setAlpha(0.5f); - binding.spinnerService.setEnabled(true); - binding.spinnerCustomer.setAlpha(1.0f); - binding.spinnerStore.setAlpha(1.0f); - binding.spinnerService.setAlpha(1.0f); - - binding.tvLabelCustomer.setAlpha(1.0f); - binding.tvLabelStore.setAlpha(1.0f); - binding.tvLabelPet.setAlpha(1.0f); - binding.tvLabelService.setAlpha(1.0f); + UIUtils.setViewsEnabled(true, binding.spinnerCustomer, binding.spinnerStore, binding.spinnerService); + UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff); + UIUtils.setViewsAlpha(1.0f, binding.tvLabelCustomer, binding.tvLabelStore, binding.tvLabelPet, binding.tvLabelService, binding.tvLabelStaff); } } @@ -416,8 +400,7 @@ public class AppointmentDetailFragment extends Fragment { if ("Cancelled".equalsIgnoreCase(currentStatus)) { isPastAppointment = true; disableAllExceptStatus(); - binding.spinnerAppointmentStatus.setEnabled(false); - binding.spinnerAppointmentStatus.setAlpha(0.5f); + UIUtils.setViewsEnabled(false, binding.spinnerAppointmentStatus); binding.btnSaveAppointment.setVisibility(View.GONE); return; } @@ -445,36 +428,18 @@ public class AppointmentDetailFragment extends Fragment { * Disables all input fields except the status spinner */ private void disableAllExceptStatus() { - binding.spinnerCustomer.setEnabled(false); - binding.spinnerStore.setEnabled(false); - binding.spinnerPet.setEnabled(false); - binding.spinnerService.setEnabled(false); - binding.spinnerStaff.setEnabled(false); - binding.etAppointmentDate.setEnabled(false); - binding.spinnerHour.setEnabled(false); - binding.spinnerMinute.setEnabled(false); - - float disabledAlpha = 0.5f; - binding.spinnerCustomer.setAlpha(disabledAlpha); - binding.spinnerStore.setAlpha(disabledAlpha); - binding.spinnerPet.setAlpha(disabledAlpha); - binding.spinnerService.setAlpha(disabledAlpha); - binding.spinnerStaff.setAlpha(disabledAlpha); - binding.etAppointmentDate.setAlpha(disabledAlpha); - binding.spinnerHour.setAlpha(disabledAlpha); - binding.spinnerMinute.setAlpha(disabledAlpha); - - binding.tvLabelCustomer.setAlpha(disabledAlpha); - binding.tvLabelStore.setAlpha(disabledAlpha); - binding.tvLabelPet.setAlpha(disabledAlpha); - binding.tvLabelService.setAlpha(disabledAlpha); - binding.tvLabelStaff.setAlpha(disabledAlpha); - binding.tvLabelDate.setAlpha(disabledAlpha); - binding.tvLabelTime.setAlpha(disabledAlpha); + UIUtils.setViewsEnabled(false, + binding.spinnerCustomer, binding.spinnerStore, binding.spinnerPet, + binding.spinnerService, binding.spinnerStaff, binding.etAppointmentDate, + binding.spinnerHour, binding.spinnerMinute); + + UIUtils.setViewsAlpha(0.5f, + binding.tvLabelCustomer, binding.tvLabelStore, binding.tvLabelPet, + binding.tvLabelService, binding.tvLabelStaff, binding.tvLabelDate, + binding.tvLabelTime); // Keep status enabled - binding.spinnerAppointmentStatus.setEnabled(true); - binding.spinnerAppointmentStatus.setAlpha(1.0f); + UIUtils.setViewsEnabled(true, binding.spinnerAppointmentStatus); } /** 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 e45a93b2..6c44ca59 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 @@ -25,6 +25,7 @@ import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.CustomerViewModel; import com.example.petstoremobile.viewmodels.PetViewModel; import com.example.petstoremobile.viewmodels.StoreViewModel; @@ -221,10 +222,7 @@ public class PetDetailFragment extends Fragment { binding.btnDeletePet.setVisibility(View.VISIBLE); // Disable species and breed fields in edit mode - binding.etPetSpecies.setEnabled(false); - binding.etPetBreed.setEnabled(false); - binding.etPetSpecies.setAlpha(0.5f); - binding.etPetBreed.setAlpha(0.5f); + UIUtils.setViewsEnabled(false, binding.etPetSpecies, binding.etPetBreed); loadPetData(); } else { @@ -237,10 +235,7 @@ public class PetDetailFragment extends Fragment { binding.btnSavePet.setText("Add"); // Enable species and breed fields in edit mode - binding.etPetSpecies.setEnabled(true); - binding.etPetBreed.setEnabled(true); - binding.etPetSpecies.setAlpha(1.0f); - binding.etPetBreed.setAlpha(1.0f); + UIUtils.setViewsEnabled(true, binding.etPetSpecies, binding.etPetBreed); } } @@ -345,21 +340,17 @@ public class PetDetailFragment extends Fragment { //Disable the customer spinner if the status is "Available" if ("Available".equalsIgnoreCase(status)) { binding.spinnerCustomer.setSelection(0); - binding.spinnerCustomer.setEnabled(false); - binding.spinnerCustomer.setAlpha(0.5f); + UIUtils.setViewsEnabled(false, binding.spinnerCustomer); } else { - binding.spinnerCustomer.setEnabled(true); - binding.spinnerCustomer.setAlpha(1.0f); + UIUtils.setViewsEnabled(true, binding.spinnerCustomer); } //Disable the store spinner if the status is "Owned" if ("Owned".equalsIgnoreCase(status)) { binding.spinnerStore.setSelection(0); - binding.spinnerStore.setEnabled(false); - binding.spinnerStore.setAlpha(0.5f); + UIUtils.setViewsEnabled(false, binding.spinnerStore); } else { - binding.spinnerStore.setEnabled(true); - binding.spinnerStore.setAlpha(1.0f); + UIUtils.setViewsEnabled(true, binding.spinnerStore); } } @@ -378,4 +369,4 @@ public class PetDetailFragment extends Fragment { ((TextView) selectedView).setError(null); } } -} \ No newline at end of file +} 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 0be0120c..690a2fa1 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 @@ -15,6 +15,7 @@ import com.example.petstoremobile.viewmodels.*; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.UIUtils; import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.util.*; @@ -80,9 +81,10 @@ public class SaleDetailFragment extends Fragment { // Hide save and input controls for view only if (viewOnly) { binding.btnSaveSale.setVisibility(View.GONE); - binding.spinnerSaleStore.setEnabled(false); - binding.spinnerSaleCustomer.setEnabled(false); - binding.spinnerPaymentMethod.setEnabled(false); + UIUtils.setViewsEnabled(false, + binding.spinnerSaleStore, + binding.spinnerSaleCustomer, + binding.spinnerPaymentMethod); binding.llAddItemRow.setVisibility(View.GONE); binding.llExtraInfo.setVisibility(View.VISIBLE); } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java index 05f6ac18..40f6c92d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java @@ -66,4 +66,28 @@ public class UIUtils { @Override public void afterTextChanged(Editable s) {} }); } + + /** + * Sets the enabled state and alpha for multiple views. + * Alpha is set to 1.0f when enabled and 0.5f when disabled. + */ + public static void setViewsEnabled(boolean enabled, View... views) { + for (View v : views) { + if (v != null) { + v.setEnabled(enabled); + v.setAlpha(enabled ? 1.0f : 0.5f); + } + } + } + + /** + * Sets the alpha for multiple views. + */ + public static void setViewsAlpha(float alpha, View... views) { + for (View v : views) { + if (v != null) { + v.setAlpha(alpha); + } + } + } } From f06f98a6573db95497766b485739932dc8b784ad Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:14:52 -0600 Subject: [PATCH 06/33] Appointments should be fully user frendly now --- .../AppointmentDetailFragment.java | 2 +- .../layout/fragment_appointment_detail.xml | 71 +++++++++---------- 2 files changed, 34 insertions(+), 39 deletions(-) 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 72d291e8..8e0f2be8 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 @@ -319,7 +319,7 @@ public class AppointmentDetailFragment extends Fragment { binding.tvAppointmentId.setVisibility(View.GONE); UIUtils.setViewsEnabled(true, binding.spinnerCustomer, binding.spinnerStore, binding.spinnerService); - UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff); + UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff, binding.spinnerAppointmentStatus); UIUtils.setViewsAlpha(1.0f, binding.tvLabelCustomer, binding.tvLabelStore, binding.tvLabelPet, binding.tvLabelService, binding.tvLabelStaff); } } diff --git a/android/app/src/main/res/layout/fragment_appointment_detail.xml b/android/app/src/main/res/layout/fragment_appointment_detail.xml index 4248311c..7d5ec31d 100644 --- a/android/app/src/main/res/layout/fragment_appointment_detail.xml +++ b/android/app/src/main/res/layout/fragment_appointment_detail.xml @@ -65,8 +65,6 @@ android:layout_gravity="end" android:layout_marginBottom="8dp"/> - - + + + + + - - - - - - - - - - - - - - + + + + + Date: Wed, 8 Apr 2026 21:30:40 -0600 Subject: [PATCH 07/33] small change --- .../fragments/listfragments/AnalyticsFragment.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java index dbd24c75..2e2b2592 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java @@ -52,11 +52,6 @@ public class AnalyticsFragment extends Fragment { binding.llEmployeePerformance.removeAllViews(); binding.llDailyRevenue.removeAllViews(); - // Show loading - binding.tvTotalRevenue.setText("Loading..."); - binding.tvTotalTransactions.setText("..."); - binding.tvAvgTransaction.setText("..."); - binding.tvTotalItems.setText("..."); saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc") .observe(getViewLifecycleOwner(), resource -> { @@ -72,7 +67,10 @@ public class AnalyticsFragment extends Fragment { showError("Failed to load sales data"); break; case LOADING: - // Already showing loading state in UI + binding.tvTotalRevenue.setText("Loading..."); + binding.tvTotalTransactions.setText("..."); + binding.tvAvgTransaction.setText("..."); + binding.tvTotalItems.setText("..."); break; } } From f98abf19efc2cc4af9ddafde1ba294bec011d63b Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:55:00 -0600 Subject: [PATCH 08/33] Moved appointments businiss logic to modelview andriod --- .../AppointmentDetailFragment.java | 568 ++++++------------ .../petstoremobile/utils/DateTimeUtils.java | 98 +++ .../viewmodels/AppointmentViewModel.java | 312 +++++++++- 3 files changed, 584 insertions(+), 394 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java 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 8e0f2be8..96001734 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 @@ -13,16 +13,12 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.databinding.FragmentAppointmentDetailBinding; import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.AppointmentViewModel; -import com.example.petstoremobile.viewmodels.CustomerViewModel; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.ServiceViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; -import com.example.petstoremobile.viewmodels.UserViewModel; import java.util.*; @@ -36,40 +32,22 @@ public class AppointmentDetailFragment extends Fragment { private FragmentAppointmentDetailBinding binding; - private long appointmentId = -1; - private boolean isEditing = false; - private boolean isPastAppointment = false; private long preselectedPetId = -1; private long preselectedServiceId = -1; private long preselectedCustomerId = -1; private long preselectedStoreId = -1; private long preselectedStaffId = -1; - private List petList = new ArrayList<>(); - private List serviceList = new ArrayList<>(); - private List customerList = new ArrayList<>(); - private List storeList = new ArrayList<>(); - private List staffList = new ArrayList<>(); - private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; private final Integer[] MINUTES = {0,15,30,45}; private AppointmentViewModel appointmentViewModel; - private PetViewModel petViewModel; - private ServiceViewModel serviceViewModel; - private StoreViewModel storeViewModel; - private CustomerViewModel customerViewModel; - private UserViewModel userViewModel; + private boolean isUpdatingUI = false; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); - petViewModel = new ViewModelProvider(this).get(PetViewModel.class); - serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); - customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); - userViewModel = new ViewModelProvider(this).get(UserViewModel.class); } @Override @@ -83,7 +61,8 @@ public class AppointmentDetailFragment extends Fragment { super.onViewCreated(view, savedInstanceState); setupSpinners(); setupDatePicker(); - loadSpinnersData(); + observeViewModel(); + appointmentViewModel.loadInitialFormData(); handleArguments(); binding.btnApptBack.setOnClickListener(v -> navigateBack()); @@ -101,9 +80,10 @@ public class AppointmentDetailFragment extends Fragment { * Configures the adapters for spinners. */ private void setupSpinners() { - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, - new String[]{"Booked", "Completed", "Cancelled", "Missed"}); + //Status Spinner is empty by default the date determines whats in here + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, new String[]{}); + // Set up hour and minute spinners String[] hours = new String[HOURS.length]; for (int i = 0; i < HOURS.length; i++) hours[i] = String.format("%02d:00", HOURS[i]); @@ -113,50 +93,41 @@ public class AppointmentDetailFragment extends Fragment { // Pet and Staff spinners disabled by until parent selection UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff); - // Listener to load pets based on selected customer + // Listener to notify ViewModel of customer selection binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (position > 0 && position <= customerList.size()) { - DropdownDTO selectedCustomer = customerList.get(position - 1); - loadPets(selectedCustomer.getId()); - if (!isEditing) { - UIUtils.setViewsEnabled(true, binding.spinnerPet); - } - } else { - petList.clear(); - refreshPetSpinner(); - if (!isEditing) { - binding.spinnerPet.setSelection(0); - UIUtils.setViewsEnabled(false, binding.spinnerPet); - } - } + appointmentViewModel.onCustomerSelected(position); } - @Override public void onNothingSelected(AdapterView parent) {} }); - // Listener to load staff based on selected store + // Listener to notify ViewModel of store selection binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (position > 0 && position <= storeList.size()) { - DropdownDTO selectedStore = storeList.get(position - 1); - loadStaff(selectedStore.getId()); - if (!isPastAppointment) { - UIUtils.setViewsEnabled(true, binding.spinnerStaff); - } - } else { - staffList.clear(); - refreshStaffSpinner(); - if (!isEditing) { - binding.spinnerStaff.setSelection(0); - UIUtils.setViewsEnabled(false, binding.spinnerStaff); - } - } + appointmentViewModel.onStoreSelected(position); } + @Override + public void onNothingSelected(AdapterView parent) {} + }); + // Listeners for other selections + binding.spinnerService.setOnItemSelectedListener(new OnIndexSelected(p -> appointmentViewModel.onServiceSelected(p))); + binding.spinnerPet.setOnItemSelectedListener(new OnIndexSelected(p -> appointmentViewModel.onPetSelected(p))); + binding.spinnerStaff.setOnItemSelectedListener(new OnIndexSelected(p -> appointmentViewModel.onStaffSelected(p))); + + // Listeners for time changes + binding.spinnerHour.setOnItemSelectedListener(new OnIndexSelected(p -> notifyDateTimeStatusChange())); + binding.spinnerMinute.setOnItemSelectedListener(new OnIndexSelected(p -> notifyDateTimeStatusChange())); + + // Listener to notify ViewModel of status selection + binding.spinnerAppointmentStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + notifyDateTimeStatusChange(); + } @Override public void onNothingSelected(AdapterView parent) {} }); @@ -167,11 +138,13 @@ public class AppointmentDetailFragment extends Fragment { */ private void setupDatePicker() { binding.etAppointmentDate.setOnClickListener(v -> { - if (isPastAppointment) return; Calendar c = Calendar.getInstance(); DatePickerDialog d = new DatePickerDialog(requireContext(), - (dp,y,m,d1) -> binding.etAppointmentDate.setText( - String.format("%04d-%02d-%02d", y, m+1, d1)), + (dp,y,m,d1) -> { + String selectedDate = String.format("%04d-%02d-%02d", y, m+1, d1); + binding.etAppointmentDate.setText(selectedDate); + notifyDateTimeStatusChange(); + }, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)); d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000); @@ -180,118 +153,79 @@ public class AppointmentDetailFragment extends Fragment { } /** - * Fetches all required data for spinners from the backend. + * Observes the ViewModel for UI state and list updates. */ - private void loadSpinnersData() { - loadServices(); - loadCustomers(); - loadStores(); + private void observeViewModel() { + appointmentViewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); + + // Populate spinners when data arrives + appointmentViewModel.getCustomers().observe(getViewLifecycleOwner(), list -> + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, list, DropdownDTO::getLabel, "-- Select Customer --", preselectedCustomerId, DropdownDTO::getId)); + + appointmentViewModel.getStores().observe(getViewLifecycleOwner(), list -> + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, list, DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId)); + + appointmentViewModel.getServices().observe(getViewLifecycleOwner(), list -> + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, list, ServiceDTO::getServiceName, "-- Select Service --", preselectedServiceId, ServiceDTO::getServiceId)); + + appointmentViewModel.getCustomerPets().observe(getViewLifecycleOwner(), list -> + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, list, DropdownDTO::getLabel, "-- Select Pet --", preselectedPetId, DropdownDTO::getId)); + + appointmentViewModel.getStoreEmployees().observe(getViewLifecycleOwner(), list -> + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, list, DropdownDTO::getLabel, "-- Select Staff --", preselectedStaffId, DropdownDTO::getId)); } /** - * Loads the list of pets from the ViewModel, filtered by customerId. + * Applies the ViewState provided by the ViewModel to the UI components. */ - private void loadPets(Long customerId) { - petViewModel.getCustomerPets(customerId).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - petList = resource.data; - refreshPetSpinner(); - } - }); + private void applyViewState(AppointmentViewModel.ViewState state) { + isUpdatingUI = true; + + // Mode specific UI + binding.tvApptMode.setText(state.isEditing ? "Edit Appointment" : "Add Appointment"); + binding.tvAppointmentId.setText("ID: " + appointmentViewModel.getAppointmentId()); + binding.tvAppointmentId.setVisibility(state.isEditing ? View.VISIBLE : View.GONE); + binding.btnDeleteAppointment.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); + binding.btnSaveAppointment.setVisibility(state.isSaveVisible ? View.VISIBLE : View.GONE); + + // Enabling/Disabling Views + UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer); + UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore); + UIUtils.setViewsEnabled(state.isPetEnabled, binding.spinnerPet); + UIUtils.setViewsEnabled(state.isServiceEnabled, binding.spinnerService); + UIUtils.setViewsEnabled(state.isStaffEnabled, binding.spinnerStaff); + UIUtils.setViewsEnabled(state.isDateEnabled, binding.etAppointmentDate); + UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerHour, binding.spinnerMinute); + UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAppointmentStatus); + + // Alpha for disabled look + float alpha = 1.0f; + float disabledAlpha = 0.5f; + UIUtils.setViewsAlpha(state.isCustomerEnabled ? alpha : disabledAlpha, binding.tvLabelCustomer); + UIUtils.setViewsAlpha(state.isStoreEnabled ? alpha : disabledAlpha, binding.tvLabelStore); + UIUtils.setViewsAlpha(state.isPetEnabled ? alpha : disabledAlpha, binding.tvLabelPet); + UIUtils.setViewsAlpha(state.isServiceEnabled ? alpha : disabledAlpha, binding.tvLabelService); + UIUtils.setViewsAlpha(state.isStaffEnabled ? alpha : disabledAlpha, binding.tvLabelStaff); + UIUtils.setViewsAlpha(state.isDateEnabled ? alpha : disabledAlpha, binding.tvLabelDate); + UIUtils.setViewsAlpha(state.isTimeEnabled ? alpha : disabledAlpha, binding.tvLabelTime); + + // Update status options + Object selected = binding.spinnerAppointmentStatus.getSelectedItem(); + String current = selected != null ? selected.toString() : ""; + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, state.availableStatuses); + SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, current); + + isUpdatingUI = false; } - /** - * Populates the pet selection spinner. - */ - private void refreshPetSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList, - DropdownDTO::getLabel, "-- Select Pet --", - preselectedPetId, DropdownDTO::getId); - } + private void notifyDateTimeStatusChange() { + if (isUpdatingUI) return; - /** - * Loads the list of services from the API. - */ - private void loadServices() { - serviceViewModel.getAllServices(0, 200, null, "serviceName").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - serviceList = resource.data.getContent(); - refreshServiceSpinner(); - } - }); - } - - /** - * Populates the service selection spinner. - */ - private void refreshServiceSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, serviceList, - ServiceDTO::getServiceName, "-- Select Service --", - preselectedServiceId, ServiceDTO::getServiceId); - } - - /** - * Loads the list of customers from the API. - */ - private void loadCustomers() { - customerViewModel.getCustomerDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data; - refreshCustomerSpinner(); - } - }); - } - - /** - * Populates the customer selection spinner. - */ - private void refreshCustomerSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, customerList, - DropdownDTO::getLabel, - "-- Select Customer --", - preselectedCustomerId, DropdownDTO::getId); - } - - /** - * Loads the list of stores from the API. - */ - private void loadStores() { - storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data; - refreshStoreSpinner(); - } - }); - } - - /** - * Populates the store selection spinner. - */ - private void refreshStoreSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList, - DropdownDTO::getLabel, "-- Select Store --", - preselectedStoreId, DropdownDTO::getId); - } - - /** - * Loads the list of staff for a specific store. - */ - private void loadStaff(Long storeId) { - storeViewModel.getStoreEmployees(storeId).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - staffList = resource.data; - refreshStaffSpinner(); - } - }); - } - - /** - * Populates the staff selection spinner. - */ - private void refreshStaffSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, staffList, - DropdownDTO::getLabel, "-- Select Staff --", - preselectedStaffId, DropdownDTO::getId); + String date = binding.etAppointmentDate.getText().toString(); + String time = buildTimeString(); + Object selected = binding.spinnerAppointmentStatus.getSelectedItem(); + String status = selected != null ? selected.toString() : ""; + appointmentViewModel.onDateOrTimeChanged(date, time, status); } /** @@ -300,27 +234,10 @@ public class AppointmentDetailFragment extends Fragment { private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("appointmentId")) { - //edit mode - isEditing = true; - appointmentId = a.getLong("appointmentId"); - binding.tvApptMode.setText("Edit Appointment"); - binding.tvAppointmentId.setText("ID: " + appointmentId); - binding.tvAppointmentId.setVisibility(View.VISIBLE); - binding.btnDeleteAppointment.setVisibility(View.VISIBLE); - - UIUtils.setViewsEnabled(false, binding.spinnerCustomer, binding.spinnerStore, binding.spinnerPet, binding.spinnerService); - UIUtils.setViewsAlpha(0.5f, binding.tvLabelCustomer, binding.tvLabelStore, binding.tvLabelPet, binding.tvLabelService); - + appointmentViewModel.setAppointmentId(a.getLong("appointmentId")); loadAppointmentData(); } else { - //add mode - binding.tvApptMode.setText("Add Appointment"); - binding.btnDeleteAppointment.setVisibility(View.GONE); - binding.tvAppointmentId.setVisibility(View.GONE); - - UIUtils.setViewsEnabled(true, binding.spinnerCustomer, binding.spinnerStore, binding.spinnerService); - UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff, binding.spinnerAppointmentStatus); - UIUtils.setViewsAlpha(1.0f, binding.tvLabelCustomer, binding.tvLabelStore, binding.tvLabelPet, binding.tvLabelService, binding.tvLabelStaff); + appointmentViewModel.setAppointmentId(-1); } } @@ -328,229 +245,83 @@ public class AppointmentDetailFragment extends Fragment { * Fetches specific appointment details from the backend using the ID. */ private void loadAppointmentData() { - appointmentViewModel.getAppointmentById(appointmentId).observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - AppointmentDTO a = resource.data; - preselectedPetId = (a.getPetId() != null) ? a.getPetId() : -1; - preselectedServiceId = (a.getServiceId() != null) ? a.getServiceId() : -1; - preselectedCustomerId = (a.getCustomerId() != null) ? a.getCustomerId() : -1; - preselectedStoreId = (a.getStoreId() != null) ? a.getStoreId() : -1; - preselectedStaffId = (a.getEmployeeId() != null) ? a.getEmployeeId() : -1; + appointmentViewModel.loadAppointment().observe(getViewLifecycleOwner(), resource -> { + if (resource == null || resource.status != Resource.Status.SUCCESS || resource.data == null) return; + AppointmentDTO a = resource.data; + preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; + preselectedServiceId = a.getServiceId() != null ? a.getServiceId() : -1; + preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; + preselectedStoreId = a.getStoreId() != null ? a.getStoreId() : -1; + preselectedStaffId = a.getEmployeeId() != null ? a.getEmployeeId() : -1; - binding.etAppointmentDate.setText(a.getAppointmentDate()); - - // Pre-fill time spinners - String time = a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00"; - if (time.length() > 5) time = time.substring(0, 5); - String[] parts = time.split(":"); - if (parts.length == 2) { - try { - int hour = Integer.parseInt(parts[0]); - int min = Integer.parseInt(parts[1]); - for (int i = 0; i < HOURS.length; i++) - if (HOURS[i] == hour) { binding.spinnerHour.setSelection(i); break; } - for (int i = 0; i < MINUTES.length; i++) - if (MINUTES[i] == min) { binding.spinnerMinute.setSelection(i); break; } - } catch (NumberFormatException ignored) {} - } - - // Match Title labels with backend values - String status = a.getAppointmentStatus(); - if (status != null && !status.isEmpty()) { - String formattedStatus = status.substring(0, 1).toUpperCase() + status.substring(1).toLowerCase(); - SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, formattedStatus); - } - - checkIfPastAndDisable(a.getAppointmentDate(), time); - - refreshPetSpinner(); - refreshServiceSpinner(); - refreshCustomerSpinner(); - refreshStoreSpinner(); - refreshStaffSpinner(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Failed to load appointment: " + resource.message, Toast.LENGTH_SHORT).show(); + binding.etAppointmentDate.setText(a.getAppointmentDate()); + parseAndSetTimeSpinners(a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00"); + + String status = a.getAppointmentStatus(); + if (status != null && !status.isEmpty()) { + SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, DateTimeUtils.formatStatusFromBackend(status)); } + notifyDateTimeStatusChange(); }); } - /** - * Checks if the appointment is in the past and disables fields. - */ - private void checkIfPastAndDisable(String date, String time) { - if (date == null || time == null) return; - try { - Calendar selected = Calendar.getInstance(); - String[] dateParts = date.split("-"); - String[] timeParts = time.split(":"); - selected.set( - Integer.parseInt(dateParts[0]), - Integer.parseInt(dateParts[1]) - 1, - Integer.parseInt(dateParts[2]), - Integer.parseInt(timeParts[0]), - Integer.parseInt(timeParts[1]), - 0 - ); - - Object selectedItem = binding.spinnerAppointmentStatus.getSelectedItem(); - String currentStatus = selectedItem != null ? selectedItem.toString() : ""; - - // If the appointment is already Cancelled, disable all fields - if ("Cancelled".equalsIgnoreCase(currentStatus)) { - isPastAppointment = true; - disableAllExceptStatus(); - UIUtils.setViewsEnabled(false, binding.spinnerAppointmentStatus); - binding.btnSaveAppointment.setVisibility(View.GONE); - return; - } - - // If the appointment date/time is in the past - if (selected.before(Calendar.getInstance())) { - isPastAppointment = true; - disableAllExceptStatus(); - - // Make status spinner only have Completed or Missed - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, - new String[]{"Completed", "Missed"}); - - // Restore selection if it's already one of the valid options - if (currentStatus.equals("Completed") || currentStatus.equals("Missed")) { - SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, currentStatus); - } - } - } catch (Exception e) { - Log.e("APPT_DETAIL", "Error parsing date/time for past check: " + e.getMessage()); - } - } - - /** - * Disables all input fields except the status spinner - */ - private void disableAllExceptStatus() { - UIUtils.setViewsEnabled(false, - binding.spinnerCustomer, binding.spinnerStore, binding.spinnerPet, - binding.spinnerService, binding.spinnerStaff, binding.etAppointmentDate, - binding.spinnerHour, binding.spinnerMinute); - - UIUtils.setViewsAlpha(0.5f, - binding.tvLabelCustomer, binding.tvLabelStore, binding.tvLabelPet, - binding.tvLabelService, binding.tvLabelStaff, binding.tvLabelDate, - binding.tvLabelTime); - - // Keep status enabled - UIUtils.setViewsEnabled(true, binding.spinnerAppointmentStatus); - } - /** * Validates input and saves the appointment to the backend. */ private void saveAppointment() { - if (binding.spinnerCustomer.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; - } - if (binding.spinnerStore.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return; - } - if (binding.spinnerPet.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return; - } - if (binding.spinnerService.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a service", Toast.LENGTH_SHORT).show(); return; - } + if (!validateRequiredFields()) return; + String date = binding.etAppointmentDate.getText().toString().trim(); - if (date.isEmpty()) { - Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; - } - - DropdownDTO customer = customerList.get(binding.spinnerCustomer.getSelectedItemPosition() - 1); - DropdownDTO store = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1); - DropdownDTO pet = petList.get(binding.spinnerPet.getSelectedItemPosition() - 1); - ServiceDTO service = serviceList.get(binding.spinnerService.getSelectedItemPosition() - 1); - - Long employeeId = null; - if (binding.spinnerStaff.getSelectedItemPosition() > 0) { - employeeId = staffList.get(binding.spinnerStaff.getSelectedItemPosition() - 1).getId(); - } - - String time = String.format("%02d:%02d", - HOURS[binding.spinnerHour.getSelectedItemPosition()], - MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); - - // Get status and convert to uppercase for backend + String time = buildTimeString(); String status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase(); - - // Validate future date+time if status is BOOKED - if ("BOOKED".equalsIgnoreCase(status)) { - try { - String[] dateParts = date.split("-"); - String[] timeParts = time.split(":"); - Calendar selected = Calendar.getInstance(); - selected.set( - Integer.parseInt(dateParts[0]), - Integer.parseInt(dateParts[1]) - 1, - Integer.parseInt(dateParts[2]), - Integer.parseInt(timeParts[0]), - Integer.parseInt(timeParts[1]), - 0 - ); - if (selected.before(Calendar.getInstance())) { - DialogUtils.showInfoDialog(requireContext(), "Invalid Time", - "Booked appointments must be in the future. " + - "Please select a future date and time."); - return; - } - } catch (Exception e) { - Log.e("APPT_SAVE", "Date parse error: " + e.getMessage()); - } + if (!appointmentViewModel.isValidFutureBooking(status, date, time)) { + DialogUtils.showInfoDialog(requireContext(), "Invalid Time", "Booked appointments must be in the future."); + return; } - // Build DTO with all required IDs - AppointmentDTO dto = new AppointmentDTO( - customer.getId(), - store.getId(), - service.getServiceId(), - employeeId, - date, - time, - status, - pet.getId() - ); - - androidx.lifecycle.Observer> observer = resource -> { + appointmentViewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), isEditing ? "Updated" : "Saved", Toast.LENGTH_SHORT).show(); + AppointmentViewModel.ViewState state = appointmentViewModel.getViewState().getValue(); + String message = (state != null && state.isEditing) ? "Updated" : "Saved"; + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { handleSaveError(resource.message); } - }; + }); + } - if (isEditing) { - appointmentViewModel.updateAppointment(appointmentId, dto).observe(getViewLifecycleOwner(), observer); - } else { - appointmentViewModel.createAppointment(dto).observe(getViewLifecycleOwner(), observer); - } + /** + * Validates that all required fields are selected. + */ + private boolean validateRequiredFields() { + if (binding.spinnerCustomer.getSelectedItemPosition() == 0) return showToast("Select a customer"); + if (binding.spinnerStore.getSelectedItemPosition() == 0) return showToast("Select a store"); + if (binding.spinnerPet.getSelectedItemPosition() == 0) return showToast("Select a pet"); + if (binding.spinnerService.getSelectedItemPosition() == 0) return showToast("Select a service"); + if (binding.etAppointmentDate.getText().toString().trim().isEmpty()) return showToast("Select a date"); + return true; + } + + private boolean showToast(String msg) { + Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); + return false; + } + + /** + * Builds a time string from the hour and minute spinners. + */ + private String buildTimeString() { + return String.format("%02d:%02d", HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); } /** * Handles errors that occur during the saving process. */ private void handleSaveError(String errorMessage) { - if (errorMessage != null) { - Log.e("APPT_SAVE", "Error: " + errorMessage); - if (errorMessage.toLowerCase().contains("future")) { - DialogUtils.showInfoDialog(requireContext(), "Invalid Date/Time", - "Booked appointments must be scheduled in the future."); - } else if (errorMessage.toLowerCase().contains("not available")) { - showNoAvailabilityDialog(); - } else { - Toast.makeText(getContext(), errorMessage, Toast.LENGTH_SHORT).show(); - } - } else { - Toast.makeText(getContext(), "Something went wrong", Toast.LENGTH_SHORT).show(); - } + if (errorMessage != null && errorMessage.toLowerCase().contains("not available")) showNoAvailabilityDialog(); + else Toast.makeText(getContext(), errorMessage != null ? errorMessage : "Error saving", Toast.LENGTH_SHORT).show(); } /** @@ -559,11 +330,9 @@ public class AppointmentDetailFragment extends Fragment { private void showNoAvailabilityDialog() { new androidx.appcompat.app.AlertDialog.Builder(requireContext()) .setTitle("No Availability") - .setMessage("This time slot is already booked. Please choose a different time or date.") + .setMessage("This time slot is already booked.") .setPositiveButton("Change Time", (d, w) -> d.dismiss()) - .setNegativeButton("Cancel Booking", (d, w) -> navigateBack()) - .setCancelable(false) - .show(); + .setNegativeButton("Cancel Booking", (d, w) -> navigateBack()).show(); } /** @@ -571,13 +340,8 @@ public class AppointmentDetailFragment extends Fragment { */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () -> - appointmentViewModel.deleteAppointment(appointmentId).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show(); - } + appointmentViewModel.deleteAppointment().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) navigateBack(); })); } @@ -587,4 +351,24 @@ public class AppointmentDetailFragment extends Fragment { private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + + /** + * Parses a time string and sets the hour and minute spinners. + */ + private void parseAndSetTimeSpinners(String time) { + int[] parsedTime = DateTimeUtils.parseTimeString(time); + if (parsedTime == null) return; + for (int i = 0; i < HOURS.length; i++) if (HOURS[i] == parsedTime[0]) binding.spinnerHour.setSelection(i); + for (int i = 0; i < MINUTES.length; i++) if (MINUTES[i] == parsedTime[1]) binding.spinnerMinute.setSelection(i); + } + + /** + * Helper listener for simple index reporting. + */ + private static class OnIndexSelected implements AdapterView.OnItemSelectedListener { + private final java.util.function.Consumer callback; + public OnIndexSelected(java.util.function.Consumer callback) { this.callback = callback; } + @Override public void onItemSelected(AdapterView p, View v, int pos, long id) { callback.accept(pos); } + @Override public void onNothingSelected(AdapterView p) {} + } } 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 new file mode 100644 index 00000000..4e79a190 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java @@ -0,0 +1,98 @@ +package com.example.petstoremobile.utils; + +import android.util.Log; + +import java.util.Calendar; + +/** + * Utility class for date and time operations. + */ +public class DateTimeUtils { + + private static final String TAG = "DateTimeUtils"; + + /** + * Formats status from backend format to UI format + * (backend is using all caps so we lower case them with this function) + */ + public static String formatStatusFromBackend(String status) { + if (status == null || status.isEmpty()) return status; + return status.substring(0, 1).toUpperCase() + status.substring(1).toLowerCase(); + } + + /** + * Converts a date and time string to a Calendar object. + * format: date = "YYYY-MM-DD", time = "HH:MM" + */ + public static Calendar parseDateTimeToCalendar(String date, String time) throws Exception { + Calendar calendar = Calendar.getInstance(); + String[] dateParts = date.split("-"); + String[] timeParts = time.split(":"); + + calendar.set( + Integer.parseInt(dateParts[0]), + Integer.parseInt(dateParts[1]) - 1, + Integer.parseInt(dateParts[2]), + Integer.parseInt(timeParts[0]), + Integer.parseInt(timeParts[1]), + 0 + ); + return calendar; + } + + /** + * Checks if a given date is in the past. + * format: date = "YYYY-MM-DD" + */ + public static boolean isDateInPast(String date) { + if (date == null || date.isEmpty()) return false; + try { + Calendar selected = Calendar.getInstance(); + String[] dateParts = date.split("-"); + selected.set( + Integer.parseInt(dateParts[0]), + Integer.parseInt(dateParts[1]) - 1, + Integer.parseInt(dateParts[2]), + 0, 0, 0 + ); + return selected.before(Calendar.getInstance()); + } 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" + */ + public static boolean isDateTimeInPast(String date, String time) { + if (date == null || date.isEmpty() || time == null || time.isEmpty()) return false; + try { + Calendar selected = parseDateTimeToCalendar(date, time); + return selected.before(Calendar.getInstance()); + } catch (Exception e) { + Log.e(TAG, "Error parsing date/time: " + e.getMessage()); + return false; + } + } + + /** + * Parses a time string and returns hour and minute indices for the spinners. + */ + public static int[] parseTimeString(String time) { + if (time == null || time.isEmpty()) return null; + if (time.length() > 5) time = time.substring(0, 5); + + String[] parts = time.split(":"); + if (parts.length != 2) return null; + + try { + int hour = Integer.parseInt(parts[0]); + int min = Integer.parseInt(parts[1]); + return new int[]{hour, min}; + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java index 69f24c95..32f359e7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java @@ -1,14 +1,23 @@ package com.example.petstoremobile.viewmodels; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.repositories.AppointmentRepository; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.repositories.ServiceRepository; +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.List; import javax.inject.Inject; @@ -18,12 +27,42 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class AppointmentViewModel extends ViewModel { private final AppointmentRepository repository; + private final CustomerRepository customerRepository; + private final StoreRepository storeRepository; + private final PetRepository petRepository; + private final ServiceRepository serviceRepository; + + private final MutableLiveData> customers = new MutableLiveData<>(); + private final MutableLiveData> stores = new MutableLiveData<>(); + private final MutableLiveData> services = new MutableLiveData<>(); + private final MutableLiveData> customerPets = new MutableLiveData<>(); + private final MutableLiveData> storeEmployees = new MutableLiveData<>(); + + private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); + + private long appointmentId = -1; + private Long currentCustomerId; + private Long currentStoreId; + private Long currentPetId; + private Long currentServiceId; + private Long currentStaffId; @Inject - public AppointmentViewModel(AppointmentRepository repository) { + public AppointmentViewModel( + AppointmentRepository repository, + CustomerRepository customerRepository, + StoreRepository storeRepository, + PetRepository petRepository, + ServiceRepository serviceRepository) { this.repository = repository; + this.customerRepository = customerRepository; + this.storeRepository = storeRepository; + this.petRepository = petRepository; + this.serviceRepository = serviceRepository; } + // API CRUD + /** * Fetches a paginated list of all appointments with optional filters. */ @@ -65,4 +104,273 @@ public class AppointmentViewModel extends ViewModel { public LiveData> bulkDeleteAppointments(List ids) { return repository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); } -} \ No newline at end of file + + // Initial Data Loading + + /** + * Loads initial dropdown data for customers, stores, and services. + */ + public void loadInitialFormData() { + customerRepository.getCustomerDropdowns().observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) customers.setValue(r.data); + }); + storeRepository.getStoreDropdowns().observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) stores.setValue(r.data); + }); + serviceRepository.getAllServices(0, 200, null, "serviceName").observeForever(r -> { + if (r.status == Resource.Status.SUCCESS && r.data != null) services.setValue(r.data.getContent()); + }); + } + + // LiveData Getters + + public LiveData> getCustomers() { return customers; } + public LiveData> getStores() { return stores; } + public LiveData> getServices() { return services; } + public LiveData> getCustomerPets() { return customerPets; } + public LiveData> getStoreEmployees() { return storeEmployees; } + public LiveData getViewState() { return viewState; } + + //State Getters + + public long getAppointmentId() { return appointmentId; } + + /** + * Sets the current appointment ID and updates the mode. + */ + public void setAppointmentId(long id) { + this.appointmentId = id; + initMode(id != -1); + } + + // Selection Handlers for spinners + + public void onCustomerSelected(int position) { + List list = customers.getValue(); + if (position > 0 && list != null && position <= list.size()) { + currentCustomerId = list.get(position - 1).getId(); + loadPetsForCustomer(currentCustomerId); + updateViewState(s -> { + s.selectedCustomerId = currentCustomerId; + s.isPetEnabled = !s.isEditing; + }); + } else { + currentCustomerId = null; + customerPets.setValue(new ArrayList<>()); + updateViewState(s -> { + s.selectedCustomerId = null; + s.isPetEnabled = false; + }); + } + } + + public void onStoreSelected(int position) { + List list = stores.getValue(); + if (position > 0 && list != null && position <= list.size()) { + currentStoreId = list.get(position - 1).getId(); + loadEmployeesForStore(currentStoreId); + updateViewState(s -> { + s.selectedStoreId = currentStoreId; + s.isStaffEnabled = !s.isPast; + }); + } else { + currentStoreId = null; + storeEmployees.setValue(new ArrayList<>()); + updateViewState(s -> { + s.selectedStoreId = null; + s.isStaffEnabled = false; + }); + } + } + + public void onServiceSelected(int position) { + List list = services.getValue(); + currentServiceId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getServiceId() : null; + updateViewState(s -> s.selectedServiceId = currentServiceId); + } + + public void onPetSelected(int position) { + List list = customerPets.getValue(); + currentPetId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; + updateViewState(s -> s.selectedPetId = currentPetId); + } + + public void onStaffSelected(int position) { + List list = storeEmployees.getValue(); + currentStaffId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; + updateViewState(s -> s.selectedStaffId = currentStaffId); + } + + private void loadPetsForCustomer(Long customerId) { + petRepository.getCustomerPets(customerId).observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) customerPets.setValue(r.data); + }); + } + + private void loadEmployeesForStore(Long storeId) { + storeRepository.getStoreEmployees(storeId).observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data); + }); + } + + // Appointment Detail CRUD + + /** + * Fetches appointment details and populates internal state. + */ + public LiveData> loadAppointment() { + MutableLiveData> result = new MutableLiveData<>(); + repository.getAppointmentById(appointmentId).observeForever(resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + AppointmentDTO a = resource.data; + currentCustomerId = a.getCustomerId(); + currentStoreId = a.getStoreId(); + currentPetId = a.getPetId(); + currentServiceId = a.getServiceId(); + currentStaffId = a.getEmployeeId(); + + updateViewState(s -> { + s.selectedCustomerId = currentCustomerId; + s.selectedStoreId = currentStoreId; + s.selectedPetId = currentPetId; + s.selectedServiceId = currentServiceId; + s.selectedStaffId = currentStaffId; + }); + + if (currentCustomerId != null) loadPetsForCustomer(currentCustomerId); + if (currentStoreId != null) loadEmployeesForStore(currentStoreId); + } + result.setValue(resource); + }); + return result; + } + + /** + * Saves the appointment by building the DTO from tracked state. + */ + public LiveData> saveAppointment(String date, String time, String status) { + AppointmentDTO dto = new AppointmentDTO(currentCustomerId, currentStoreId, currentServiceId, currentStaffId, date, time, status, currentPetId); + if (appointmentId != -1) { + return repository.updateAppointment(appointmentId, dto); + } else { + return repository.createAppointment(dto); + } + } + + /** + * Deletes the current appointment. + */ + public LiveData> deleteAppointment() { + return repository.deleteAppointment(appointmentId); + } + + // --- UI Logic --- + + public void onDateOrTimeChanged(String date, String time, String currentStatus) { + updateViewState(s -> { + s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus); + boolean isPast = DateTimeUtils.isDateTimeInPast(date, time); + boolean isCancelled = "Cancelled".equalsIgnoreCase(currentStatus); + + if (isCancelled) { + s.isPast = true; + setAllFieldsEnabled(s, false); + s.isStatusEnabled = false; + s.isSaveVisible = false; + } else if (isPast) { + s.isPast = true; + setAllFieldsEnabled(s, false); + s.isStatusEnabled = true; + } else { + s.isPast = false; + if (!s.isEditing) { + s.isCustomerEnabled = true; + s.isStoreEnabled = true; + s.isServiceEnabled = true; + s.isPetEnabled = currentCustomerId != null; + } + s.isDateEnabled = true; + s.isTimeEnabled = true; + s.isStatusEnabled = true; + } + }); + } + + private String[] calculateAvailableStatuses(boolean isEditing, String date, String currentTime, String currentStatus) { + if (!isEditing) return new String[]{"Booked"}; + if (date == null || date.isEmpty()) return new String[]{}; + if ("Cancelled".equalsIgnoreCase(currentStatus)) return new String[]{"Cancelled"}; + if (DateTimeUtils.isDateTimeInPast(date, currentTime)) return new String[]{"Completed", "Missed"}; + return new String[]{"Booked", "Cancelled"}; + } + + private void setAllFieldsEnabled(ViewState s, boolean enabled) { + s.isCustomerEnabled = enabled; + s.isStoreEnabled = enabled; + s.isPetEnabled = enabled; + s.isServiceEnabled = enabled; + s.isStaffEnabled = enabled; + s.isDateEnabled = enabled; + s.isTimeEnabled = enabled; + } + + public void initMode(boolean isEditing) { + updateViewState(s -> { + s.isEditing = isEditing; + s.isDeleteVisible = isEditing; + if (isEditing) { + s.isCustomerEnabled = false; + s.isStoreEnabled = false; + s.isPetEnabled = false; + s.isServiceEnabled = false; + } else { + s.isCustomerEnabled = true; + s.isStoreEnabled = true; + s.isServiceEnabled = true; + s.isPetEnabled = false; // until customer selected + s.isStaffEnabled = false; // until store selected + s.availableStatuses = new String[]{"Booked"}; + } + }); + } + + public boolean isValidFutureBooking(String status, String date, String time) { + return !"BOOKED".equalsIgnoreCase(status) || !DateTimeUtils.isDateTimeInPast(date, time); + } + + private void updateViewState(Action action) { + ViewState current = viewState.getValue(); + if (current != null) { + action.run(current); + viewState.setValue(current); + } + } + + private interface Action { void run(T t); } + + /** + * A Class to show the states of Appointment Detail Fragment. + */ + public static class ViewState { + public boolean isPast = false; + public boolean isEditing = false; + public boolean isSaveVisible = true; + public boolean isDeleteVisible = false; + public boolean isCustomerEnabled = true; + public boolean isStoreEnabled = true; + public boolean isPetEnabled = false; + public boolean isServiceEnabled = true; + public boolean isStaffEnabled = false; + public boolean isDateEnabled = true; + public boolean isTimeEnabled = true; + public boolean isStatusEnabled = true; + public String[] availableStatuses = new String[]{}; + + // Selected IDs + public Long selectedCustomerId = null; + public Long selectedStoreId = null; + public Long selectedPetId = null; + public Long selectedServiceId = null; + public Long selectedStaffId = null; + } +} From 8fa74240bce57cc63a7c50a811c5d1ecd82c0617 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 02:07:35 -0600 Subject: [PATCH 09/33] split viewmodels for appointments --- .../AppointmentDetailFragment.java | 146 +++---- .../petstoremobile/utils/DateTimeUtils.java | 17 +- .../petstoremobile/utils/SpinnerUtils.java | 40 ++ .../example/petstoremobile/utils/UIUtils.java | 48 ++- .../AppointmentDetailViewModel.java | 402 ++++++++++++++++++ .../viewmodels/AppointmentViewModel.java | 338 +-------------- 6 files changed, 566 insertions(+), 425 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java 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 96001734..d43304c0 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 @@ -1,10 +1,12 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; -import android.app.DatePickerDialog; import android.os.Bundle; -import android.util.Log; -import android.view.*; -import android.widget.*; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; @@ -12,15 +14,15 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.databinding.FragmentAppointmentDetailBinding; -import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.DropdownDTO; +import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.AppointmentViewModel; - -import java.util.*; +import com.example.petstoremobile.viewmodels.AppointmentDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -38,24 +40,33 @@ public class AppointmentDetailFragment extends Fragment { private long preselectedStoreId = -1; private long preselectedStaffId = -1; - private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; - private final Integer[] MINUTES = {0,15,30,45}; + private final Integer[] HOURS = {9, 10, 11, 12, 13, 14, 15, 16, 17}; + private final Integer[] MINUTES = {0, 15, 30, 45}; - private AppointmentViewModel appointmentViewModel; + private AppointmentDetailViewModel appointmentViewModel; private boolean isUpdatingUI = false; + /** + * Called when the fragment is first created. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); + appointmentViewModel = new ViewModelProvider(this).get(AppointmentDetailViewModel.class); } + /** + * Creates and returns the view hierarchy with the fragment. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentAppointmentDetailBinding.inflate(inflater, container, false); return binding.getRoot(); } + /** + * Called immediately after onCreateView has returned. + */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -70,6 +81,9 @@ public class AppointmentDetailFragment extends Fragment { binding.btnDeleteAppointment.setOnClickListener(v -> confirmDelete()); } + /** + * Called when the view previously created by onCreateView has been detached. + */ @Override public void onDestroyView() { super.onDestroyView(); @@ -77,7 +91,7 @@ public class AppointmentDetailFragment extends Fragment { } /** - * Configures the adapters for spinners. + * Configures the adapters and listeners for all spinners. */ private void setupSpinners() { //Status Spinner is empty by default the date determines whats in here @@ -86,9 +100,9 @@ public class AppointmentDetailFragment extends Fragment { // Set up hour and minute spinners String[] hours = new String[HOURS.length]; for (int i = 0; i < HOURS.length; i++) - hours[i] = String.format("%02d:00", HOURS[i]); + hours[i] = DateTimeUtils.formatTime(HOURS[i], 0); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours); - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00","15","30","45"}); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00", "15", "30", "45"}); // Pet and Staff spinners disabled by until parent selection UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff); @@ -114,42 +128,24 @@ public class AppointmentDetailFragment extends Fragment { }); // Listeners for other selections - binding.spinnerService.setOnItemSelectedListener(new OnIndexSelected(p -> appointmentViewModel.onServiceSelected(p))); - binding.spinnerPet.setOnItemSelectedListener(new OnIndexSelected(p -> appointmentViewModel.onPetSelected(p))); - binding.spinnerStaff.setOnItemSelectedListener(new OnIndexSelected(p -> appointmentViewModel.onStaffSelected(p))); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerService, p -> appointmentViewModel.onServiceSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPet, p -> appointmentViewModel.onPetSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerStaff, p -> appointmentViewModel.onStaffSelected(p)); // Listeners for time changes - binding.spinnerHour.setOnItemSelectedListener(new OnIndexSelected(p -> notifyDateTimeStatusChange())); - binding.spinnerMinute.setOnItemSelectedListener(new OnIndexSelected(p -> notifyDateTimeStatusChange())); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerHour, p -> notifyDateTimeStatusChange()); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerMinute, p -> notifyDateTimeStatusChange()); // Listener to notify ViewModel of status selection - binding.spinnerAppointmentStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - notifyDateTimeStatusChange(); - } - @Override - public void onNothingSelected(AdapterView parent) {} - }); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAppointmentStatus, p -> notifyDateTimeStatusChange()); } /** * Configures the date picker dialog for the appointment date field. */ private void setupDatePicker() { - binding.etAppointmentDate.setOnClickListener(v -> { - Calendar c = Calendar.getInstance(); - DatePickerDialog d = new DatePickerDialog(requireContext(), - (dp,y,m,d1) -> { - String selectedDate = String.format("%04d-%02d-%02d", y, m+1, d1); - binding.etAppointmentDate.setText(selectedDate); - notifyDateTimeStatusChange(); - }, - c.get(Calendar.YEAR), c.get(Calendar.MONTH), - c.get(Calendar.DAY_OF_MONTH)); - d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000); - d.show(); - }); + binding.etAppointmentDate.setOnClickListener(v -> + UIUtils.showDatePicker(requireContext(), binding.etAppointmentDate, this::notifyDateTimeStatusChange)); } /** @@ -178,37 +174,27 @@ public class AppointmentDetailFragment extends Fragment { /** * Applies the ViewState provided by the ViewModel to the UI components. */ - private void applyViewState(AppointmentViewModel.ViewState state) { + private void applyViewState(AppointmentDetailViewModel.ViewState state) { isUpdatingUI = true; // Mode specific UI binding.tvApptMode.setText(state.isEditing ? "Edit Appointment" : "Add Appointment"); - binding.tvAppointmentId.setText("ID: " + appointmentViewModel.getAppointmentId()); + binding.tvAppointmentId.setText(DateTimeUtils.formatId(appointmentViewModel.getAppointmentId())); binding.tvAppointmentId.setVisibility(state.isEditing ? View.VISIBLE : View.GONE); binding.btnDeleteAppointment.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); binding.btnSaveAppointment.setVisibility(state.isSaveVisible ? View.VISIBLE : View.GONE); - // Enabling/Disabling Views - UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer); - UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore); - UIUtils.setViewsEnabled(state.isPetEnabled, binding.spinnerPet); - UIUtils.setViewsEnabled(state.isServiceEnabled, binding.spinnerService); - UIUtils.setViewsEnabled(state.isStaffEnabled, binding.spinnerStaff); - UIUtils.setViewsEnabled(state.isDateEnabled, binding.etAppointmentDate); - UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerHour, binding.spinnerMinute); + // Enabling/Disabling Views and Labels + UIUtils.setFieldEnabled(state.isCustomerEnabled, binding.spinnerCustomer, binding.tvLabelCustomer); + UIUtils.setFieldEnabled(state.isStoreEnabled, binding.spinnerStore, binding.tvLabelStore); + UIUtils.setFieldEnabled(state.isPetEnabled, binding.spinnerPet, binding.tvLabelPet); + UIUtils.setFieldEnabled(state.isServiceEnabled, binding.spinnerService, binding.tvLabelService); + UIUtils.setFieldEnabled(state.isStaffEnabled, binding.spinnerStaff, binding.tvLabelStaff); + UIUtils.setFieldEnabled(state.isDateEnabled, binding.etAppointmentDate, binding.tvLabelDate); + UIUtils.setFieldEnabled(state.isTimeEnabled, binding.spinnerHour, binding.tvLabelTime); + UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerMinute); UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAppointmentStatus); - // Alpha for disabled look - float alpha = 1.0f; - float disabledAlpha = 0.5f; - UIUtils.setViewsAlpha(state.isCustomerEnabled ? alpha : disabledAlpha, binding.tvLabelCustomer); - UIUtils.setViewsAlpha(state.isStoreEnabled ? alpha : disabledAlpha, binding.tvLabelStore); - UIUtils.setViewsAlpha(state.isPetEnabled ? alpha : disabledAlpha, binding.tvLabelPet); - UIUtils.setViewsAlpha(state.isServiceEnabled ? alpha : disabledAlpha, binding.tvLabelService); - UIUtils.setViewsAlpha(state.isStaffEnabled ? alpha : disabledAlpha, binding.tvLabelStaff); - UIUtils.setViewsAlpha(state.isDateEnabled ? alpha : disabledAlpha, binding.tvLabelDate); - UIUtils.setViewsAlpha(state.isTimeEnabled ? alpha : disabledAlpha, binding.tvLabelTime); - // Update status options Object selected = binding.spinnerAppointmentStatus.getSelectedItem(); String current = selected != null ? selected.toString() : ""; @@ -218,6 +204,9 @@ public class AppointmentDetailFragment extends Fragment { isUpdatingUI = false; } + /** + * Notifies the ViewModel that the date, time, or status has changed. + */ private void notifyDateTimeStatusChange() { if (isUpdatingUI) return; @@ -282,7 +271,7 @@ public class AppointmentDetailFragment extends Fragment { appointmentViewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - AppointmentViewModel.ViewState state = appointmentViewModel.getViewState().getValue(); + AppointmentDetailViewModel.ViewState state = appointmentViewModel.getViewState().getValue(); String message = (state != null && state.isEditing) ? "Updated" : "Saved"; Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); navigateBack(); @@ -296,24 +285,19 @@ public class AppointmentDetailFragment extends Fragment { * Validates that all required fields are selected. */ private boolean validateRequiredFields() { - if (binding.spinnerCustomer.getSelectedItemPosition() == 0) return showToast("Select a customer"); - if (binding.spinnerStore.getSelectedItemPosition() == 0) return showToast("Select a store"); - if (binding.spinnerPet.getSelectedItemPosition() == 0) return showToast("Select a pet"); - if (binding.spinnerService.getSelectedItemPosition() == 0) return showToast("Select a service"); - if (binding.etAppointmentDate.getText().toString().trim().isEmpty()) return showToast("Select a date"); + if (binding.spinnerCustomer.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a customer"); + if (binding.spinnerStore.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a store"); + if (binding.spinnerPet.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a pet"); + if (binding.spinnerService.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a service"); + if (binding.etAppointmentDate.getText().toString().trim().isEmpty()) return UIUtils.showToast(getContext(), "Select a date"); return true; } - private boolean showToast(String msg) { - Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); - return false; - } - /** * Builds a time string from the hour and minute spinners. */ private String buildTimeString() { - return String.format("%02d:%02d", HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); + return DateTimeUtils.formatTime(HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); } /** @@ -358,17 +342,7 @@ public class AppointmentDetailFragment extends Fragment { private void parseAndSetTimeSpinners(String time) { int[] parsedTime = DateTimeUtils.parseTimeString(time); if (parsedTime == null) return; - for (int i = 0; i < HOURS.length; i++) if (HOURS[i] == parsedTime[0]) binding.spinnerHour.setSelection(i); - for (int i = 0; i < MINUTES.length; i++) if (MINUTES[i] == parsedTime[1]) binding.spinnerMinute.setSelection(i); - } - - /** - * Helper listener for simple index reporting. - */ - private static class OnIndexSelected implements AdapterView.OnItemSelectedListener { - private final java.util.function.Consumer callback; - public OnIndexSelected(java.util.function.Consumer callback) { this.callback = callback; } - @Override public void onItemSelected(AdapterView p, View v, int pos, long id) { callback.accept(pos); } - @Override public void onNothingSelected(AdapterView p) {} + SpinnerUtils.setSelectionByValueArray(binding.spinnerHour, HOURS, parsedTime[0]); + SpinnerUtils.setSelectionByValueArray(binding.spinnerMinute, MINUTES, parsedTime[1]); } } 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 4e79a190..11e8f4fd 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 @@ -3,6 +3,7 @@ package com.example.petstoremobile.utils; import android.util.Log; import java.util.Calendar; +import java.util.Locale; /** * Utility class for date and time operations. @@ -78,7 +79,7 @@ public class DateTimeUtils { } /** - * Parses a time string and returns hour and minute indices for the spinners. + * Parses a time string and returns hour and minute values. */ public static int[] parseTimeString(String time) { if (time == null || time.isEmpty()) return null; @@ -95,4 +96,18 @@ public class DateTimeUtils { return null; } } + + /** + * Formats an hour and minute into an HH:mm string. + */ + public static String formatTime(int hour, int minute) { + return String.format(Locale.getDefault(), "%02d:%02d", hour, minute); + } + + /** + * Formats an ID for display (e.g., "ID: 123"). + */ + public static String formatId(long id) { + return String.format(Locale.getDefault(), "ID: %d", id); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java index b0aae8b8..9c5385c7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java @@ -12,6 +12,7 @@ import com.example.petstoremobile.adapters.WhiteTextArrayAdapter; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Function; /** @@ -96,6 +97,13 @@ public class SpinnerUtils { }); } + /** + * Sets a listener that provides the selected index to a consumer. + */ + public static void setOnIndexSelectedListener(Spinner spinner, Consumer callback) { + spinner.setOnItemSelectedListener(new OnIndexSelected(callback)); + } + /** * Sets the selection of a spinner based on a string value. */ @@ -108,6 +116,19 @@ public class SpinnerUtils { } } + /** + * Sets the selection of a spinner based on a value within an array. + */ + public static void setSelectionByValueArray(Spinner spinner, T[] array, T value) { + if (spinner == null || array == null || value == null) return; + for (int i = 0; i < array.length; i++) { + if (Objects.equals(array[i], value)) { + spinner.setSelection(i); + return; + } + } + } + /** * Configures a simple string array spinner. */ @@ -117,4 +138,23 @@ public class SpinnerUtils { adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); } + + /** + * Helper listener to get selected index from a spinner. + */ + public static class OnIndexSelected implements AdapterView.OnItemSelectedListener { + private final Consumer callback; + + public OnIndexSelected(Consumer callback) { + this.callback = callback; + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + callback.accept(position); + } + + @Override + public void onNothingSelected(AdapterView parent) {} + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java index 40f6c92d..8a70621b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java @@ -1,5 +1,7 @@ package com.example.petstoremobile.utils; +import android.app.DatePickerDialog; +import android.content.Context; import android.telephony.PhoneNumberFormattingTextWatcher; import android.text.Editable; import android.text.InputFilter; @@ -8,10 +10,14 @@ import android.view.View; import android.widget.EditText; import android.widget.ImageButton; import android.widget.Spinner; +import android.widget.Toast; import androidx.fragment.app.Fragment; import com.example.petstoremobile.fragments.ListFragment; +import java.util.Calendar; +import java.util.Locale; + /** * Utility class for shared UI component logic and formatting. */ @@ -32,7 +38,6 @@ public class UIUtils { boolean isVisible = layoutFilter.getVisibility() == View.VISIBLE; layoutFilter.setVisibility(isVisible ? View.GONE : View.VISIBLE); - // Use Android default icons or app-specific ones if available btnToggle.setImageResource(isVisible ? android.R.drawable.ic_menu_search : android.R.drawable.ic_menu_close_clear_cancel); @@ -69,7 +74,6 @@ public class UIUtils { /** * Sets the enabled state and alpha for multiple views. - * Alpha is set to 1.0f when enabled and 0.5f when disabled. */ public static void setViewsEnabled(boolean enabled, View... views) { for (View v : views) { @@ -80,6 +84,19 @@ public class UIUtils { } } + /** + * Sets enabled state for a field and updates alpha for both the field and its label. + */ + public static void setFieldEnabled(boolean enabled, View field, View label) { + if (field != null) { + field.setEnabled(enabled); + field.setAlpha(enabled ? 1.0f : 0.5f); + } + if (label != null) { + label.setAlpha(enabled ? 1.0f : 0.5f); + } + } + /** * Sets the alpha for multiple views. */ @@ -90,4 +107,31 @@ public class UIUtils { } } } + + /** + * Displays a DatePickerDialog and sets the result to an EditText. + */ + public static void showDatePicker(Context context, EditText editText, Runnable onDateSet) { + Calendar c = Calendar.getInstance(); + DatePickerDialog d = new DatePickerDialog(context, + (dp, y, m, d1) -> { + String selectedDate = String.format(Locale.getDefault(), "%04d-%02d-%02d", y, m + 1, d1); + editText.setText(selectedDate); + if (onDateSet != null) onDateSet.run(); + }, + c.get(Calendar.YEAR), c.get(Calendar.MONTH), + c.get(Calendar.DAY_OF_MONTH)); + d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000); + d.show(); + } + + /** + * Displays a toast and returns false. Useful for validation chains. + */ + public static boolean showToast(Context context, String msg) { + if (context != null) { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + } + return false; + } } 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 new file mode 100644 index 00000000..685b645a --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java @@ -0,0 +1,402 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.DropdownDTO; +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.repositories.AppointmentRepository; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.repositories.ServiceRepository; +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.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +/** + * ViewModel for managing appointment details and form state. + */ +@HiltViewModel +public class AppointmentDetailViewModel extends ViewModel { + private final AppointmentRepository repository; + private final CustomerRepository customerRepository; + private final StoreRepository storeRepository; + private final PetRepository petRepository; + private final ServiceRepository serviceRepository; + + private final MutableLiveData> customers = new MutableLiveData<>(); + private final MutableLiveData> stores = new MutableLiveData<>(); + private final MutableLiveData> services = new MutableLiveData<>(); + private final MutableLiveData> customerPets = new MutableLiveData<>(); + private final MutableLiveData> storeEmployees = new MutableLiveData<>(); + private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); + + private long appointmentId = -1; + private Long currentCustomerId; + private Long currentStoreId; + private Long currentPetId; + private Long currentServiceId; + private Long currentStaffId; + + /** + * Constructor for AppointmentDetailViewModel. + */ + @Inject + public AppointmentDetailViewModel( + AppointmentRepository repository, + CustomerRepository customerRepository, + StoreRepository storeRepository, + PetRepository petRepository, + ServiceRepository serviceRepository) { + this.repository = repository; + this.customerRepository = customerRepository; + this.storeRepository = storeRepository; + this.petRepository = petRepository; + this.serviceRepository = serviceRepository; + } + + // Initial Data Loading + + /** + * Loads initial dropdown data for customers, stores, and services. + */ + public void loadInitialFormData() { + customerRepository.getCustomerDropdowns().observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) customers.setValue(r.data); + }); + storeRepository.getStoreDropdowns().observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) stores.setValue(r.data); + }); + serviceRepository.getAllServices(0, 200, null, "serviceName").observeForever(r -> { + if (r.status == Resource.Status.SUCCESS && r.data != null) services.setValue(r.data.getContent()); + }); + } + + // LiveData Getters + + /** + * Returns the LiveData for the list of customers. + */ + public LiveData> getCustomers() { return customers; } + + /** + * Returns the LiveData for the list of stores. + */ + public LiveData> getStores() { return stores; } + + /** + * Returns the LiveData for the list of services. + */ + public LiveData> getServices() { return services; } + + /** + * Returns the LiveData for the list of pets for the current customer. + */ + public LiveData> getCustomerPets() { return customerPets; } + + /** + * Returns the LiveData for the list of employees for the current store. + */ + public LiveData> getStoreEmployees() { return storeEmployees; } + + /** + * Returns the LiveData for the view state. + */ + public LiveData getViewState() { return viewState; } + + //State Getters + + /** + * Returns the current appointment ID. + */ + public long getAppointmentId() { return appointmentId; } + + /** + * Sets the current appointment ID and updates the mode. + */ + public void setAppointmentId(long id) { + this.appointmentId = id; + initMode(id != -1); + } + + // Selection Handlers for spinners + + /** + * Handles customer selection and loads their pets. + */ + public void onCustomerSelected(int position) { + List list = customers.getValue(); + if (position > 0 && list != null && position <= list.size()) { + currentCustomerId = list.get(position - 1).getId(); + loadPetsForCustomer(currentCustomerId); + updateViewState(s -> { + s.selectedCustomerId = currentCustomerId; + s.isPetEnabled = !s.isEditing; + }); + } else { + currentCustomerId = null; + customerPets.setValue(new ArrayList<>()); + updateViewState(s -> { + s.selectedCustomerId = null; + s.isPetEnabled = false; + }); + } + } + + /** + * Handles store selection and loads its employees. + */ + public void onStoreSelected(int position) { + List list = stores.getValue(); + if (position > 0 && list != null && position <= list.size()) { + currentStoreId = list.get(position - 1).getId(); + loadEmployeesForStore(currentStoreId); + updateViewState(s -> { + s.selectedStoreId = currentStoreId; + s.isStaffEnabled = !s.isPast; + }); + } else { + currentStoreId = null; + storeEmployees.setValue(new ArrayList<>()); + updateViewState(s -> { + s.selectedStoreId = null; + s.isStaffEnabled = false; + }); + } + } + + /** + * Handles service selection. + */ + public void onServiceSelected(int position) { + List list = services.getValue(); + currentServiceId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getServiceId() : null; + updateViewState(s -> s.selectedServiceId = currentServiceId); + } + + /** + * Handles pet selection. + */ + public void onPetSelected(int position) { + List list = customerPets.getValue(); + currentPetId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; + updateViewState(s -> s.selectedPetId = currentPetId); + } + + /** + * Handles staff selection. + */ + public void onStaffSelected(int position) { + List list = storeEmployees.getValue(); + currentStaffId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; + updateViewState(s -> s.selectedStaffId = currentStaffId); + } + + /** + * Loads the list of pets for a specific customer. + */ + private void loadPetsForCustomer(Long customerId) { + petRepository.getCustomerPets(customerId).observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) customerPets.setValue(r.data); + }); + } + + /** + * Loads the list of employees for a specific store. + */ + private void loadEmployeesForStore(Long storeId) { + storeRepository.getStoreEmployees(storeId).observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data); + }); + } + + // Appointment Detail CRUD + + /** + * Fetches appointment details and populates internal state. + */ + public LiveData> loadAppointment() { + MutableLiveData> result = new MutableLiveData<>(); + repository.getAppointmentById(appointmentId).observeForever(resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + AppointmentDTO a = resource.data; + currentCustomerId = a.getCustomerId(); + currentStoreId = a.getStoreId(); + currentPetId = a.getPetId(); + currentServiceId = a.getServiceId(); + currentStaffId = a.getEmployeeId(); + + updateViewState(s -> { + s.selectedCustomerId = currentCustomerId; + s.selectedStoreId = currentStoreId; + s.selectedPetId = currentPetId; + s.selectedServiceId = currentServiceId; + s.selectedStaffId = currentStaffId; + }); + + if (currentCustomerId != null) loadPetsForCustomer(currentCustomerId); + if (currentStoreId != null) loadEmployeesForStore(currentStoreId); + } + result.setValue(resource); + }); + return result; + } + + /** + * Saves the appointment by building the DTO from tracked state. + */ + public LiveData> saveAppointment(String date, String time, String status) { + AppointmentDTO dto = new AppointmentDTO(currentCustomerId, currentStoreId, currentServiceId, currentStaffId, date, time, status, currentPetId); + if (appointmentId != -1) { + return repository.updateAppointment(appointmentId, dto); + } else { + return repository.createAppointment(dto); + } + } + + /** + * Deletes the current appointment. + */ + public LiveData> deleteAppointment() { + return repository.deleteAppointment(appointmentId); + } + + // UI Logic + + /** + * Updates the UI state when date, time, or status changes. + */ + public void onDateOrTimeChanged(String date, String time, String currentStatus) { + updateViewState(s -> { + s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus); + boolean isPast = DateTimeUtils.isDateTimeInPast(date, time); + boolean isCancelled = "Cancelled".equalsIgnoreCase(currentStatus); + + if (isCancelled) { + s.isPast = true; + setAllFieldsEnabled(s, false); + s.isStatusEnabled = false; + s.isSaveVisible = false; + } else if (isPast) { + s.isPast = true; + setAllFieldsEnabled(s, false); + s.isStatusEnabled = true; + } else { + s.isPast = false; + if (!s.isEditing) { + s.isCustomerEnabled = true; + s.isStoreEnabled = true; + s.isServiceEnabled = true; + s.isPetEnabled = currentCustomerId != null; + } + s.isDateEnabled = true; + s.isTimeEnabled = true; + s.isStatusEnabled = true; + } + }); + } + + /** + * Calculates available appointment statuses based on the current context. + */ + private String[] calculateAvailableStatuses(boolean isEditing, String date, String currentTime, String currentStatus) { + if (!isEditing) return new String[]{"Booked"}; + if (date == null || date.isEmpty()) return new String[]{}; + if ("Cancelled".equalsIgnoreCase(currentStatus)) return new String[]{"Cancelled"}; + if (DateTimeUtils.isDateTimeInPast(date, currentTime)) return new String[]{"Completed", "Missed"}; + return new String[]{"Booked", "Cancelled"}; + } + + /** + * Helper method to enable or disable all fields. + */ + private void setAllFieldsEnabled(ViewState s, boolean enabled) { + s.isCustomerEnabled = enabled; + s.isStoreEnabled = enabled; + s.isPetEnabled = enabled; + s.isServiceEnabled = enabled; + s.isStaffEnabled = enabled; + s.isDateEnabled = enabled; + s.isTimeEnabled = enabled; + } + + /** + * Initializes the UI mode (Create vs Edit). + */ + public void initMode(boolean isEditing) { + updateViewState(s -> { + s.isEditing = isEditing; + s.isDeleteVisible = isEditing; + if (isEditing) { + s.isCustomerEnabled = false; + s.isStoreEnabled = false; + s.isPetEnabled = false; + s.isServiceEnabled = false; + } else { + s.isCustomerEnabled = true; + s.isStoreEnabled = true; + s.isServiceEnabled = true; + s.isPetEnabled = false; // until customer selected + s.isStaffEnabled = false; // until store selected + s.availableStatuses = new String[]{"Booked"}; + } + }); + } + + /** + * Validates if a booking is in the future. + */ + public boolean isValidFutureBooking(String status, String date, String time) { + return !"BOOKED".equalsIgnoreCase(status) || !DateTimeUtils.isDateTimeInPast(date, time); + } + + /** + * Helper to update the view state and notify observers. + */ + private void updateViewState(Action action) { + ViewState current = viewState.getValue(); + if (current != null) { + action.run(current); + viewState.setValue(current); + } + } + + private interface Action { + void run(T t); + } + + /** + * A Class to show the states of Appointment Detail Fragment. + */ + public static class ViewState { + public boolean isPast = false; + public boolean isEditing = false; + public boolean isSaveVisible = true; + public boolean isDeleteVisible = false; + public boolean isCustomerEnabled = true; + public boolean isStoreEnabled = true; + public boolean isPetEnabled = false; + public boolean isServiceEnabled = true; + public boolean isStaffEnabled = false; + public boolean isDateEnabled = true; + public boolean isTimeEnabled = true; + public boolean isStatusEnabled = true; + public String[] availableStatuses = new String[]{}; + + // Selected IDs + public Long selectedCustomerId = null; + public Long selectedStoreId = null; + public Long selectedPetId = null; + public Long selectedServiceId = null; + public Long selectedStaffId = null; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java index 32f359e7..5605a8d1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java @@ -1,23 +1,14 @@ package com.example.petstoremobile.viewmodels; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.ServiceDTO; -import com.example.petstoremobile.repositories.AppointmentRepository; -import com.example.petstoremobile.repositories.CustomerRepository; -import com.example.petstoremobile.repositories.PetRepository; -import com.example.petstoremobile.repositories.ServiceRepository; -import com.example.petstoremobile.repositories.StoreRepository; -import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.repositories.AppointmentRepository; -import java.util.ArrayList; import java.util.List; import javax.inject.Inject; @@ -27,38 +18,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class AppointmentViewModel extends ViewModel { private final AppointmentRepository repository; - private final CustomerRepository customerRepository; - private final StoreRepository storeRepository; - private final PetRepository petRepository; - private final ServiceRepository serviceRepository; - - private final MutableLiveData> customers = new MutableLiveData<>(); - private final MutableLiveData> stores = new MutableLiveData<>(); - private final MutableLiveData> services = new MutableLiveData<>(); - private final MutableLiveData> customerPets = new MutableLiveData<>(); - private final MutableLiveData> storeEmployees = new MutableLiveData<>(); - - private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); - - private long appointmentId = -1; - private Long currentCustomerId; - private Long currentStoreId; - private Long currentPetId; - private Long currentServiceId; - private Long currentStaffId; @Inject - public AppointmentViewModel( - AppointmentRepository repository, - CustomerRepository customerRepository, - StoreRepository storeRepository, - PetRepository petRepository, - ServiceRepository serviceRepository) { + public AppointmentViewModel(AppointmentRepository repository) { this.repository = repository; - this.customerRepository = customerRepository; - this.storeRepository = storeRepository; - this.petRepository = petRepository; - this.serviceRepository = serviceRepository; } // API CRUD @@ -70,307 +33,10 @@ public class AppointmentViewModel extends ViewModel { return repository.getAllAppointments(page, size, query, status, storeId, date, employeeId); } - /** - * Retrieves a single appointment by its ID. - */ - public LiveData> getAppointmentById(Long id) { - return repository.getAppointmentById(id); - } - - /** - * Creates a new appointment. - */ - public LiveData> createAppointment(AppointmentDTO appointment) { - return repository.createAppointment(appointment); - } - - /** - * Updates an existing appointment record by ID. - */ - public LiveData> updateAppointment(Long id, AppointmentDTO appointment) { - return repository.updateAppointment(id, appointment); - } - - /** - * Deletes an appointment record by ID. - */ - public LiveData> deleteAppointment(Long id) { - return repository.deleteAppointment(id); - } - /** * Deletes multiple appointment records. */ public LiveData> bulkDeleteAppointments(List ids) { return repository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); } - - // Initial Data Loading - - /** - * Loads initial dropdown data for customers, stores, and services. - */ - public void loadInitialFormData() { - customerRepository.getCustomerDropdowns().observeForever(r -> { - if (r.status == Resource.Status.SUCCESS) customers.setValue(r.data); - }); - storeRepository.getStoreDropdowns().observeForever(r -> { - if (r.status == Resource.Status.SUCCESS) stores.setValue(r.data); - }); - serviceRepository.getAllServices(0, 200, null, "serviceName").observeForever(r -> { - if (r.status == Resource.Status.SUCCESS && r.data != null) services.setValue(r.data.getContent()); - }); - } - - // LiveData Getters - - public LiveData> getCustomers() { return customers; } - public LiveData> getStores() { return stores; } - public LiveData> getServices() { return services; } - public LiveData> getCustomerPets() { return customerPets; } - public LiveData> getStoreEmployees() { return storeEmployees; } - public LiveData getViewState() { return viewState; } - - //State Getters - - public long getAppointmentId() { return appointmentId; } - - /** - * Sets the current appointment ID and updates the mode. - */ - public void setAppointmentId(long id) { - this.appointmentId = id; - initMode(id != -1); - } - - // Selection Handlers for spinners - - public void onCustomerSelected(int position) { - List list = customers.getValue(); - if (position > 0 && list != null && position <= list.size()) { - currentCustomerId = list.get(position - 1).getId(); - loadPetsForCustomer(currentCustomerId); - updateViewState(s -> { - s.selectedCustomerId = currentCustomerId; - s.isPetEnabled = !s.isEditing; - }); - } else { - currentCustomerId = null; - customerPets.setValue(new ArrayList<>()); - updateViewState(s -> { - s.selectedCustomerId = null; - s.isPetEnabled = false; - }); - } - } - - public void onStoreSelected(int position) { - List list = stores.getValue(); - if (position > 0 && list != null && position <= list.size()) { - currentStoreId = list.get(position - 1).getId(); - loadEmployeesForStore(currentStoreId); - updateViewState(s -> { - s.selectedStoreId = currentStoreId; - s.isStaffEnabled = !s.isPast; - }); - } else { - currentStoreId = null; - storeEmployees.setValue(new ArrayList<>()); - updateViewState(s -> { - s.selectedStoreId = null; - s.isStaffEnabled = false; - }); - } - } - - public void onServiceSelected(int position) { - List list = services.getValue(); - currentServiceId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getServiceId() : null; - updateViewState(s -> s.selectedServiceId = currentServiceId); - } - - public void onPetSelected(int position) { - List list = customerPets.getValue(); - currentPetId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; - updateViewState(s -> s.selectedPetId = currentPetId); - } - - public void onStaffSelected(int position) { - List list = storeEmployees.getValue(); - currentStaffId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; - updateViewState(s -> s.selectedStaffId = currentStaffId); - } - - private void loadPetsForCustomer(Long customerId) { - petRepository.getCustomerPets(customerId).observeForever(r -> { - if (r.status == Resource.Status.SUCCESS) customerPets.setValue(r.data); - }); - } - - private void loadEmployeesForStore(Long storeId) { - storeRepository.getStoreEmployees(storeId).observeForever(r -> { - if (r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data); - }); - } - - // Appointment Detail CRUD - - /** - * Fetches appointment details and populates internal state. - */ - public LiveData> loadAppointment() { - MutableLiveData> result = new MutableLiveData<>(); - repository.getAppointmentById(appointmentId).observeForever(resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - AppointmentDTO a = resource.data; - currentCustomerId = a.getCustomerId(); - currentStoreId = a.getStoreId(); - currentPetId = a.getPetId(); - currentServiceId = a.getServiceId(); - currentStaffId = a.getEmployeeId(); - - updateViewState(s -> { - s.selectedCustomerId = currentCustomerId; - s.selectedStoreId = currentStoreId; - s.selectedPetId = currentPetId; - s.selectedServiceId = currentServiceId; - s.selectedStaffId = currentStaffId; - }); - - if (currentCustomerId != null) loadPetsForCustomer(currentCustomerId); - if (currentStoreId != null) loadEmployeesForStore(currentStoreId); - } - result.setValue(resource); - }); - return result; - } - - /** - * Saves the appointment by building the DTO from tracked state. - */ - public LiveData> saveAppointment(String date, String time, String status) { - AppointmentDTO dto = new AppointmentDTO(currentCustomerId, currentStoreId, currentServiceId, currentStaffId, date, time, status, currentPetId); - if (appointmentId != -1) { - return repository.updateAppointment(appointmentId, dto); - } else { - return repository.createAppointment(dto); - } - } - - /** - * Deletes the current appointment. - */ - public LiveData> deleteAppointment() { - return repository.deleteAppointment(appointmentId); - } - - // --- UI Logic --- - - public void onDateOrTimeChanged(String date, String time, String currentStatus) { - updateViewState(s -> { - s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus); - boolean isPast = DateTimeUtils.isDateTimeInPast(date, time); - boolean isCancelled = "Cancelled".equalsIgnoreCase(currentStatus); - - if (isCancelled) { - s.isPast = true; - setAllFieldsEnabled(s, false); - s.isStatusEnabled = false; - s.isSaveVisible = false; - } else if (isPast) { - s.isPast = true; - setAllFieldsEnabled(s, false); - s.isStatusEnabled = true; - } else { - s.isPast = false; - if (!s.isEditing) { - s.isCustomerEnabled = true; - s.isStoreEnabled = true; - s.isServiceEnabled = true; - s.isPetEnabled = currentCustomerId != null; - } - s.isDateEnabled = true; - s.isTimeEnabled = true; - s.isStatusEnabled = true; - } - }); - } - - private String[] calculateAvailableStatuses(boolean isEditing, String date, String currentTime, String currentStatus) { - if (!isEditing) return new String[]{"Booked"}; - if (date == null || date.isEmpty()) return new String[]{}; - if ("Cancelled".equalsIgnoreCase(currentStatus)) return new String[]{"Cancelled"}; - if (DateTimeUtils.isDateTimeInPast(date, currentTime)) return new String[]{"Completed", "Missed"}; - return new String[]{"Booked", "Cancelled"}; - } - - private void setAllFieldsEnabled(ViewState s, boolean enabled) { - s.isCustomerEnabled = enabled; - s.isStoreEnabled = enabled; - s.isPetEnabled = enabled; - s.isServiceEnabled = enabled; - s.isStaffEnabled = enabled; - s.isDateEnabled = enabled; - s.isTimeEnabled = enabled; - } - - public void initMode(boolean isEditing) { - updateViewState(s -> { - s.isEditing = isEditing; - s.isDeleteVisible = isEditing; - if (isEditing) { - s.isCustomerEnabled = false; - s.isStoreEnabled = false; - s.isPetEnabled = false; - s.isServiceEnabled = false; - } else { - s.isCustomerEnabled = true; - s.isStoreEnabled = true; - s.isServiceEnabled = true; - s.isPetEnabled = false; // until customer selected - s.isStaffEnabled = false; // until store selected - s.availableStatuses = new String[]{"Booked"}; - } - }); - } - - public boolean isValidFutureBooking(String status, String date, String time) { - return !"BOOKED".equalsIgnoreCase(status) || !DateTimeUtils.isDateTimeInPast(date, time); - } - - private void updateViewState(Action action) { - ViewState current = viewState.getValue(); - if (current != null) { - action.run(current); - viewState.setValue(current); - } - } - - private interface Action { void run(T t); } - - /** - * A Class to show the states of Appointment Detail Fragment. - */ - public static class ViewState { - public boolean isPast = false; - public boolean isEditing = false; - public boolean isSaveVisible = true; - public boolean isDeleteVisible = false; - public boolean isCustomerEnabled = true; - public boolean isStoreEnabled = true; - public boolean isPetEnabled = false; - public boolean isServiceEnabled = true; - public boolean isStaffEnabled = false; - public boolean isDateEnabled = true; - public boolean isTimeEnabled = true; - public boolean isStatusEnabled = true; - public String[] availableStatuses = new String[]{}; - - // Selected IDs - public Long selectedCustomerId = null; - public Long selectedStoreId = null; - public Long selectedPetId = null; - public Long selectedServiceId = null; - public Long selectedStaffId = null; - } } From 4b8e0b2868bf24268eca3564d85f894f41119dc8 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 02:48:55 -0600 Subject: [PATCH 10/33] fixed spinner infinite loop in appointments --- .../AppointmentDetailFragment.java | 2 +- .../petstoremobile/utils/SpinnerUtils.java | 55 ++++++++++++++++--- .../example/petstoremobile/utils/UIUtils.java | 31 ++++++++--- 3 files changed, 69 insertions(+), 19 deletions(-) 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 d43304c0..1d280270 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 @@ -176,7 +176,7 @@ public class AppointmentDetailFragment extends Fragment { */ private void applyViewState(AppointmentDetailViewModel.ViewState state) { isUpdatingUI = true; - + // Mode specific UI binding.tvApptMode.setText(state.isEditing ? "Edit Appointment" : "Add Appointment"); binding.tvAppointmentId.setText(DateTimeUtils.formatId(appointmentViewModel.getAppointmentId())); diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java index 9c5385c7..de1e6304 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java @@ -10,6 +10,7 @@ import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.WhiteTextArrayAdapter; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -51,6 +52,12 @@ public class SpinnerUtils { names.add(nameExtractor.apply(item)); } + // Only update adapter if contents changed to remove infinite loop when spinner is opened + if (isAdapterDataSame(spinner, names)) { + setSelectedId(spinner, data, defaultText, preselectedId, idExtractor); + return; + } + ArrayAdapter adapter; if (useWhiteText) { adapter = new WhiteTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, names); @@ -61,26 +68,41 @@ public class SpinnerUtils { adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); + setSelectedId(spinner, data, defaultText, preselectedId, idExtractor); + } + + private static void setSelectedId(Spinner spinner, List data, String defaultText, Long preselectedId, Function idExtractor) { if (preselectedId != null && preselectedId != -1) { int offset = (defaultText != null) ? 1 : 0; for (int i = 0; i < data.size(); i++) { Long currentId = idExtractor.apply(data.get(i)); if (Objects.equals(currentId, preselectedId)) { - spinner.setSelection(i + offset); + if (spinner.getSelectedItemPosition() != i + offset) { + spinner.setSelection(i + offset); + } break; } } } } + /** + * Checks if the adapter data is the same as the new data. + */ + private static boolean isAdapterDataSame(Spinner spinner, List newNames) { + if (spinner.getAdapter() == null) return false; + if (spinner.getAdapter().getCount() != newNames.size()) return false; + for (int i = 0; i < newNames.size(); i++) { + if (!Objects.equals(spinner.getAdapter().getItem(i), newNames.get(i))) return false; + } + return true; + } + /** * Sets up a simple string spinner for filtering with a callback. */ public static void setupStringFilterSpinner(Context context, Spinner spinner, String[] items, Runnable onSelectionChanged) { - WhiteTextArrayAdapter adapter = new WhiteTextArrayAdapter<>(context, - android.R.layout.simple_spinner_item, items); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinner.setAdapter(adapter); + updateStringSpinnerIfChanged(context, spinner, items, true); setupFilterSpinner(spinner, onSelectionChanged); } @@ -111,7 +133,7 @@ public class SpinnerUtils { if (value == null || spinner.getAdapter() == null) return; ArrayAdapter adapter = (ArrayAdapter) spinner.getAdapter(); int pos = adapter.getPosition(value); - if (pos >= 0) { + if (pos >= 0 && spinner.getSelectedItemPosition() != pos) { spinner.setSelection(pos); } } @@ -123,7 +145,9 @@ public class SpinnerUtils { if (spinner == null || array == null || value == null) return; for (int i = 0; i < array.length; i++) { if (Objects.equals(array[i], value)) { - spinner.setSelection(i); + if (spinner.getSelectedItemPosition() != i) { + spinner.setSelection(i); + } return; } } @@ -133,8 +157,21 @@ public class SpinnerUtils { * Configures a simple string array spinner. */ public static void setupStringSpinner(Context context, Spinner spinner, String[] items) { - BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(context, - android.R.layout.simple_spinner_item, items); + updateStringSpinnerIfChanged(context, spinner, items, false); + } + + /** + * Updates a string spinner only if the items have changed. + */ + public static void updateStringSpinnerIfChanged(Context context, Spinner spinner, String[] items, boolean useWhiteText) { + if (isAdapterDataSame(spinner, Arrays.asList(items))) return; + + ArrayAdapter adapter; + if (useWhiteText) { + adapter = new WhiteTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, items); + } else { + adapter = new BlackTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, items); + } adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java index 8a70621b..09b98cb6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java @@ -73,36 +73,49 @@ public class UIUtils { } /** - * Sets the enabled state and alpha for multiple views. + * Sets the enabled state and alpha for multiple views, only if changed. */ public static void setViewsEnabled(boolean enabled, View... views) { for (View v : views) { if (v != null) { - v.setEnabled(enabled); - v.setAlpha(enabled ? 1.0f : 0.5f); + if (v.isEnabled() != enabled) { + v.setEnabled(enabled); + } + float targetAlpha = enabled ? 1.0f : 0.5f; + if (Math.abs(v.getAlpha() - targetAlpha) > 0.01f) { + v.setAlpha(targetAlpha); + } } } } /** - * Sets enabled state for a field and updates alpha for both the field and its label. + * Sets enabled state for a field and updates alpha for both the field and its label, only if changed. */ public static void setFieldEnabled(boolean enabled, View field, View label) { if (field != null) { - field.setEnabled(enabled); - field.setAlpha(enabled ? 1.0f : 0.5f); + if (field.isEnabled() != enabled) { + field.setEnabled(enabled); + } + float targetAlpha = enabled ? 1.0f : 0.5f; + if (Math.abs(field.getAlpha() - targetAlpha) > 0.01f) { + field.setAlpha(targetAlpha); + } } if (label != null) { - label.setAlpha(enabled ? 1.0f : 0.5f); + float targetAlpha = enabled ? 1.0f : 0.5f; + if (Math.abs(label.getAlpha() - targetAlpha) > 0.01f) { + label.setAlpha(targetAlpha); + } } } /** - * Sets the alpha for multiple views. + * Sets the alpha for multiple views, only if changed. */ public static void setViewsAlpha(float alpha, View... views) { for (View v : views) { - if (v != null) { + if (v != null && Math.abs(v.getAlpha() - alpha) > 0.01f) { v.setAlpha(alpha); } } From 43715e05a52488809367c2d9c92d5870bea000dc Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Thu, 9 Apr 2026 10:22:51 -0600 Subject: [PATCH 11/33] Unify Table Behavior --- .../controllers/AdoptionController.java | 2 ++ .../controllers/AppointmentController.java | 4 ++- .../controllers/InventoryController.java | 2 ++ .../controllers/PetController.java | 2 ++ .../controllers/ProductController.java | 2 ++ .../ProductSupplierController.java | 2 ++ .../controllers/PurchaseOrderController.java | 4 +-- .../controllers/SaleController.java | 14 ++------ .../controllers/ServiceController.java | 2 ++ .../controllers/StaffAccountsController.java | 8 ++++- .../controllers/SupplierController.java | 2 ++ .../petshopdesktop/util/TableViewSupport.java | 33 +++++++++++++++++++ 12 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java index 7b5b6a5f..d31785be 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java @@ -16,6 +16,7 @@ import org.example.petshopdesktop.api.endpoints.AdoptionApi; import org.example.petshopdesktop.controllers.dialogcontrollers.AdoptionDialogController; import org.example.petshopdesktop.models.Adoption; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TableViewSupport; import java.io.IOException; import java.util.Comparator; @@ -80,6 +81,7 @@ public class AdoptionController { colAdoptionStatus.setCellValueFactory(new PropertyValueFactory<>("adoptionStatus")); displayAdoptions(); + TableViewSupport.installDoubleClickAction(tvAdoptions, selected -> openDialog(selected, "Edit")); tvAdoptions.getSelectionModel().selectedItemProperty().addListener( (observable, oldValue, newValue) -> { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index e6eb87b5..3b19567c 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -18,6 +18,7 @@ import org.example.petshopdesktop.api.dto.appointment.AppointmentResponse; import org.example.petshopdesktop.api.endpoints.AppointmentApi; import org.example.petshopdesktop.controllers.dialogcontrollers.AppointmentDialogController; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TableViewSupport; import java.util.List; import java.util.Comparator; @@ -61,7 +62,8 @@ public class AppointmentController { colAppointmentStatus.setCellValueFactory(new PropertyValueFactory<>("appointmentStatus")); filtered = new FilteredList<>(appointments, a -> true); - tvAppointments.setItems(filtered); + TableViewSupport.bindSortedItems(tvAppointments, filtered); + TableViewSupport.installDoubleClickAction(tvAppointments, selected -> openDialog(selected, "Edit")); if (txtSearch != null) { txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java index 1153fe04..d889237d 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java @@ -16,6 +16,7 @@ import org.example.petshopdesktop.api.endpoints.InventoryApi; import org.example.petshopdesktop.controllers.dialogcontrollers.InventoryDialogController; import org.example.petshopdesktop.models.Inventory; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TableViewSupport; import java.io.IOException; import java.util.List; @@ -70,6 +71,7 @@ public class InventoryController { colQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); displayInventory(); + TableViewSupport.installDoubleClickAction(tvInventory, selected -> openDialog(selected, "Edit")); tvInventory.getSelectionModel().selectedItemProperty().addListener( (observable, oldValue, newValue) -> { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java index 3e9052e4..fbe2df24 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java @@ -22,6 +22,7 @@ import org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogControl import org.example.petshopdesktop.models.Pet; import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.DesktopImageSupport; +import org.example.petshopdesktop.util.TableViewSupport; import java.io.IOException; import java.util.List; @@ -172,6 +173,7 @@ public class PetController { cbStatusFilter.getSelectionModel().selectFirst(); displayPets(); + TableViewSupport.installDoubleClickAction(tvPets, selected -> openDialog(selected, "Edit")); tvPets.getSelectionModel().selectedItemProperty().addListener( (observable, oldValue, newValue) -> { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java index 61702f3b..c27974b2 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java @@ -22,6 +22,7 @@ import org.example.petshopdesktop.api.endpoints.ProductApi; import org.example.petshopdesktop.controllers.dialogcontrollers.ProductDialogController; import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.DesktopImageSupport; +import org.example.petshopdesktop.util.TableViewSupport; import java.io.IOException; import java.util.ArrayList; @@ -95,6 +96,7 @@ public class ProductController { loadCategoryFilter(); displayProduct(); + TableViewSupport.installDoubleClickAction(tvProducts, selected -> openDialog(selected, "Edit")); //EventListener to Enable buttons when a row is selected tvProducts.getSelectionModel().selectedItemProperty().addListener( diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductSupplierController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductSupplierController.java index 1e9b9a95..4a632a1a 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductSupplierController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductSupplierController.java @@ -16,6 +16,7 @@ import org.example.petshopdesktop.api.dto.productsupplier.ProductSupplierRespons import org.example.petshopdesktop.api.endpoints.ProductSupplierApi; import org.example.petshopdesktop.controllers.dialogcontrollers.ProductSupplierDialogController; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TableViewSupport; import java.io.IOException; import java.util.List; @@ -76,6 +77,7 @@ public class ProductSupplierController { colCost.setCellValueFactory(new PropertyValueFactory("cost")); displayProductSupplier(); + TableViewSupport.installDoubleClickAction(tvProductSuppliers, selected -> openDialog(selected, "Edit")); //EventListener to Enable buttons when a row is selected tvProductSuppliers.getSelectionModel().selectedItemProperty().addListener( diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java index d8ea54b6..de8334ab 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java @@ -11,6 +11,7 @@ import org.example.petshopdesktop.DTOs.PurchaseOrderDTO; import org.example.petshopdesktop.api.dto.purchaseorder.PurchaseOrderResponse; import org.example.petshopdesktop.api.endpoints.PurchaseOrderApi; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TableViewSupport; import java.util.List; import java.util.Comparator; @@ -49,7 +50,7 @@ public class PurchaseOrderController { new PropertyValueFactory<>("status")); filtered = new FilteredList<>(purchaseOrders, p -> true); - tvPurchaseOrders.setItems(filtered); + TableViewSupport.bindSortedItems(tvPurchaseOrders, filtered); if (txtSearch != null) { txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); @@ -69,7 +70,6 @@ public class PurchaseOrderController { Platform.runLater(() -> { purchaseOrders.setAll(dtos); - tvPurchaseOrders.setItems(filtered); }); } catch (Exception e) { Platform.runLater(() -> { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index 22c96dbd..e0436bf5 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -7,7 +7,6 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.application.Platform; -import javafx.scene.input.MouseButton; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Button; @@ -37,6 +36,7 @@ import org.example.petshopdesktop.models.SaleCartItem; import org.example.petshopdesktop.models.SaleDetail; import org.example.petshopdesktop.models.SaleLineItem; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TableViewSupport; import java.math.BigDecimal; import java.text.NumberFormat; @@ -168,16 +168,8 @@ public class SaleController { colSalePaymentType.setCellValueFactory(new PropertyValueFactory<>("paymentMethod")); filteredSales = new FilteredList<>(saleItems, s -> true); - tvSales.setItems(filteredSales); - - tvSales.setOnMouseClicked(event -> { - if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { - SaleLineItem selected = tvSales.getSelectionModel().getSelectedItem(); - if (selected != null) { - openSaleDetailDialog(selected.getSaleId()); - } - } - }); + TableViewSupport.bindSortedItems(tvSales, filteredSales); + TableViewSupport.installDoubleClickAction(tvSales, selected -> openSaleDetailDialog(selected.getSaleId())); txtSearch.textProperty().addListener((obs, oldVal, newVal) -> applySalesFilter(newVal)); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ServiceController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ServiceController.java index 8500677a..dce27137 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ServiceController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ServiceController.java @@ -15,6 +15,7 @@ import org.example.petshopdesktop.api.dto.service.ServiceResponse; import org.example.petshopdesktop.api.endpoints.ServiceApi; import org.example.petshopdesktop.controllers.dialogcontrollers.ServiceDialogController; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TableViewSupport; import javafx.stage.Modality; import java.util.List; @@ -54,6 +55,7 @@ public class ServiceController { colServicePrice.setCellValueFactory(new PropertyValueFactory<>("servicePrice")); displayServices(); + TableViewSupport.installDoubleClickAction(tvServices, selected -> openDialog(selected, "Edit")); tvServices.getSelectionModel().selectedItemProperty().addListener( (observable, oldValue, newValue) -> { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java index bec351a4..fbd3bd87 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java @@ -21,6 +21,7 @@ import org.example.petshopdesktop.api.endpoints.EmployeeApi; import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.models.StaffAccount; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TableViewSupport; import java.sql.Timestamp; import java.time.ZoneId; @@ -76,7 +77,8 @@ public class StaffAccountsController { colCreated.setCellValueFactory(new PropertyValueFactory<>("createdAt")); filtered = new FilteredList<>(staffAccounts, a -> true); - tvStaff.setItems(filtered); + TableViewSupport.bindSortedItems(tvStaff, filtered); + TableViewSupport.installDoubleClickAction(tvStaff, this::openEditDialog); txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); @@ -131,6 +133,10 @@ public class StaffAccountsController { void btnEditAccountClicked(ActionEvent event) { lblError.setText(""); StaffAccount selected = tvStaff.getSelectionModel().getSelectedItem(); + openEditDialog(selected); + } + + private void openEditDialog(StaffAccount selected) { if (selected == null) { lblError.setText("Select a staff account to edit."); return; diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SupplierController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SupplierController.java index 28248657..b31070d3 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SupplierController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SupplierController.java @@ -16,6 +16,7 @@ import org.example.petshopdesktop.api.endpoints.SupplierApi; import org.example.petshopdesktop.controllers.dialogcontrollers.SupplierDialogController; import org.example.petshopdesktop.models.Supplier; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TableViewSupport; import java.io.IOException; import java.util.List; @@ -78,6 +79,7 @@ public class SupplierController { colSupplierPhone.setCellValueFactory(new PropertyValueFactory("supPhone")); displaySupplier(); + TableViewSupport.installDoubleClickAction(tvSuppliers, selected -> openDialog(selected, "Edit")); //EventListener to Enable buttons when a row is selected tvSuppliers.getSelectionModel().selectedItemProperty().addListener( diff --git a/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java b/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java new file mode 100644 index 00000000..9d5d327c --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java @@ -0,0 +1,33 @@ +package org.example.petshopdesktop.util; + +import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; +import javafx.scene.control.TableRow; +import javafx.scene.control.TableView; +import javafx.scene.input.MouseButton; + +import java.util.function.Consumer; + +public final class TableViewSupport { + + private TableViewSupport() { + } + + public static void bindSortedItems(TableView tableView, FilteredList filteredItems) { + SortedList sortedItems = new SortedList<>(filteredItems); + sortedItems.comparatorProperty().bind(tableView.comparatorProperty()); + tableView.setItems(sortedItems); + } + + public static void installDoubleClickAction(TableView tableView, Consumer action) { + tableView.setRowFactory(tv -> { + TableRow row = new TableRow<>(); + row.setOnMouseClicked(event -> { + if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2 && !row.isEmpty()) { + action.accept(row.getItem()); + } + }); + return row; + }); + } +} From c82a0efa9330cec68df7787c5f1c617de17f624b Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Thu, 9 Apr 2026 11:49:14 -0600 Subject: [PATCH 12/33] Improve Desktop Tables --- .../controllers/AdoptionController.java | 14 +++ .../controllers/AnalyticsController.java | 20 ++++ .../controllers/AppointmentController.java | 11 +++ .../controllers/InventoryController.java | 14 +++ .../controllers/PetController.java | 20 ++++ .../controllers/ProductController.java | 17 ++++ .../ProductSupplierController.java | 14 +++ .../controllers/PurchaseOrderController.java | 37 ++++++++ .../controllers/SaleController.java | 20 +++- .../controllers/ServiceController.java | 11 +++ .../controllers/StaffAccountsController.java | 9 ++ .../controllers/SupplierController.java | 14 +++ .../AdoptionDialogController.java | 1 + .../ProductSupplierDialogController.java | 94 ++++++++++++++++++- .../PurchaseOrderDetailsDialogController.java | 40 ++++++++ .../SaleDetailDialogController.java | 47 ++++++++++ .../petshopdesktop/models/SaleDetail.java | 8 +- .../petshopdesktop/util/TableViewSupport.java | 22 +++++ .../purchase-order-details-dialog-view.fxml | 48 ++++++++++ .../dialogviews/sale-detail-dialog-view.fxml | 1 + .../modelviews/adoption-view.fxml | 14 +++ .../modelviews/analytics-view.fxml | 10 +- .../modelviews/appointment-view.fxml | 14 +++ .../modelviews/inventory-view.fxml | 14 +++ .../petshopdesktop/modelviews/pet-view.fxml | 14 +++ .../modelviews/product-supplier-view.fxml | 14 +++ .../modelviews/product-view.fxml | 14 +++ .../modelviews/purchase-order-view.fxml | 6 ++ .../petshopdesktop/modelviews/sale-view.fxml | 5 + .../modelviews/service-view.fxml | 14 +++ .../modelviews/staff-accounts-view.fxml | 6 ++ .../modelviews/supplier-view.fxml | 14 +++ 32 files changed, 592 insertions(+), 9 deletions(-) create mode 100644 desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PurchaseOrderDetailsDialogController.java create mode 100644 desktop/src/main/resources/org/example/petshopdesktop/dialogviews/purchase-order-details-dialog-view.fxml diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java index d31785be..e139450c 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java @@ -35,6 +35,12 @@ public class AdoptionController { @FXML private Button btnEdit; + @FXML + private Button btnRefresh; + + @FXML + private Label lblStatus; + @FXML private TableColumn colAdoptionId; @@ -103,6 +109,14 @@ public class AdoptionController { }); } + @FXML + void btnRefresh(ActionEvent event) { + txtSearch.clear(); + tvAdoptions.getSortOrder().clear(); + displayAdoptions(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); + } + @FXML void btnAddClicked(ActionEvent event) { mode = "Add"; diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java index 60d14e7f..040cf0d5 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java @@ -6,6 +6,8 @@ import javafx.fxml.FXML; import javafx.scene.chart.*; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; import org.example.petshopdesktop.api.dto.analytics.DailySales; import org.example.petshopdesktop.api.dto.analytics.DashboardResponse; import org.example.petshopdesktop.api.dto.analytics.TopProduct; @@ -15,6 +17,7 @@ import org.example.petshopdesktop.api.endpoints.AnalyticsApi; import org.example.petshopdesktop.api.endpoints.SaleApi; import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TableViewSupport; import java.math.BigDecimal; import java.math.RoundingMode; @@ -32,6 +35,9 @@ public class AnalyticsController { @FXML private Button btnRefresh; + @FXML + private Label lblStatus; + @FXML private Label lblError; @@ -65,6 +71,9 @@ public class AnalyticsController { @FXML private BarChart chartEmployeePerformance; + @FXML + private TabPane tabPane; + private static final String SALES_COLOR = "#ff6b35"; private static final String REVENUE_COLOR = "#4ecdc4"; private static final String QUANTITY_COLOR = "#ff9f1c"; @@ -79,6 +88,13 @@ public class AnalyticsController { @FXML public void initialize() { configureCharts(); + if (tabPane != null) { + tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> { + if (oldTab != newTab && newTab != null) { + loadAnalyticsData(); + } + }); + } loadAnalyticsData(); } @@ -126,6 +142,9 @@ public class AnalyticsController { private void loadAnalyticsData() { lblError.setVisible(false); + if (lblStatus != null) { + lblStatus.setVisible(false); + } new Thread(() -> { try { DashboardResponse dashboard = AnalyticsApi.getInstance().getDashboard(30, 10); @@ -472,5 +491,6 @@ public class AnalyticsController { @FXML void handleRefresh(ActionEvent event) { loadAnalyticsData(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index 3b19567c..3cc4a904 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -40,6 +40,9 @@ public class AppointmentController { @FXML private Button btnAdd; @FXML private Button btnEdit; @FXML private Button btnDelete; + @FXML private Button btnRefresh; + + @FXML private Label lblStatus; @FXML private TextField txtSearch; @@ -135,6 +138,14 @@ public class AppointmentController { }).start(); } + @FXML + void btnRefresh(ActionEvent event) { + txtSearch.clear(); + tvAppointments.getSortOrder().clear(); + loadAppointments(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); + } + @FXML void btnAddClicked(ActionEvent event){ openDialog(null, "Add"); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java index d889237d..c5e6cd96 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java @@ -35,6 +35,12 @@ public class InventoryController { @FXML private Button btnEdit; + @FXML + private Button btnRefresh; + + @FXML + private Label lblStatus; + @FXML private TableColumn colInventoryId; @@ -94,6 +100,14 @@ public class InventoryController { } //Opens dialog in add mode + @FXML + void btnRefresh(ActionEvent event) { + txtSearch.clear(); + tvInventory.getSortOrder().clear(); + displayInventory(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); + } + @FXML void btnAddClicked(ActionEvent event) { mode = "Add"; diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java index fbe2df24..fa091671 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java @@ -40,6 +40,12 @@ public class PetController { @FXML private Button btnEdit; + @FXML + private Button btnRefresh; + + @FXML + private Label lblStatus; + @FXML private TableColumn colPetAge; @@ -82,6 +88,20 @@ public class PetController { @FXML private TextField txtSearch; + @FXML + void btnRefresh(ActionEvent event) { + txtSearch.clear(); + if (cbSpeciesFilter != null) { + cbSpeciesFilter.getSelectionModel().selectFirst(); + } + if (cbStatusFilter != null) { + cbStatusFilter.getSelectionModel().selectFirst(); + } + tvPets.getSortOrder().clear(); + displayPets(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); + } + @FXML void btnAddClicked(ActionEvent event) { mode = "Add"; diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java index c27974b2..25f1bf14 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java @@ -44,6 +44,12 @@ public class ProductController { @FXML private Button btnEdit; + @FXML + private Button btnRefresh; + + @FXML + private Label lblStatus; + @FXML private TableColumn colProductCategory; @@ -156,6 +162,17 @@ public class ProductController { * open a new dialog for adding a product * @param event click event for button */ + @FXML + void btnRefresh(ActionEvent event) { + txtSearch.clear(); + if (cbCategoryFilter != null) { + cbCategoryFilter.getSelectionModel().selectFirst(); + } + tvProducts.getSortOrder().clear(); + displayProduct(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); + } + @FXML void btnAddClicked(ActionEvent event) { mode = "Add"; diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductSupplierController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductSupplierController.java index 4a632a1a..8060a68f 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductSupplierController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductSupplierController.java @@ -34,6 +34,12 @@ public class ProductSupplierController { @FXML private Button btnEdit; + @FXML + private Button btnRefresh; + + @FXML + private Label lblStatus; + @FXML private TableColumn colCost; @@ -167,6 +173,14 @@ public class ProductSupplierController { * open a new dialog for adding a productSupplier * @param event click event for button */ + @FXML + void btnRefresh(ActionEvent event) { + txtSearch.clear(); + tvProductSuppliers.getSortOrder().clear(); + displayProductSupplier(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); + } + @FXML void btnAddClicked(ActionEvent event) { mode = "Add"; diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java index de8334ab..19cb952f 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java @@ -5,14 +5,21 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.control.cell.PropertyValueFactory; import org.example.petshopdesktop.DTOs.PurchaseOrderDTO; import org.example.petshopdesktop.api.dto.purchaseorder.PurchaseOrderResponse; import org.example.petshopdesktop.api.endpoints.PurchaseOrderApi; +import org.example.petshopdesktop.controllers.dialogcontrollers.PurchaseOrderDetailsDialogController; import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.TableViewSupport; +import javafx.stage.Modality; +import javafx.stage.Stage; + +import java.io.IOException; import java.util.List; import java.util.Comparator; import java.util.stream.Collectors; @@ -21,6 +28,8 @@ public class PurchaseOrderController { @FXML private Button btnRefresh; + @FXML private Label lblStatus; + @FXML private TextField txtSearch; @@ -51,6 +60,7 @@ public class PurchaseOrderController { filtered = new FilteredList<>(purchaseOrders, p -> true); TableViewSupport.bindSortedItems(tvPurchaseOrders, filtered); + TableViewSupport.installDoubleClickAction(tvPurchaseOrders, this::openDetailsDialog); if (txtSearch != null) { txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); @@ -109,7 +119,34 @@ public class PurchaseOrderController { @FXML void btnRefresh() { + if (txtSearch != null) { + txtSearch.clear(); + } + TableViewSupport.clearSort(tvPurchaseOrders); loadPurchaseOrders(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); + } + + private void openDetailsDialog(PurchaseOrderDTO selected) { + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/purchase-order-details-dialog-view.fxml")); + Stage dialog = new Stage(); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Purchase Order Details"); + dialog.setScene(new Scene(loader.load())); + PurchaseOrderDetailsDialogController controller = loader.getController(); + controller.displayPurchaseOrder(selected); + controller.setCloseAction(() -> dialog.close()); + dialog.initOwner(tvPurchaseOrders.getScene().getWindow()); + dialog.setResizable(false); + dialog.showAndWait(); + } catch (IOException e) { + ActivityLogger.getInstance().logException( + "PurchaseOrderController.openDetailsDialog", + e, + "Opening purchase order details"); + new Alert(Alert.AlertType.ERROR, "Unable to open purchase order details").showAndWait(); + } } private PurchaseOrderDTO mapToPurchaseOrderDTO(PurchaseOrderResponse response) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index e0436bf5..7e6573de 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -51,6 +51,9 @@ public class SaleController { @FXML private Button btnRefresh; + @FXML + private Label lblStatus; + @FXML private Button btnRefund; @@ -292,7 +295,10 @@ public class SaleController { @FXML void btnRefresh(ActionEvent event) { - refreshSales(true); + txtSearch.clear(); + TableViewSupport.clearSort(tvSales); + refreshSales(false); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); } @FXML @@ -425,10 +431,11 @@ public class SaleController { @FXML void btnRefund(ActionEvent event) { - openRefundDialog(); + SaleLineItem selectedSale = tvSales.getSelectionModel().getSelectedItem(); + openRefundDialog(selectedSale != null ? (long) selectedSale.getSaleId() : null); } - private void openRefundDialog() { + private void openRefundDialog(Long saleId) { try { SaleLineItem selectedSale = tvSales.getSelectionModel().getSelectedItem(); if (selectedSale != null && selectedSale.isRefund()) { @@ -444,8 +451,8 @@ public class SaleController { dialog.setTitle("Process Refund"); dialog.setScene(new Scene(loader.load())); var controller = loader.getController(); - if (selectedSale != null) { - controller.prefillSale((long) selectedSale.getSaleId()); + if (saleId != null) { + controller.prefillSale(saleId); } dialog.setMinWidth(860); dialog.setMinHeight(680); @@ -477,6 +484,8 @@ public class SaleController { dialog.setTitle("Sale Details"); dialog.setScene(new Scene(loader.load())); var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.SaleDetailDialogController) loader.getController(); + controller.setSaleId((long) sale.getSaleId()); + controller.setRefundAction(this::openRefundDialog); controller.displaySaleDetails(mapToSaleDetail(sale)); dialog.setResizable(false); dialog.showAndWait(); @@ -520,6 +529,7 @@ public class SaleController { sale.getTotalAmount() != null ? sale.getTotalAmount().doubleValue() : 0.0, sale.getPaymentMethod(), sale.getEmployeeName(), + Boolean.TRUE.equals(sale.getIsRefund()), items ); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ServiceController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ServiceController.java index dce27137..04a72dba 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ServiceController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ServiceController.java @@ -28,6 +28,9 @@ public class ServiceController { @FXML private Button btnAdd; @FXML private Button btnDelete; @FXML private Button btnEdit; + @FXML private Button btnRefresh; + + @FXML private Label lblStatus; @FXML private TableColumn colServiceId; @FXML private TableColumn colServiceName; @@ -131,6 +134,14 @@ public class ServiceController { } + @FXML + void btnRefresh(ActionEvent event) { + txtSearch.clear(); + tvServices.getSortOrder().clear(); + displayServices(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); + } + @FXML void btnAddClicked(ActionEvent event) { mode = "Add"; diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java index fbd3bd87..251d9a69 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java @@ -58,6 +58,12 @@ public class StaffAccountsController { @FXML private Label lblError; + @FXML + private Label lblStatus; + + @FXML + private Button btnRefresh; + @FXML private Button btnCreateAccount; @@ -107,7 +113,10 @@ public class StaffAccountsController { @FXML void btnRefreshClicked(ActionEvent event) { + txtSearch.clear(); + TableViewSupport.clearSort(tvStaff); refresh(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); } @FXML diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SupplierController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SupplierController.java index b31070d3..c6120d15 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SupplierController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SupplierController.java @@ -37,6 +37,12 @@ public class SupplierController { @FXML private Button btnEdit; + @FXML + private Button btnRefresh; + + @FXML + private Label lblStatus; + @FXML private TableColumn colContactPerson; @@ -170,6 +176,14 @@ public class SupplierController { * open a new dialog for adding a supplier * @param event click event for button */ + @FXML + void btnRefresh(ActionEvent event) { + txtSearch.clear(); + tvSuppliers.getSortOrder().clear(); + displaySupplier(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); + } + @FXML void btnAddClicked(ActionEvent event) { mode = "Add"; diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java index 015c02ac..21a89e31 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java @@ -265,6 +265,7 @@ public class AdoptionDialogController { if (adoption != null) { selectedAdoption = adoption; lblAdoptionId.setText("ID: " + adoption.getAdoptionId()); + ensureSelectedEmployeeOption(cbEmployee.getItems()); applySelectedPet(); applySelectedCustomer(); applySelectedEmployee(); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductSupplierDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductSupplierDialogController.java index acff3799..8e2ef094 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductSupplierDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductSupplierDialogController.java @@ -19,7 +19,9 @@ import org.example.petshopdesktop.api.endpoints.ProductSupplierApi; import org.example.petshopdesktop.util.ActivityLogger; import java.math.BigDecimal; +import java.util.HashSet; import java.util.List; +import java.util.Set; public class ProductSupplierDialogController { @@ -49,6 +51,10 @@ public class ProductSupplierDialogController { private int selectedProdId = -1; private Long pendingSupplierId = null; private Long pendingProductId = null; + private boolean updatingChoices = false; + private ObservableList baseSuppliers = FXCollections.observableArrayList(); + private ObservableList baseProducts = FXCollections.observableArrayList(); + private List relations = List.of(); /** * add event listeners to buttons and set up combobox @@ -115,20 +121,39 @@ public class ProductSupplierDialogController { } }); + cbProduct.valueProperty().addListener((obs, oldValue, newValue) -> { + if (updatingChoices) { + return; + } + filterSuppliersByProduct(newValue != null ? newValue.getId() : null); + }); + + cbSupplier.valueProperty().addListener((obs, oldValue, newValue) -> { + if (updatingChoices) { + return; + } + filterProductsBySupplier(newValue != null ? newValue.getId() : null); + }); + new Thread(() -> { try { + var productSupplierLinks = ProductSupplierApi.getInstance().listProductSuppliers(null); var suppliers = DropdownApi.getInstance().getSuppliers(); var products = DropdownApi.getInstance().getProducts(); Platform.runLater(() -> { + relations = productSupplierLinks != null ? productSupplierLinks : List.of(); if (suppliers != null) { - cbSupplier.setItems(FXCollections.observableArrayList(suppliers)); + baseSuppliers = FXCollections.observableArrayList(suppliers); + cbSupplier.setItems(FXCollections.observableArrayList(baseSuppliers)); applyPendingSupplierSelection(); } if (products != null) { - cbProduct.setItems(FXCollections.observableArrayList(products)); + baseProducts = FXCollections.observableArrayList(products); + cbProduct.setItems(FXCollections.observableArrayList(baseProducts)); applyPendingProductSelection(); } + applyCurrentFilters(); }); } catch (Exception e) { Platform.runLater(() -> { @@ -271,7 +296,10 @@ public class ProductSupplierDialogController { } DropdownOption product = findOptionById(cbProduct.getItems(), pendingProductId); if (product != null) { + updatingChoices = true; cbProduct.getSelectionModel().select(product); + updatingChoices = false; + filterSuppliersByProduct(product.getId()); pendingProductId = null; } } @@ -282,11 +310,73 @@ public class ProductSupplierDialogController { } DropdownOption supplier = findOptionById(cbSupplier.getItems(), pendingSupplierId); if (supplier != null) { + updatingChoices = true; cbSupplier.getSelectionModel().select(supplier); + updatingChoices = false; + filterProductsBySupplier(supplier.getId()); pendingSupplierId = null; } } + private void applyCurrentFilters() { + DropdownOption selectedProduct = cbProduct.getSelectionModel().getSelectedItem(); + DropdownOption selectedSupplier = cbSupplier.getSelectionModel().getSelectedItem(); + filterSuppliersByProduct(selectedProduct != null ? selectedProduct.getId() : null); + filterProductsBySupplier(selectedSupplier != null ? selectedSupplier.getId() : null); + } + + private void filterSuppliersByProduct(Long productId) { + updatingChoices = true; + try { + if (productId == null) { + cbSupplier.setItems(FXCollections.observableArrayList(baseSuppliers)); + return; + } + Set allowedSupplierIds = new HashSet<>(); + for (ProductSupplierResponse relation : relations) { + if (relation.getProductId() != null && relation.getProductId().equals(productId) && relation.getSupplierId() != null) { + allowedSupplierIds.add(relation.getSupplierId()); + } + } + ObservableList filtered = FXCollections.observableArrayList( + baseSuppliers.stream().filter(option -> option.getId() != null && allowedSupplierIds.contains(option.getId())).toList() + ); + cbSupplier.setItems(filtered); + DropdownOption selectedSupplier = cbSupplier.getSelectionModel().getSelectedItem(); + if (selectedSupplier != null && !allowedSupplierIds.contains(selectedSupplier.getId())) { + cbSupplier.getSelectionModel().clearSelection(); + } + } finally { + updatingChoices = false; + } + } + + private void filterProductsBySupplier(Long supplierId) { + updatingChoices = true; + try { + if (supplierId == null) { + cbProduct.setItems(FXCollections.observableArrayList(baseProducts)); + return; + } + Set allowedProductIds = new HashSet<>(); + for (ProductSupplierResponse relation : relations) { + if (relation.getSupplierId() != null && relation.getSupplierId().equals(supplierId) && relation.getProductId() != null) { + allowedProductIds.add(relation.getProductId()); + } + } + ObservableList filtered = FXCollections.observableArrayList( + baseProducts.stream().filter(option -> option.getId() != null && allowedProductIds.contains(option.getId())).toList() + ); + cbProduct.setItems(filtered); + DropdownOption selectedProduct = cbProduct.getSelectionModel().getSelectedItem(); + if (selectedProduct != null && !allowedProductIds.contains(selectedProduct.getId())) { + cbProduct.getSelectionModel().clearSelection(); + } + } finally { + updatingChoices = false; + } + } + private DropdownOption findOptionById(List options, Long id) { if (options == null || id == null) { return null; diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PurchaseOrderDetailsDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PurchaseOrderDetailsDialogController.java new file mode 100644 index 00000000..ff597d36 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PurchaseOrderDetailsDialogController.java @@ -0,0 +1,40 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.stage.Stage; +import org.example.petshopdesktop.DTOs.PurchaseOrderDTO; + +public class PurchaseOrderDetailsDialogController { + + @FXML private Label lblOrderId; + @FXML private Label lblSupplier; + @FXML private Label lblOrderDate; + @FXML private Label lblStatus; + + private Runnable closeAction; + + public void displayPurchaseOrder(PurchaseOrderDTO order) { + if (order == null) { + return; + } + lblOrderId.setText(String.valueOf(order.getPurchaseOrderId())); + lblSupplier.setText(order.getSupplierName() != null ? order.getSupplierName() : ""); + lblOrderDate.setText(order.getOrderDate() != null ? order.getOrderDate() : ""); + lblStatus.setText(order.getStatus() != null ? order.getStatus() : ""); + } + + public void setCloseAction(Runnable closeAction) { + this.closeAction = closeAction; + } + + @FXML + void btnCloseClicked() { + if (closeAction != null) { + closeAction.run(); + return; + } + Stage stage = (Stage) lblOrderId.getScene().getWindow(); + stage.close(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java index 3a0c67cc..abeb3355 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java @@ -2,11 +2,19 @@ package org.example.petshopdesktop.controllers.dialogcontrollers; import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.control.Button; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; import javafx.stage.Stage; import org.example.petshopdesktop.models.SaleDetail; +import org.example.petshopdesktop.controllers.dialogcontrollers.RefundDialogController; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.stage.Modality; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.util.function.Consumer; import java.text.NumberFormat; import java.time.format.DateTimeFormatter; @@ -19,6 +27,7 @@ public class SaleDetailDialogController { @FXML private Label lblEmployee; @FXML private Label lblPayment; @FXML private Label lblTotal; + @FXML private Button btnRefund; @FXML private TableView tvItems; @FXML private TableColumn colProduct; @FXML private TableColumn colQuantity; @@ -27,6 +36,8 @@ public class SaleDetailDialogController { private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + private Long saleId; + private Consumer refundAction; @FXML public void initialize() { @@ -38,12 +49,48 @@ public class SaleDetailDialogController { } public void displaySaleDetails(SaleDetail sale) { + saleId = (long) sale.getSaleId(); lblSaleId.setText(String.valueOf(sale.getSaleId())); lblSaleDate.setText(sale.getSaleDate() != null ? sale.getSaleDate().format(DATE_FORMATTER) : ""); lblEmployee.setText(sale.getEmployeeName() != null ? sale.getEmployeeName() : ""); lblPayment.setText(sale.getPaymentMethod() != null ? sale.getPaymentMethod() : ""); lblTotal.setText(currency.format(sale.getTotalAmount())); tvItems.setItems(sale.getItems()); + if (btnRefund != null) { + btnRefund.setDisable(sale.isRefund()); + } + } + + public void setSaleId(Long saleId) { + this.saleId = saleId; + } + + public void setRefundAction(Consumer refundAction) { + this.refundAction = refundAction; + } + + @FXML + void btnRefundClicked() { + if (saleId == null) { + return; + } + if (refundAction != null) { + refundAction.accept(saleId); + return; + } + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml")); + Stage dialog = new Stage(); + dialog.initOwner(tvItems.getScene().getWindow()); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Process Refund"); + dialog.setScene(new Scene(loader.load())); + RefundDialogController controller = loader.getController(); + controller.prefillSale(saleId); + dialog.showAndWait(); + } catch (Exception e) { + ActivityLogger.getInstance().logException("SaleDetailDialogController.btnRefundClicked", e, "Opening refund dialog"); + } } @FXML diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java b/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java index 8ed6e291..c3ff719e 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java +++ b/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java @@ -9,14 +9,16 @@ public class SaleDetail { private final double totalAmount; private final String paymentMethod; private final String employeeName; + private final boolean refund; private final ObservableList items; - public SaleDetail(int saleId, LocalDateTime saleDate, double totalAmount, String paymentMethod, String employeeName, ObservableList items) { + public SaleDetail(int saleId, LocalDateTime saleDate, double totalAmount, String paymentMethod, String employeeName, boolean refund, ObservableList items) { this.saleId = saleId; this.saleDate = saleDate; this.totalAmount = totalAmount; this.paymentMethod = paymentMethod; this.employeeName = employeeName; + this.refund = refund; this.items = items; } @@ -40,6 +42,10 @@ public class SaleDetail { return employeeName; } + public boolean isRefund() { + return refund; + } + public ObservableList getItems() { return items; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java b/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java index 9d5d327c..bd18548e 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java +++ b/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java @@ -2,9 +2,12 @@ package org.example.petshopdesktop.util; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; +import javafx.animation.PauseTransition; +import javafx.scene.control.Label; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.input.MouseButton; +import javafx.util.Duration; import java.util.function.Consumer; @@ -19,6 +22,25 @@ public final class TableViewSupport { tableView.setItems(sortedItems); } + public static void clearSort(TableView tableView) { + tableView.getSortOrder().clear(); + } + + public static void flashStatus(Label label, String message) { + if (label == null) { + return; + } + label.setText(message); + label.setVisible(true); + label.setManaged(true); + PauseTransition delay = new PauseTransition(Duration.seconds(1.5)); + delay.setOnFinished(event -> { + label.setVisible(false); + label.setManaged(false); + }); + delay.playFromStart(); + } + public static void installDoubleClickAction(TableView tableView, Consumer action) { tableView.setRowFactory(tv -> { TableRow row = new TableRow<>(); diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/purchase-order-details-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/purchase-order-details-dialog-view.fxml new file mode 100644 index 00000000..f209eefb --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/purchase-order-details-dialog-view.fxml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml index dec7a883..215211b9 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml @@ -10,6 +10,7 @@ + @@ -25,6 +26,7 @@ + + + - + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml index 8e920be0..a508f9c0 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/inventory-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/inventory-view.fxml index 18e9ad5a..81eb71ba 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/inventory-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/inventory-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml index d599e010..5aa0acdb 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml @@ -53,6 +53,14 @@ + @@ -69,6 +77,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-supplier-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-supplier-view.fxml index 19817faa..7d5afc4c 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-supplier-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-supplier-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml index eeff74b4..641ba665 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml @@ -52,6 +52,14 @@ + @@ -67,6 +75,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/purchase-order-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/purchase-order-view.fxml index 74a00e6d..ab9276c4 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/purchase-order-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/purchase-order-view.fxml @@ -55,6 +55,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml index 3a3c9608..abf0df77 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml @@ -89,6 +89,11 @@ + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/service-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/service-view.fxml index 5353b0e6..3a4ae448 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/service-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/service-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml index 9619e374..da15274d 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml @@ -76,6 +76,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/supplier-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/supplier-view.fxml index 9c6d75cc..ade4b18d 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/supplier-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/supplier-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + From dd1502d2cebdf35938b82f32c873153d05251552 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Thu, 9 Apr 2026 11:49:55 -0600 Subject: [PATCH 13/33] Adjust Sales Layout --- .../org/example/petshopdesktop/modelviews/sale-view.fxml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml index abf0df77..b237753f 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml @@ -19,7 +19,7 @@ - + - + From 8559a46cb9c2bd11b7fc3b73a0774ff99d02636e Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:17:51 -0600 Subject: [PATCH 14/33] created viewmodels for detailFragments --- .../AdoptionDetailFragment.java | 198 ++++-------- .../AppointmentDetailFragment.java | 116 ++------ .../InventoryDetailFragment.java | 139 +++------ .../detailfragments/PetDetailFragment.java | 128 +++----- .../ProductDetailFragment.java | 133 +++------ .../ProductSupplierDetailFragment.java | 122 +++----- .../PurchaseOrderDetailFragment.java | 8 +- .../detailfragments/RefundFragment.java | 281 +++++------------- .../detailfragments/SaleDetailFragment.java | 238 +++++++-------- .../ServiceDetailFragment.java | 75 ++--- .../detailfragments/StaffDetailFragment.java | 76 ++--- .../SupplierDetailFragment.java | 77 ++--- .../viewmodels/AdoptionDetailViewModel.java | 102 +++++++ .../viewmodels/InventoryDetailViewModel.java | 79 +++++ .../viewmodels/PetDetailViewModel.java | 94 ++++++ .../viewmodels/ProductDetailViewModel.java | 85 ++++++ .../ProductSupplierDetailViewModel.java | 78 +++++ .../PurchaseOrderDetailViewModel.java | 26 ++ .../viewmodels/RefundViewModel.java | 167 +++++++++++ .../viewmodels/SaleDetailViewModel.java | 109 +++++++ .../viewmodels/ServiceDetailViewModel.java | 54 ++++ .../viewmodels/StaffDetailViewModel.java | 49 +++ .../viewmodels/SupplierDetailViewModel.java | 54 ++++ 23 files changed, 1359 insertions(+), 1129 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java 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 ece652fd..eae36642 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 @@ -16,11 +16,7 @@ import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.AdoptionViewModel; -import com.example.petstoremobile.viewmodels.CustomerViewModel; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; -import com.example.petstoremobile.viewmodels.UserViewModel; +import com.example.petstoremobile.viewmodels.AdoptionDetailViewModel; import java.math.BigDecimal; import java.util.*; @@ -34,35 +30,19 @@ import dagger.hilt.android.AndroidEntryPoint; public class AdoptionDetailFragment extends Fragment { private FragmentAdoptionDetailBinding binding; + private AdoptionDetailViewModel viewModel; - private long adoptionId = -1; - private boolean isEditing = false; private long preselectedPetId = -1; private long preselectedCustomerId = -1; private long preselectedStoreId = -1; private long preselectedEmployeeId = -1; - private List petList = new ArrayList<>(); - private List customerList = new ArrayList<>(); - private List storeList = new ArrayList<>(); - private List employeeList = new ArrayList<>(); - private final String[] STATUSES = {"Pending", "Completed", "Cancelled"}; - private AdoptionViewModel adoptionViewModel; - private PetViewModel petViewModel; - private CustomerViewModel customerViewModel; - private StoreViewModel storeViewModel; - private UserViewModel userViewModel; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); - petViewModel = new ViewModelProvider(this).get(PetViewModel.class); - customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); - userViewModel = new ViewModelProvider(this).get(UserViewModel.class); + viewModel = new ViewModelProvider(this).get(AdoptionDetailViewModel.class); } @Override @@ -77,6 +57,7 @@ public class AdoptionDetailFragment extends Fragment { super.onViewCreated(view, savedInstanceState); setupSpinners(); setupDatePicker(); + observeViewModel(); loadSpinnersData(); handleArguments(); @@ -85,29 +66,31 @@ public class AdoptionDetailFragment extends Fragment { binding.btnDeleteAdoption.setOnClickListener(v -> confirmDelete()); } + private void observeViewModel() { + viewModel.getPetList().observe(getViewLifecycleOwner(), list -> refreshPetSpinner()); + viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> refreshCustomerSpinner()); + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner()); + viewModel.getEmployeeList().observe(getViewLifecycleOwner(), list -> refreshEmployeeSpinner()); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Configures the spinner for adoption status. - */ private void setupSpinners() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES); - // Pet spinner disabled by default until customer is selected UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); - // Listener to enable pet spinner based on customer selection binding.spinnerAdoptionCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { if (position > 0) { UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet); } else { - if (!isEditing) { + if (!viewModel.isEditing()) { binding.spinnerAdoptionPet.setSelection(0); UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); } @@ -117,27 +100,21 @@ public class AdoptionDetailFragment extends Fragment { public void onNothingSelected(AdapterView parent) {} }); - // Listener to load employees based on selected store binding.spinnerAdoptionStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (position > 0 && position <= storeList.size()) { - DropdownDTO selectedStore = storeList.get(position - 1); + if (position > 0 && viewModel.getStoreList().getValue() != null && position <= viewModel.getStoreList().getValue().size()) { + DropdownDTO selectedStore = viewModel.getStoreList().getValue().get(position - 1); loadEmployees(selectedStore.getId()); } else { - employeeList.clear(); - refreshEmployeeSpinner(); + viewModel.setEmployeeList(new ArrayList<>()); } } - @Override public void onNothingSelected(AdapterView parent) {} }); } - /** - * Configures the date picker dialog for the adoption date field. - */ private void setupDatePicker() { binding.etAdoptionDate.setOnClickListener(v -> { Calendar c = Calendar.getInstance(); @@ -150,129 +127,77 @@ public class AdoptionDetailFragment extends Fragment { }); } - /** - * Fetches required data for spinners from the backend. - */ private void loadSpinnersData() { - loadPets(); - loadCustomers(); - loadStores(); - } - - /** - * Loads the list of pets from the API. - */ - private void loadPets() { - petViewModel.getAdoptionPets().observe(getViewLifecycleOwner(), resource -> { + viewModel.loadPets().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - petList = resource.data; - refreshPetSpinner(); + viewModel.setPetList(resource.data); + } + }); + viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setCustomerList(resource.data); + } + }); + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setStoreList(resource.data); } }); } - /** - * Populates the pet selection spinner with data. - */ private void refreshPetSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, viewModel.getPetList().getValue(), DropdownDTO::getLabel, "-- Select Pet --", preselectedPetId, DropdownDTO::getId); } - /** - * Loads the list of customers from the API. - */ - private void loadCustomers() { - customerViewModel.getCustomerDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data; - refreshCustomerSpinner(); - } - }); - } - - /** - * Populates the customer selection spinner with data. - */ private void refreshCustomerSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList, - DropdownDTO::getLabel, - "-- Select Customer --", + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, viewModel.getCustomerList().getValue(), + DropdownDTO::getLabel, "-- Select Customer --", preselectedCustomerId, DropdownDTO::getId); } - /** - * Loads the list of stores from the API. - */ - private void loadStores() { - storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data; - refreshStoreSpinner(); - } - }); - } - - /** - * Populates the store selection spinner with data. - */ private void refreshStoreSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, storeList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, viewModel.getStoreList().getValue(), DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId); } - /** - * Loads the list of employees for a specific store. - */ private void loadEmployees(Long storeId) { - storeViewModel.getStoreEmployees(storeId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadEmployees(storeId).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - employeeList = resource.data; - refreshEmployeeSpinner(); + viewModel.setEmployeeList(resource.data); } }); } - /** - * Populates the employee selection spinner with data. - */ private void refreshEmployeeSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, employeeList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, viewModel.getEmployeeList().getValue(), DropdownDTO::getLabel, "-- Select Staff --", preselectedEmployeeId, DropdownDTO::getId); } - /** - * Handles arguments to determine if the fragment is in edit or add mode. - */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("adoptionId")) { - isEditing = true; - adoptionId = a.getLong("adoptionId"); + long adoptionId = a.getLong("adoptionId"); + viewModel.setAdoptionId(adoptionId); binding.tvAdoptionMode.setText("Edit Adoption"); binding.tvAdoptionId.setText("ID: " + adoptionId); binding.tvAdoptionId.setVisibility(View.VISIBLE); binding.btnDeleteAdoption.setVisibility(View.VISIBLE); loadAdoptionData(); } else { - isEditing = false; + viewModel.setAdoptionId(-1); binding.tvAdoptionMode.setText("Add Adoption"); binding.btnDeleteAdoption.setVisibility(View.GONE); binding.tvAdoptionId.setVisibility(View.GONE); - - // Explicitly disable in add mode UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); } } - /** - * Fetches specific adoption details from the backend using the ID. - */ private void loadAdoptionData() { - adoptionViewModel.getAdoptionById(adoptionId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadAdoption().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { AdoptionDTO a = resource.data; @@ -289,7 +214,6 @@ public class AdoptionDetailFragment extends Fragment { refreshCustomerSpinner(); refreshStoreSpinner(); - // In edit mode, if a customer is already set, ensure pet spinner is enabled if (preselectedCustomerId != -1) { UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet); } @@ -299,9 +223,6 @@ public class AdoptionDetailFragment extends Fragment { }); } - /** - * Validates input and saves the adoption request to the backend. - */ private void saveAdoption() { if (binding.spinnerAdoptionCustomer.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; @@ -328,13 +249,13 @@ public class AdoptionDetailFragment extends Fragment { } } - DropdownDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); - DropdownDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); - DropdownDTO store = storeList.get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); + DropdownDTO customer = viewModel.getCustomerList().getValue().get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); + DropdownDTO pet = viewModel.getPetList().getValue().get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); + DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); Long employeeId = null; if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0) { - employeeId = employeeList.get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId(); + employeeId = viewModel.getEmployeeList().getValue().get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId(); } String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; @@ -349,33 +270,19 @@ public class AdoptionDetailFragment extends Fragment { fee ); - if (isEditing) { - adoptionViewModel.updateAdoption(adoptionId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - adoptionViewModel.createAdoption(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } + viewModel.saveAdoption(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), viewModel.isEditing() ? "Updated" : "Saved", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Shows a confirmation dialog before deleting an adoption request. - */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption", () -> - adoptionViewModel.deleteAdoption(adoptionId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteAdoption().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); navigateBack(); @@ -385,9 +292,6 @@ public class AdoptionDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous fragment. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } 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 1d280270..bf63fa41 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 @@ -43,37 +43,28 @@ public class AppointmentDetailFragment extends Fragment { private final Integer[] HOURS = {9, 10, 11, 12, 13, 14, 15, 16, 17}; private final Integer[] MINUTES = {0, 15, 30, 45}; - private AppointmentDetailViewModel appointmentViewModel; + private AppointmentDetailViewModel viewModel; private boolean isUpdatingUI = false; - /** - * Called when the fragment is first created. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - appointmentViewModel = new ViewModelProvider(this).get(AppointmentDetailViewModel.class); + viewModel = new ViewModelProvider(this).get(AppointmentDetailViewModel.class); } - /** - * Creates and returns the view hierarchy with the fragment. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentAppointmentDetailBinding.inflate(inflater, container, false); return binding.getRoot(); } - /** - * Called immediately after onCreateView has returned. - */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); setupSpinners(); setupDatePicker(); observeViewModel(); - appointmentViewModel.loadInitialFormData(); + viewModel.loadInitialFormData(); handleArguments(); binding.btnApptBack.setOnClickListener(v -> navigateBack()); @@ -81,110 +72,83 @@ public class AppointmentDetailFragment extends Fragment { binding.btnDeleteAppointment.setOnClickListener(v -> confirmDelete()); } - /** - * Called when the view previously created by onCreateView has been detached. - */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Configures the adapters and listeners for all spinners. - */ private void setupSpinners() { - //Status Spinner is empty by default the date determines whats in here SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, new String[]{}); - // Set up hour and minute spinners String[] hours = new String[HOURS.length]; for (int i = 0; i < HOURS.length; i++) hours[i] = DateTimeUtils.formatTime(HOURS[i], 0); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00", "15", "30", "45"}); - // Pet and Staff spinners disabled by until parent selection UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff); - // Listener to notify ViewModel of customer selection binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - appointmentViewModel.onCustomerSelected(position); + viewModel.onCustomerSelected(position); } @Override public void onNothingSelected(AdapterView parent) {} }); - // Listener to notify ViewModel of store selection binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - appointmentViewModel.onStoreSelected(position); + viewModel.onStoreSelected(position); } @Override public void onNothingSelected(AdapterView parent) {} }); - // Listeners for other selections - SpinnerUtils.setOnIndexSelectedListener(binding.spinnerService, p -> appointmentViewModel.onServiceSelected(p)); - SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPet, p -> appointmentViewModel.onPetSelected(p)); - SpinnerUtils.setOnIndexSelectedListener(binding.spinnerStaff, p -> appointmentViewModel.onStaffSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerService, p -> viewModel.onServiceSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPet, p -> viewModel.onPetSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerStaff, p -> viewModel.onStaffSelected(p)); - // Listeners for time changes SpinnerUtils.setOnIndexSelectedListener(binding.spinnerHour, p -> notifyDateTimeStatusChange()); SpinnerUtils.setOnIndexSelectedListener(binding.spinnerMinute, p -> notifyDateTimeStatusChange()); - - // Listener to notify ViewModel of status selection SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAppointmentStatus, p -> notifyDateTimeStatusChange()); } - /** - * Configures the date picker dialog for the appointment date field. - */ private void setupDatePicker() { binding.etAppointmentDate.setOnClickListener(v -> UIUtils.showDatePicker(requireContext(), binding.etAppointmentDate, this::notifyDateTimeStatusChange)); } - /** - * Observes the ViewModel for UI state and list updates. - */ private void observeViewModel() { - appointmentViewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); + viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); - // Populate spinners when data arrives - appointmentViewModel.getCustomers().observe(getViewLifecycleOwner(), list -> + viewModel.getCustomers().observe(getViewLifecycleOwner(), list -> SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, list, DropdownDTO::getLabel, "-- Select Customer --", preselectedCustomerId, DropdownDTO::getId)); - appointmentViewModel.getStores().observe(getViewLifecycleOwner(), list -> + viewModel.getStores().observe(getViewLifecycleOwner(), list -> SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, list, DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId)); - appointmentViewModel.getServices().observe(getViewLifecycleOwner(), list -> + viewModel.getServices().observe(getViewLifecycleOwner(), list -> SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, list, ServiceDTO::getServiceName, "-- Select Service --", preselectedServiceId, ServiceDTO::getServiceId)); - appointmentViewModel.getCustomerPets().observe(getViewLifecycleOwner(), list -> + viewModel.getCustomerPets().observe(getViewLifecycleOwner(), list -> SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, list, DropdownDTO::getLabel, "-- Select Pet --", preselectedPetId, DropdownDTO::getId)); - appointmentViewModel.getStoreEmployees().observe(getViewLifecycleOwner(), list -> + viewModel.getStoreEmployees().observe(getViewLifecycleOwner(), list -> SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, list, DropdownDTO::getLabel, "-- Select Staff --", preselectedStaffId, DropdownDTO::getId)); } - /** - * Applies the ViewState provided by the ViewModel to the UI components. - */ private void applyViewState(AppointmentDetailViewModel.ViewState state) { isUpdatingUI = true; - // Mode specific UI binding.tvApptMode.setText(state.isEditing ? "Edit Appointment" : "Add Appointment"); - binding.tvAppointmentId.setText(DateTimeUtils.formatId(appointmentViewModel.getAppointmentId())); + binding.tvAppointmentId.setText(DateTimeUtils.formatId(viewModel.getAppointmentId())); binding.tvAppointmentId.setVisibility(state.isEditing ? View.VISIBLE : View.GONE); binding.btnDeleteAppointment.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); binding.btnSaveAppointment.setVisibility(state.isSaveVisible ? View.VISIBLE : View.GONE); - // Enabling/Disabling Views and Labels UIUtils.setFieldEnabled(state.isCustomerEnabled, binding.spinnerCustomer, binding.tvLabelCustomer); UIUtils.setFieldEnabled(state.isStoreEnabled, binding.spinnerStore, binding.tvLabelStore); UIUtils.setFieldEnabled(state.isPetEnabled, binding.spinnerPet, binding.tvLabelPet); @@ -195,7 +159,6 @@ public class AppointmentDetailFragment extends Fragment { UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerMinute); UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAppointmentStatus); - // Update status options Object selected = binding.spinnerAppointmentStatus.getSelectedItem(); String current = selected != null ? selected.toString() : ""; SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, state.availableStatuses); @@ -204,9 +167,6 @@ public class AppointmentDetailFragment extends Fragment { isUpdatingUI = false; } - /** - * Notifies the ViewModel that the date, time, or status has changed. - */ private void notifyDateTimeStatusChange() { if (isUpdatingUI) return; @@ -214,27 +174,21 @@ public class AppointmentDetailFragment extends Fragment { String time = buildTimeString(); Object selected = binding.spinnerAppointmentStatus.getSelectedItem(); String status = selected != null ? selected.toString() : ""; - appointmentViewModel.onDateOrTimeChanged(date, time, status); + viewModel.onDateOrTimeChanged(date, time, status); } - /** - * Handles arguments to determine if the fragment is in edit or add mode. - */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("appointmentId")) { - appointmentViewModel.setAppointmentId(a.getLong("appointmentId")); + viewModel.setAppointmentId(a.getLong("appointmentId")); loadAppointmentData(); } else { - appointmentViewModel.setAppointmentId(-1); + viewModel.setAppointmentId(-1); } } - /** - * Fetches specific appointment details from the backend using the ID. - */ private void loadAppointmentData() { - appointmentViewModel.loadAppointment().observe(getViewLifecycleOwner(), resource -> { + viewModel.loadAppointment().observe(getViewLifecycleOwner(), resource -> { if (resource == null || resource.status != Resource.Status.SUCCESS || resource.data == null) return; AppointmentDTO a = resource.data; preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; @@ -254,9 +208,6 @@ public class AppointmentDetailFragment extends Fragment { }); } - /** - * Validates input and saves the appointment to the backend. - */ private void saveAppointment() { if (!validateRequiredFields()) return; @@ -264,14 +215,14 @@ public class AppointmentDetailFragment extends Fragment { String time = buildTimeString(); String status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase(); - if (!appointmentViewModel.isValidFutureBooking(status, date, time)) { + if (!viewModel.isValidFutureBooking(status, date, time)) { DialogUtils.showInfoDialog(requireContext(), "Invalid Time", "Booked appointments must be in the future."); return; } - appointmentViewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> { + viewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - AppointmentDetailViewModel.ViewState state = appointmentViewModel.getViewState().getValue(); + AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue(); String message = (state != null && state.isEditing) ? "Updated" : "Saved"; Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); navigateBack(); @@ -281,9 +232,6 @@ public class AppointmentDetailFragment extends Fragment { }); } - /** - * Validates that all required fields are selected. - */ private boolean validateRequiredFields() { if (binding.spinnerCustomer.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a customer"); if (binding.spinnerStore.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a store"); @@ -293,24 +241,15 @@ public class AppointmentDetailFragment extends Fragment { return true; } - /** - * Builds a time string from the hour and minute spinners. - */ private String buildTimeString() { return DateTimeUtils.formatTime(HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); } - /** - * Handles errors that occur during the saving process. - */ private void handleSaveError(String errorMessage) { if (errorMessage != null && errorMessage.toLowerCase().contains("not available")) showNoAvailabilityDialog(); else Toast.makeText(getContext(), errorMessage != null ? errorMessage : "Error saving", Toast.LENGTH_SHORT).show(); } - /** - * Shows a specialized dialog when a time slot is not available. - */ private void showNoAvailabilityDialog() { new androidx.appcompat.app.AlertDialog.Builder(requireContext()) .setTitle("No Availability") @@ -319,26 +258,17 @@ public class AppointmentDetailFragment extends Fragment { .setNegativeButton("Cancel Booking", (d, w) -> navigateBack()).show(); } - /** - * Shows a confirmation dialog and handles the deletion of an appointment. - */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () -> - appointmentViewModel.deleteAppointment().observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteAppointment().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) navigateBack(); })); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Parses a time string and sets the hour and minute spinners. - */ private void parseAndSetTimeSpinners(String time) { int[] parsedTime = DateTimeUtils.parseTimeString(time); if (parsedTime == null) return; 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 640b99b9..a6d1c613 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 @@ -17,16 +17,10 @@ import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding; import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.ProductDTO; -import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; -import com.example.petstoremobile.viewmodels.InventoryViewModel; -import com.example.petstoremobile.viewmodels.ProductViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; - -import java.util.ArrayList; -import java.util.List; +import com.example.petstoremobile.viewmodels.InventoryDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -37,33 +31,17 @@ import dagger.hilt.android.AndroidEntryPoint; public class InventoryDetailFragment extends Fragment { private FragmentInventoryDetailBinding binding; + private InventoryDetailViewModel viewModel; - private InventoryViewModel inventoryViewModel; - private ProductViewModel productViewModel; - private StoreViewModel storeViewModel; - - private boolean isEditing = false; - private long inventoryId = -1; private long preselectedStoreId = -1; private long preselectedProductId = -1; - private List storeList = new ArrayList<>(); - private List productList = new ArrayList<>(); - - /** - * Initializes the view models. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - inventoryViewModel = new ViewModelProvider(this).get(InventoryViewModel.class); - productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(InventoryDetailViewModel.class); } - /** - * Inflates the layout. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -71,13 +49,11 @@ public class InventoryDetailFragment extends Fragment { return binding.getRoot(); } - /** - * Sets up UI components after the view is created. - */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + observeViewModel(); loadSpinnersData(); handleArguments(); @@ -86,64 +62,47 @@ public class InventoryDetailFragment extends Fragment { binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete()); } + private void observeViewModel() { + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner()); + viewModel.getProductList().observe(getViewLifecycleOwner(), list -> refreshProductSpinner()); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Fetches required data for spinners from the backend. - */ private void loadSpinnersData() { - loadStores(); - loadProducts(); - } - - /** - * Loads the list of stores for the spinner. - */ - private void loadStores() { - storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data; - refreshStoreSpinner(); + viewModel.setStoreList(resource.data); + } + }); + viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setProductList(resource.data.getContent()); } }); } private void refreshStoreSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, storeList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, viewModel.getStoreList().getValue(), DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId); } - /** - * Loads the list of products for the spinner. - */ - private void loadProducts() { - productViewModel.getAllProducts(null, null, 0, 500, "prodName").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - productList = resource.data.getContent(); - refreshProductSpinner(); - } - }); - } - private void refreshProductSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, productList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, viewModel.getProductList().getValue(), ProductDTO::getProdName, "-- Select Product --", preselectedProductId, ProductDTO::getProdId); } - /** - * Handles fragment arguments to determine if we are in edit or add mode. - */ private void handleArguments() { Bundle args = getArguments(); if (args != null && args.containsKey("inventoryId")) { - isEditing = true; - inventoryId = args.getLong("inventoryId"); + long inventoryId = args.getLong("inventoryId"); + viewModel.setInventoryId(inventoryId); binding.tvInventoryMode.setText("Edit Inventory"); binding.tvInventoryId.setText("Inventory ID: " + inventoryId); @@ -153,7 +112,7 @@ public class InventoryDetailFragment extends Fragment { loadInventoryData(); } else { - isEditing = false; + viewModel.setInventoryId(-1); binding.tvInventoryMode.setText("Add Inventory"); binding.tvInventoryId.setVisibility(View.GONE); binding.btnDeleteInventory.setVisibility(View.GONE); @@ -161,11 +120,8 @@ public class InventoryDetailFragment extends Fragment { } } - /** - * Loads existing inventory data from the backend. - */ private void loadInventoryData() { - inventoryViewModel.getInventoryById(inventoryId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadInventory().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { InventoryDTO inv = resource.data; @@ -181,9 +137,6 @@ public class InventoryDetailFragment extends Fragment { }); } - /** - * Validates input and saves the current inventory item details to the backend. - */ private void saveInventory() { if (binding.spinnerInventoryStore.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Please select a store", Toast.LENGTH_SHORT).show(); @@ -200,38 +153,23 @@ public class InventoryDetailFragment extends Fragment { } int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim()); - DropdownDTO store = storeList.get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); - ProductDTO product = productList.get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); + DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); + ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); InventoryDTO request = new InventoryDTO(product.getProdId(), store.getId(), quantity); setButtonsEnabled(false); - if (isEditing) { - inventoryViewModel.updateInventory(inventoryId, request).observe(getViewLifecycleOwner(), resource -> { - setButtonsEnabled(true); - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - inventoryViewModel.createInventory(request).observe(getViewLifecycleOwner(), resource -> { - setButtonsEnabled(true); - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } + viewModel.saveInventory(request).observe(getViewLifecycleOwner(), resource -> { + setButtonsEnabled(true); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), viewModel.isEditing() ? "Inventory updated" : "Inventory created", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Shows a confirmation dialog before deleting an inventory item. - */ private void confirmDelete() { new AlertDialog.Builder(requireContext()) .setTitle("Delete inventory item?") @@ -241,12 +179,9 @@ public class InventoryDetailFragment extends Fragment { .show(); } - /** - * Sends a request to the API to delete the inventory item. - */ private void deleteInventory() { setButtonsEnabled(false); - inventoryViewModel.deleteInventory(inventoryId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteInventory().observe(getViewLifecycleOwner(), resource -> { setButtonsEnabled(true); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); @@ -257,16 +192,10 @@ public class InventoryDetailFragment extends Fragment { }); } - /** - * Navigates back to the previous fragment. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Enables or disables action buttons. - */ private void setButtonsEnabled(boolean enabled) { binding.btnSaveInventory.setEnabled(enabled); binding.btnDeleteInventory.setEnabled(enabled); 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 6c44ca59..d260d723 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 @@ -26,12 +26,8 @@ import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.CustomerViewModel; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.PetDetailViewModel; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; import dagger.hilt.android.AndroidEntryPoint; @@ -43,23 +39,15 @@ import dagger.hilt.android.AndroidEntryPoint; public class PetDetailFragment extends Fragment { private FragmentPetDetailBinding binding; - private long petId; - private boolean isEditing = false; - - private PetViewModel viewModel; - private CustomerViewModel customerViewModel; - private StoreViewModel storeViewModel; - private List customerList = new ArrayList<>(); - private List storeList = new ArrayList<>(); + private PetDetailViewModel viewModel; + private Long selectedCustomerId = null; private Long selectedStoreId = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PetViewModel.class); - customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(PetDetailViewModel.class); } @Override @@ -74,8 +62,7 @@ public class PetDetailFragment extends Fragment { super.onViewCreated(view, savedInstanceState); setupSpinner(); - loadCustomers(); - loadStores(); + observeViewModel(); handleArguments(); //set button click listeners @@ -84,6 +71,23 @@ public class PetDetailFragment extends Fragment { binding.btnDeletePet.setOnClickListener(v -> deletePet()); } + private void observeViewModel() { + viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> updateCustomerSpinnerSelection()); + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> updateStoreSpinnerSelection()); + + viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setCustomerList(resource.data); + } + }); + + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setStoreList(resource.data); + } + }); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -113,14 +117,14 @@ public class PetDetailFragment extends Fragment { Long customerId = null; int customerPos = binding.spinnerCustomer.getSelectedItemPosition(); if (customerPos > 0) { // 0 means no customer for pet - customerId = customerList.get(customerPos - 1).getId(); + customerId = viewModel.getCustomerList().getValue().get(customerPos - 1).getId(); } // Get selected store Long storeId = null; int storePos = binding.spinnerStore.getSelectedItemPosition(); if (storePos > 0) { - storeId = storeList.get(storePos - 1).getId(); + storeId = viewModel.getStoreList().getValue().get(storePos - 1).getId(); } // Validation: If status is Available, a store must be selected @@ -150,31 +154,20 @@ public class PetDetailFragment extends Fragment { petDTO.setCustomerId(customerId); petDTO.setStoreId(storeId); - //check if the pet is being edited or added - if (isEditing) { - // Update existing pet - petDTO.setPetId(petId); - viewModel.updatePet(petId, petDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) petId); + viewModel.savePet(petDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + if (viewModel.isEditing()) { + ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) viewModel.getPetId()); Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); - navigateToPetList(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - // Add new pet - viewModel.createPet(petDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { + } else { ActivityLogger.log(requireContext(), "Added new Pet: " + name); Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show(); - navigateToPetList(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } + navigateToPetList(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } /** @@ -182,9 +175,9 @@ public class PetDetailFragment extends Fragment { */ private void deletePet() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () -> - viewModel.deletePet(petId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deletePet().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) petId); + ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) viewModel.getPetId()); Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); navigateToPetList(); } else if (resource.status == Resource.Status.ERROR) { @@ -211,30 +204,23 @@ public class PetDetailFragment extends Fragment { * Handles arguments passed to the fragment to determine if it's in edit or add mode. */ private void handleArguments() { - // Pet is being edited if the bundle contains a petId if (getArguments() != null && getArguments().containsKey("petId")) { - // Get pet data from arguments and populate fields - isEditing = true; - petId = getArguments().getLong("petId"); + long petId = getArguments().getLong("petId"); + viewModel.setPetId(petId); binding.tvMode.setText("Edit Pet"); binding.tvPetId.setText("ID: " + petId); binding.tvPetId.setVisibility(View.VISIBLE); binding.btnDeletePet.setVisibility(View.VISIBLE); - // Disable species and breed fields in edit mode UIUtils.setViewsEnabled(false, binding.etPetSpecies, binding.etPetBreed); - loadPetData(); } else { - // Pet is being added - // Set default values for add a new pet - isEditing = false; + viewModel.setPetId(-1); binding.tvMode.setText("Add Pet"); binding.tvPetId.setVisibility(View.GONE); binding.btnDeletePet.setVisibility(View.GONE); binding.btnSavePet.setText("Add"); - // Enable species and breed fields in edit mode UIUtils.setViewsEnabled(true, binding.etPetSpecies, binding.etPetBreed); } } @@ -243,7 +229,7 @@ public class PetDetailFragment extends Fragment { * Fetches specific pet details from the backend using the ID. */ private void loadPetData() { - viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadPet().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { PetDTO p = resource.data; @@ -267,30 +253,6 @@ public class PetDetailFragment extends Fragment { }); } - /** - * Fetches the list of customers and populates the spinner. - */ - private void loadCustomers() { - customerViewModel.getCustomerDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data; - updateCustomerSpinnerSelection(); - } - }); - } - - /** - * Fetches the list of stores and populates the spinner. - */ - private void loadStores() { - storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data; - updateStoreSpinnerSelection(); - } - }); - } - /** * Updates the customer spinner with the current list and sets the selection if needed. */ @@ -298,7 +260,7 @@ public class PetDetailFragment extends Fragment { SpinnerUtils.populateSpinner( requireContext(), binding.spinnerCustomer, - customerList, + viewModel.getCustomerList().getValue(), DropdownDTO::getLabel, "No Owner", selectedCustomerId, @@ -313,7 +275,7 @@ public class PetDetailFragment extends Fragment { SpinnerUtils.populateSpinner( requireContext(), binding.spinnerStore, - storeList, + viewModel.getStoreList().getValue(), DropdownDTO::getLabel, "None", selectedStoreId, @@ -333,11 +295,9 @@ public class PetDetailFragment extends Fragment { public void onItemSelected(AdapterView parent, View view, int position, long id) { String status = parent.getItemAtPosition(position).toString(); - // Clear any existing error icons when status changes clearSpinnerError(binding.spinnerCustomer); clearSpinnerError(binding.spinnerStore); - //Disable the customer spinner if the status is "Available" if ("Available".equalsIgnoreCase(status)) { binding.spinnerCustomer.setSelection(0); UIUtils.setViewsEnabled(false, binding.spinnerCustomer); @@ -345,7 +305,6 @@ public class PetDetailFragment extends Fragment { UIUtils.setViewsEnabled(true, binding.spinnerCustomer); } - //Disable the store spinner if the status is "Owned" if ("Owned".equalsIgnoreCase(status)) { binding.spinnerStore.setSelection(0); UIUtils.setViewsEnabled(false, binding.spinnerStore); @@ -360,9 +319,6 @@ public class PetDetailFragment extends Fragment { }); } - /** - * Clears error messages from a Spinner's selected view. - */ private void clearSpinnerError(Spinner spinner) { View selectedView = spinner.getSelectedView(); if (selectedView instanceof TextView) { 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 d2527d71..2d07c280 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 @@ -17,7 +17,7 @@ import com.example.petstoremobile.api.*; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentProductDetailBinding; import com.example.petstoremobile.dtos.*; -import com.example.petstoremobile.viewmodels.ProductViewModel; +import com.example.petstoremobile.viewmodels.ProductDetailViewModel; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; @@ -31,7 +31,6 @@ import java.math.BigDecimal; import java.util.*; import javax.inject.Inject; - import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; @@ -46,29 +45,22 @@ import okhttp3.RequestBody; public class ProductDetailFragment extends Fragment { private FragmentProductDetailBinding binding; + private ProductDetailViewModel viewModel; + private ImagePickerHelper imagePickerHelper; - private long prodId = -1; - private boolean isEditing = false; private long preselectedCategoryId = -1; private boolean hasImage = false; private boolean isImageChanged = false; private boolean isImageRemoved = false; - - private List categoryList = new ArrayList<>(); private Uri photoUri; - private ProductViewModel viewModel; - private ImagePickerHelper imagePickerHelper; @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; - /** - * Initializes activity launchers and the ImagePickerHelper. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ProductViewModel.class); + viewModel = new ViewModelProvider(this).get(ProductDetailViewModel.class); imagePickerHelper = new ImagePickerHelper(this, "product_photo.jpg", new ImagePickerHelper.ImagePickerListener() { @Override @@ -95,9 +87,6 @@ public class ProductDetailFragment extends Fragment { }); } - /** - * Inflates the layout. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -105,14 +94,11 @@ public class ProductDetailFragment extends Fragment { return binding.getRoot(); } - /** - * Sets up UI components and listeners after the view is created. - */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - loadCategories(); + observeViewModel(); handleArguments(); binding.btnProductBack.setOnClickListener(v -> navigateBack()); @@ -121,34 +107,33 @@ public class ProductDetailFragment extends Fragment { binding.ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage)); } + private void observeViewModel() { + viewModel.getCategoryList().observe(getViewLifecycleOwner(), list -> updateCategorySpinner()); + + viewModel.loadCategories().observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setCategoryList(resource.data.getContent()); + } + }); + } + + private void updateCategorySpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, viewModel.getCategoryList().getValue(), + CategoryDTO::getCategoryName, "-- Select Category --", + preselectedCategoryId, CategoryDTO::getCategoryId); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Fetches all product categories for the selection spinner. - */ - private void loadCategories() { - viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - categoryList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList, - CategoryDTO::getCategoryName, "-- Select Category --", - preselectedCategoryId, CategoryDTO::getCategoryId); - } - }); - } - - /** - * Checks if the fragment was opened with existing product data for editing. - */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("prodId")) { - isEditing = true; - prodId = a.getLong("prodId"); + long prodId = a.getLong("prodId"); + viewModel.setProdId(prodId); binding.tvProductMode.setText("Edit Product"); binding.tvProductId.setText("ID: " + prodId); binding.tvProductId.setVisibility(View.VISIBLE); @@ -156,6 +141,7 @@ public class ProductDetailFragment extends Fragment { loadProductData(); loadProductImage(); } else { + viewModel.setProdId(-1); binding.tvProductMode.setText("Add Product"); binding.btnDeleteProduct.setVisibility(View.GONE); binding.tvProductId.setVisibility(View.GONE); @@ -163,11 +149,8 @@ public class ProductDetailFragment extends Fragment { } } - /** - * Loads the product data from the backend. - */ private void loadProductData() { - viewModel.getProductById(prodId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadProduct().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { ProductDTO p = resource.data; @@ -175,24 +158,15 @@ public class ProductDetailFragment extends Fragment { binding.etProductDesc.setText(p.getProdDesc()); binding.etProductPrice.setText(p.getProdPrice() != null ? p.getProdPrice().toString() : ""); preselectedCategoryId = p.getCategoryId() != null ? p.getCategoryId() : -1; - - // Refresh spinner selection once data is loaded - if (!categoryList.isEmpty()) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList, - CategoryDTO::getCategoryName, "-- Select Category --", - preselectedCategoryId, CategoryDTO::getCategoryId); - } + updateCategorySpinner(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load product: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } - /** - * Loads the product image from the backend. - */ private void loadProductImage() { - String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId); + String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, viewModel.getProdId()); String token = tokenManager.getToken(); GlideUtils.loadImageWithToken(requireContext(), binding.ivProductImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() { @@ -208,12 +182,9 @@ public class ProductDetailFragment extends Fragment { }); } - /** - * Performs image related actions (upload/delete) after product details are saved. - */ private void performPendingImageActions(String successMsg) { if (isImageRemoved) { - viewModel.deleteProductImage(prodId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteProductImage().observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status != Resource.Status.LOADING) { if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); @@ -231,9 +202,6 @@ public class ProductDetailFragment extends Fragment { } } - /** - * Uploads the selected image file to the server. - */ private void uploadProductImageAndNavigate(Uri uri, String successMsg) { File file = FileUtils.getFileFromUri(requireContext(), uri); if (file == null) { @@ -245,7 +213,7 @@ public class ProductDetailFragment extends Fragment { RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); - viewModel.uploadProductImage(prodId, body).observe(getViewLifecycleOwner(), resource -> { + viewModel.uploadProductImage(body).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status != Resource.Status.LOADING) { if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); @@ -257,9 +225,6 @@ public class ProductDetailFragment extends Fragment { }); } - /** - * Validates input fields and saves product information to the backend. - */ private void saveProduct() { if (!InputValidator.isNotEmpty(binding.etProductName, "Product Name")) return; @@ -276,39 +241,26 @@ public class ProductDetailFragment extends Fragment { String desc = binding.etProductDesc.getText().toString().trim(); BigDecimal price = new BigDecimal(binding.etProductPrice.getText().toString().trim()); - CategoryDTO category = categoryList.get(binding.spinnerProductCategory.getSelectedItemPosition() - 1); + CategoryDTO category = viewModel.getCategoryList().getValue().get(binding.spinnerProductCategory.getSelectedItemPosition() - 1); ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); - if (isEditing) { - viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS) { - performPendingImageActions("Updated"); - } else { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + viewModel.saveProduct(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { + if (resource.data != null) { + viewModel.setProdId(resource.data.getProdId()); } + performPendingImageActions(viewModel.isEditing() ? "Updated" : "Saved"); + } else { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } else { - viewModel.createProduct(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - prodId = resource.data.getProdId(); - performPendingImageActions("Saved"); - } else { - Toast.makeText(getContext(), "Error saving: " + resource.message, Toast.LENGTH_SHORT).show(); - } - } - }); - } + } + }); } - /** - * Displays a confirmation dialog before deleting the product. - */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () -> - viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteProduct().observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS) { navigateBack(); } else if (resource != null && resource.status == Resource.Status.ERROR) { @@ -317,9 +269,6 @@ public class ProductDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous fragment. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index aa570d13..a0e7d1a6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -15,9 +15,7 @@ import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; -import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; -import com.example.petstoremobile.viewmodels.ProductViewModel; -import com.example.petstoremobile.viewmodels.SupplierViewModel; +import com.example.petstoremobile.viewmodels.ProductSupplierDetailViewModel; import java.math.BigDecimal; import java.util.*; @@ -31,26 +29,15 @@ import dagger.hilt.android.AndroidEntryPoint; public class ProductSupplierDetailFragment extends Fragment { private FragmentProductSupplierDetailBinding binding; + private ProductSupplierDetailViewModel viewModel; - private boolean isEditing = false; - private long editProductId = -1; - private long editSupplierId = -1; private long preselectedProductId = -1; private long preselectedSupplierId = -1; - private List productList = new ArrayList<>(); - private List supplierList = new ArrayList<>(); - - private ProductSupplierViewModel psViewModel; - private ProductViewModel productViewModel; - private SupplierViewModel supplierViewModel; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - psViewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class); - productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); - supplierViewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + viewModel = new ViewModelProvider(this).get(ProductSupplierDetailViewModel.class); } @Override @@ -63,6 +50,7 @@ public class ProductSupplierDetailFragment extends Fragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + observeViewModel(); loadSpinnersData(); handleArguments(); @@ -71,81 +59,59 @@ public class ProductSupplierDetailFragment extends Fragment { binding.btnDeletePS.setOnClickListener(v -> confirmDelete()); } + private void observeViewModel() { + viewModel.getProductList().observe(getViewLifecycleOwner(), list -> refreshProductSpinner()); + viewModel.getSupplierList().observe(getViewLifecycleOwner(), list -> refreshSupplierSpinner()); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Fetches products and suppliers to populate the spinners. - */ private void loadSpinnersData() { - loadProducts(); - loadSuppliers(); - } - - /** - * Loads the list of products from the API. - */ - private void loadProducts() { - productViewModel.getAllProducts(null, null, 0, 200, "prodName").observe(getViewLifecycleOwner(), resource -> { + viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - productList = resource.data.getContent(); - refreshProductSpinner(); + viewModel.setProductList(resource.data.getContent()); + } + }); + viewModel.loadSuppliers().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setSupplierList(resource.data.getContent()); } }); } private void refreshProductSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, viewModel.getProductList().getValue(), ProductDTO::getProdName, "-- Select Product --", preselectedProductId, ProductDTO::getProdId); } - /** - * Loads the list of suppliers from the API. - */ - private void loadSuppliers() { - supplierViewModel.getAllSuppliers(0, 200, null, "supCompany").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - supplierList = resource.data.getContent(); - refreshSupplierSpinner(); - } - }); - } - private void refreshSupplierSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, viewModel.getSupplierList().getValue(), SupplierDTO::getSupCompany, "-- Select Supplier --", preselectedSupplierId, SupplierDTO::getSupId); } - /** - * Handles arguments to determine if the fragment is in edit or add mode. - */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("productId") && a.containsKey("supplierId")) { - isEditing = true; - editProductId = a.getLong("productId"); - editSupplierId = a.getLong("supplierId"); - preselectedProductId = editProductId; - preselectedSupplierId = editSupplierId; + long productId = a.getLong("productId"); + long supplierId = a.getLong("supplierId"); + viewModel.setEditMode(productId, supplierId); + preselectedProductId = productId; + preselectedSupplierId = supplierId; binding.tvPSMode.setText("Edit Product Supplier"); binding.btnDeletePS.setVisibility(View.VISIBLE); - } else { binding.tvPSMode.setText("Add Product Supplier"); binding.btnDeletePS.setVisibility(View.GONE); } } - - /** - * Validates input and saves the product-supplier to the backend. - */ private void save() { if (binding.spinnerPSProduct.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return; @@ -159,40 +125,25 @@ public class ProductSupplierDetailFragment extends Fragment { return; } - ProductDTO product = productList.get(binding.spinnerPSProduct.getSelectedItemPosition() - 1); - SupplierDTO supplier = supplierList.get(binding.spinnerPSSupplier.getSelectedItemPosition() - 1); + ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerPSProduct.getSelectedItemPosition() - 1); + SupplierDTO supplier = viewModel.getSupplierList().getValue().get(binding.spinnerPSSupplier.getSelectedItemPosition() - 1); BigDecimal cost = new BigDecimal(binding.etPSCost.getText().toString().trim()); - ProductSupplierDTO dto = new ProductSupplierDTO( - product.getProdId(), supplier.getSupId(), cost); + ProductSupplierDTO dto = new ProductSupplierDTO(product.getProdId(), supplier.getSupId(), cost); - if (isEditing) { - psViewModel.updateProductSupplier(editProductId, editSupplierId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - psViewModel.createProductSupplier(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } + viewModel.saveProductSupplier(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), viewModel.isEditing() ? "Updated" : "Saved", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Shows a confirmation dialog before deleting a product-supplier relationship. - */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier", () -> - psViewModel.deleteProductSupplier(editProductId, editSupplierId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteProductSupplier().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); navigateBack(); @@ -202,9 +153,6 @@ public class ProductSupplierDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java index eb69bd16..90ebf645 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java @@ -14,7 +14,7 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.databinding.FragmentPurchaseOrderDetailBinding; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; +import com.example.petstoremobile.viewmodels.PurchaseOrderDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -25,13 +25,13 @@ import dagger.hilt.android.AndroidEntryPoint; public class PurchaseOrderDetailFragment extends Fragment { private FragmentPurchaseOrderDetailBinding binding; - private PurchaseOrderViewModel viewModel; + private PurchaseOrderDetailViewModel viewModel; private long purchaseOrderId; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); + viewModel = new ViewModelProvider(this).get(PurchaseOrderDetailViewModel.class); } /** @@ -67,7 +67,7 @@ public class PurchaseOrderDetailFragment extends Fragment { } private void loadPurchaseOrderData() { - viewModel.getPurchaseOrderById(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadPurchaseOrder(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { PurchaseOrderDTO po = resource.data; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java index 8d05252c..d94b0041 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java @@ -12,7 +12,8 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentRefundBinding; import com.example.petstoremobile.dtos.SaleDTO; -import com.example.petstoremobile.viewmodels.SaleViewModel; +import com.example.petstoremobile.viewmodels.RefundViewModel; +import com.example.petstoremobile.utils.Resource; import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.math.RoundingMode; @@ -22,53 +23,23 @@ import java.util.*; public class RefundFragment extends Fragment { private FragmentRefundBinding binding; - private SaleViewModel saleViewModel; - private SaleDTO currentSale; - private List allSales = new ArrayList<>(); - - // Items available to refund (after accounting for previous refunds) - private List availableItems = new ArrayList<>(); - // Items user has added to refund cart - private List refundCart = new ArrayList<>(); + private RefundViewModel viewModel; private final String[] PAYMENT_METHODS = {"Cash", "Card"}; - // Inner class to track refund items - static class RefundItem { - long prodId; - String productName; - int quantity; - BigDecimal unitPrice; - - RefundItem(long prodId, String productName, int quantity, BigDecimal unitPrice) { - this.prodId = prodId; - this.productName = productName; - this.quantity = quantity; - this.unitPrice = unitPrice; - } - - BigDecimal getTotal() { - return unitPrice != null - ? unitPrice.multiply(BigDecimal.valueOf(quantity)) - : BigDecimal.ZERO; - } - } - @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentRefundBinding.inflate(inflater, container, false); - saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); + viewModel = new ViewModelProvider(this).get(RefundViewModel.class); setupSpinner(); + observeViewModel(); loadAllSales(); - // Pre-fill sale ID if passed from SaleFragment Bundle args = getArguments(); if (args != null && args.containsKey("saleId")) { - long saleId = args.getLong("saleId"); - binding.etRefundSaleId.setText(String.valueOf(saleId)); - // Auto-load after sales are fetched + binding.etRefundSaleId.setText(String.valueOf(args.getLong("saleId"))); } binding.btnLoadSale.setOnClickListener(v -> loadSale()); @@ -83,27 +54,25 @@ public class RefundFragment extends Fragment { android.R.layout.simple_spinner_item, PAYMENT_METHODS)); } + private void observeViewModel() { + viewModel.getAvailableItems().observe(getViewLifecycleOwner(), items -> renderOriginalItems()); + viewModel.getRefundCart().observe(getViewLifecycleOwner(), cart -> { + renderRefundCart(); + updateRefundTotal(); + renderOriginalItems(); // Re-render to reflect quantities in cart + }); + } + private void loadAllSales() { - saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc") - .observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - if (resource.data != null) { - allSales = resource.data.getContent(); - // Auto-load if saleId was pre-filled - Bundle args = getArguments(); - if (args != null && args.containsKey("saleId")) { - loadSale(); - } - } - break; - case ERROR: - Log.e("Refund", "Failed to load sales: " + resource.message); - break; - } - } - }); + viewModel.loadAllSales().observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setAllSales(resource.data.getContent()); + Bundle args = getArguments(); + if (args != null && args.containsKey("saleId")) { + loadSale(); + } + } + }); } private void loadSale() { @@ -120,11 +89,12 @@ public class RefundFragment extends Fragment { return; } - // Find sale in loaded list SaleDTO found = null; - for (SaleDTO s : allSales) { - if (s.getSaleId() != null && s.getSaleId() == saleId) { - found = s; break; + if (viewModel.getAllSalesList() != null) { + for (SaleDTO s : viewModel.getAllSalesList()) { + if (s.getSaleId() != null && s.getSaleId() == saleId) { + found = s; break; + } } } @@ -139,9 +109,9 @@ public class RefundFragment extends Fragment { return; } - currentSale = found; + viewModel.setCurrentSale(found); + SaleDTO currentSale = viewModel.getCurrentSale(); - // Show sale info binding.tvSaleInfo.setVisibility(View.VISIBLE); binding.tvSaleInfo.setText("Sale #" + currentSale.getSaleId() + " | " + (currentSale.getSaleDate() != null @@ -151,7 +121,6 @@ public class RefundFragment extends Fragment { + " | Total: $" + currentSale.getTotalAmount() + " | Payment: " + currentSale.getPaymentMethod()); - // Pre-select payment method if (currentSale.getPaymentMethod() != null) { for (int i = 0; i < PAYMENT_METHODS.length; i++) { if (PAYMENT_METHODS[i].equalsIgnoreCase(currentSale.getPaymentMethod())) { @@ -160,85 +129,40 @@ public class RefundFragment extends Fragment { } } - // Build refundable items accounting for previous refunds - buildRefundableItems(); - - if (availableItems.isEmpty()) { - Toast.makeText(getContext(), - "This sale has no remaining refundable items", Toast.LENGTH_LONG).show(); + if (viewModel.getAvailableItems().getValue() == null || viewModel.getAvailableItems().getValue().isEmpty()) { + Toast.makeText(getContext(), "This sale has no remaining refundable items", Toast.LENGTH_LONG).show(); return; } - // Reset refund cart - refundCart.clear(); - - // Show cards binding.cardOriginalItems.setVisibility(View.VISIBLE); binding.cardRefundItems.setVisibility(View.VISIBLE); binding.cardPayment.setVisibility(View.VISIBLE); binding.btnProcessRefund.setVisibility(View.VISIBLE); - - renderOriginalItems(); - renderRefundCart(); - updateRefundTotal(); - } - - private void buildRefundableItems() { - availableItems.clear(); - if (currentSale.getItems() == null) return; - - // Find all previous refunds for this sale - Map alreadyRefunded = new HashMap<>(); - for (SaleDTO s : allSales) { - if (Boolean.TRUE.equals(s.getIsRefund()) - && currentSale.getSaleId().equals(s.getOriginalSaleId()) - && s.getItems() != null) { - for (SaleDTO.SaleItemDTO item : s.getItems()) { - if (item.getProdId() != null && item.getQuantity() != null) { - alreadyRefunded.merge(item.getProdId(), - Math.abs(item.getQuantity()), Integer::sum); - } - } - } - } - - // Build available items - for (SaleDTO.SaleItemDTO item : currentSale.getItems()) { - if (item.getProdId() == null || item.getQuantity() == null) continue; - int refunded = alreadyRefunded.getOrDefault(item.getProdId(), 0); - int remaining = item.getQuantity() - refunded; - if (remaining > 0) { - availableItems.add(new RefundItem( - item.getProdId(), - item.getProductName() != null ? item.getProductName() : "Unknown", - remaining, - item.getUnitPrice() - )); - } - } } private void renderOriginalItems() { binding.llOriginalItems.removeAllViews(); + List available = viewModel.getAvailableItems().getValue(); + if (available == null) return; - // Header addTableHeader(binding.llOriginalItems); - for (RefundItem item : availableItems) { - // Calculate pending in cart - int pendingQty = 0; - for (RefundItem r : refundCart) { - if (r.prodId == item.prodId) { pendingQty = r.quantity; break; } + for (RefundViewModel.RefundItem item : available) { + int inCart = 0; + if (viewModel.getRefundCart().getValue() != null) { + for (RefundViewModel.RefundItem r : viewModel.getRefundCart().getValue()) { + if (r.prodId == item.prodId) { inCart = r.quantity; break; } + } } - int displayQty = item.quantity - pendingQty; + int displayQty = item.quantity - inCart; if (displayQty <= 0) continue; LinearLayout row = buildItemRow( item.productName, displayQty, item.unitPrice, - true, // show add button - () -> showQuantityDialog(item) + true, + () -> showQuantityDialog(item, displayQty) ); binding.llOriginalItems.addView(row); } @@ -246,8 +170,9 @@ public class RefundFragment extends Fragment { private void renderRefundCart() { binding.llRefundItems.removeAllViews(); + List cart = viewModel.getRefundCart().getValue(); - if (refundCart.isEmpty()) { + if (cart == null || cart.isEmpty()) { TextView empty = new TextView(getContext()); empty.setText("No items added to refund yet"); empty.setTextColor(0xFF888780); @@ -258,18 +183,13 @@ public class RefundFragment extends Fragment { addTableHeader(binding.llRefundItems); - for (RefundItem item : refundCart) { + for (RefundViewModel.RefundItem item : cart) { LinearLayout row = buildItemRow( item.productName, item.quantity, item.unitPrice, - false, // show remove button - () -> { - refundCart.remove(item); - renderOriginalItems(); - renderRefundCart(); - updateRefundTotal(); - } + false, + () -> viewModel.removeFromCart(item) ); binding.llRefundItems.addView(row); } @@ -342,24 +262,10 @@ public class RefundFragment extends Fragment { return row; } - private void showQuantityDialog(RefundItem item) { - // Calculate how many are already in cart - int inCart = 0; - for (RefundItem r : refundCart) { - if (r.prodId == item.prodId) { inCart = r.quantity; break; } - } - int available = item.quantity - inCart; - if (available <= 0) { - Toast.makeText(getContext(), "All units already added to refund", - Toast.LENGTH_SHORT).show(); - return; - } - - // Build dialog + private void showQuantityDialog(RefundViewModel.RefundItem item, int available) { AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); builder.setTitle("Refund Quantity"); - builder.setMessage("Product: " + item.productName - + "\nAvailable: " + available); + builder.setMessage("Product: " + item.productName + "\nAvailable: " + available); EditText input = new EditText(getContext()); input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); @@ -377,36 +283,15 @@ public class RefundFragment extends Fragment { return; } if (qty <= 0) { - Toast.makeText(getContext(), "Quantity must be at least 1", - Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Quantity must be at least 1", Toast.LENGTH_SHORT).show(); return; } if (qty > available) { - Toast.makeText(getContext(), "Cannot exceed " + available, - Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Cannot exceed " + available, Toast.LENGTH_SHORT).show(); return; } - // Add or merge into cart - boolean merged = false; - for (int i = 0; i < refundCart.size(); i++) { - if (refundCart.get(i).prodId == item.prodId) { - RefundItem existing = refundCart.get(i); - refundCart.set(i, new RefundItem(existing.prodId, - existing.productName, - existing.quantity + qty, - existing.unitPrice)); - merged = true; break; - } - } - if (!merged) { - refundCart.add(new RefundItem(item.prodId, item.productName, - qty, item.unitPrice)); - } - - renderOriginalItems(); - renderRefundCart(); - updateRefundTotal(); + viewModel.addToCart(item, qty); }); builder.setNegativeButton("Cancel", null); @@ -415,31 +300,31 @@ public class RefundFragment extends Fragment { private void updateRefundTotal() { BigDecimal total = BigDecimal.ZERO; - for (RefundItem item : refundCart) total = total.add(item.getTotal()); + List cart = viewModel.getRefundCart().getValue(); + if (cart != null) { + for (RefundViewModel.RefundItem item : cart) total = total.add(item.getTotal()); + } binding.tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP)); } private void processRefund() { - if (currentSale == null) { + if (viewModel.getCurrentSale() == null) { Toast.makeText(getContext(), "Load a sale first", Toast.LENGTH_SHORT).show(); return; } - if (refundCart.isEmpty()) { - Toast.makeText(getContext(), "Add at least one item to refund", - Toast.LENGTH_SHORT).show(); + if (viewModel.getRefundCart().getValue() == null || viewModel.getRefundCart().getValue().isEmpty()) { + Toast.makeText(getContext(), "Add at least one item to refund", Toast.LENGTH_SHORT).show(); return; } String payment = PAYMENT_METHODS[binding.spinnerRefundPayment.getSelectedItemPosition()]; - - // Confirm dialog BigDecimal total = BigDecimal.ZERO; - for (RefundItem item : refundCart) total = total.add(item.getTotal()); + for (RefundViewModel.RefundItem item : viewModel.getRefundCart().getValue()) total = total.add(item.getTotal()); final BigDecimal finalTotal = total; new AlertDialog.Builder(requireContext()) .setTitle("Confirm Refund") - .setMessage("Process refund for Sale #" + currentSale.getSaleId() + .setMessage("Process refund for Sale #" + viewModel.getCurrentSale().getSaleId() + "?\nRefund amount: $" + finalTotal.setScale(2, RoundingMode.HALF_UP)) .setPositiveButton("Yes", (d, w) -> submitRefund(payment)) .setNegativeButton("No", null) @@ -447,41 +332,13 @@ public class RefundFragment extends Fragment { } private void submitRefund(String payment) { - // Build sale items list - List items = new ArrayList<>(); - for (RefundItem item : refundCart) { - // Backend expects negative quantity for refunds - items.add(new SaleDTO.SaleItemDTO(item.prodId, -item.quantity)); - } - - SaleDTO dto = new SaleDTO( - currentSale.getStoreId(), - payment, - items, - true, // isRefund = true - currentSale.getSaleId(), // originalSaleId - null // no customer needed - ); - - Log.d("REFUND", "Submitting refund for saleId=" + currentSale.getSaleId() - + " items=" + items.size()); - - saleViewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { + viewModel.submitRefund(payment).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { - switch (resource.status) { - case SUCCESS: - if (resource.data != null) { - Toast.makeText(getContext(), - "Refund #" + resource.data.getSaleId() + " processed successfully!", - Toast.LENGTH_LONG).show(); - navigateBack(); - } - break; - case ERROR: - Log.e("REFUND", "Error: " + resource.message); - Toast.makeText(getContext(), "Error: " + resource.message, - Toast.LENGTH_LONG).show(); - break; + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Refund processed successfully!", Toast.LENGTH_LONG).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); } } }); 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 690a2fa1..7ae18823 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 @@ -11,7 +11,7 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentSaleDetailBinding; import com.example.petstoremobile.dtos.*; -import com.example.petstoremobile.viewmodels.*; +import com.example.petstoremobile.viewmodels.SaleDetailViewModel; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; @@ -24,18 +24,7 @@ import java.util.*; public class SaleDetailFragment extends Fragment { private FragmentSaleDetailBinding binding; - private SaleViewModel saleViewModel; - private StoreViewModel storeViewModel; - private CustomerViewModel customerViewModel; - private ProductViewModel productViewModel; - - private boolean viewOnly = false; - private long saleId = -1; - - private List storeList = new ArrayList<>(); - private List customerList = new ArrayList<>(); - private List productList = new ArrayList<>(); - private List cartItems = new ArrayList<>(); + private SaleDetailViewModel viewModel; private final String[] PAYMENT_METHODS = { "Cash", "Card"}; @@ -43,17 +32,14 @@ public class SaleDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentSaleDetailBinding.inflate(inflater, container, false); - - saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); - customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); - productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); + viewModel = new ViewModelProvider(this).get(SaleDetailViewModel.class); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPaymentMethod, PAYMENT_METHODS); + observeViewModel(); handleArguments(); - if (!viewOnly) { + if (!viewModel.isViewOnly()) { loadData(); setupAddItem(); } @@ -65,20 +51,42 @@ public class SaleDetailFragment extends Fragment { return binding.getRoot(); } + private void observeViewModel() { + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, list, + DropdownDTO::getLabel, "-- Select Store --", -1L, DropdownDTO::getId); + }); + + viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleCustomer, list, + DropdownDTO::getLabel, "-- No Customer --", -1L, DropdownDTO::getId); + }); + + viewModel.getProductList().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleProduct, list, + ProductDTO::getProdName, "Select Product", -1L, ProductDTO::getProdId); + }); + + viewModel.getCartItems().observe(getViewLifecycleOwner(), items -> { + renderCartItems(); + updateTotal(); + }); + } + private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("saleId")) { - saleId = a.getLong("saleId"); - viewOnly = a.getBoolean("viewOnly", false); + long saleId = a.getLong("saleId"); + boolean viewOnly = a.getBoolean("viewOnly", false); + viewModel.setSaleId(saleId, viewOnly); + binding.tvSaleMode.setText("Sale #" + saleId); binding.tvSaleDetailId.setText("ID: " + saleId); - // Show refund button for existing non-refund sales if (!a.getBoolean("isRefund", false)) { binding.btnRefundSale.setVisibility(View.VISIBLE); } - // Hide save and input controls for view only if (viewOnly) { binding.btnSaveSale.setVisibility(View.GONE); UIUtils.setViewsEnabled(false, @@ -89,9 +97,9 @@ public class SaleDetailFragment extends Fragment { binding.llExtraInfo.setVisibility(View.VISIBLE); } - // Load sale details loadSaleDetails(); } else { + viewModel.setSaleId(-1, false); binding.tvSaleMode.setText("New Sale"); binding.tvSaleDetailId.setVisibility(View.GONE); binding.btnRefundSale.setVisibility(View.GONE); @@ -100,82 +108,47 @@ public class SaleDetailFragment extends Fragment { } private void loadData() { - loadStores(); - loadCustomers(); - loadProducts(); - } - - private void loadStores() { - storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data; - if (binding != null) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, storeList, - DropdownDTO::getLabel, "-- Select Store --", -1L, DropdownDTO::getId); - } - } + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setStoreList(resource.data); }); - } - - private void loadCustomers() { - customerViewModel.getCustomerDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data; - if (binding != null) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleCustomer, customerList, - DropdownDTO::getLabel, "-- No Customer --", -1L, DropdownDTO::getId); - } - } + viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setCustomerList(resource.data); }); - } - - private void loadProducts() { - productViewModel.getAllProducts(null, null, 0, 200, null).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - productList = resource.data.getContent(); - if (binding != null) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleProduct, productList, - ProductDTO::getProdName, "Select Product", -1L, ProductDTO::getProdId); - } - } + viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setProductList(resource.data.getContent()); }); } private void loadSaleDetails() { - saleViewModel.getSaleById(saleId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadSaleDetails().observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { SaleDTO sale = resource.data; - if (binding != null) { - binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount()); - binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount())); - - if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { - binding.llCouponDiscount.setVisibility(View.VISIBLE); - binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount()); - } else { - binding.llCouponDiscount.setVisibility(View.GONE); - } + binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount()); + binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount())); + + if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + binding.llCouponDiscount.setVisibility(View.VISIBLE); + binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount()); + } else { + binding.llCouponDiscount.setVisibility(View.GONE); + } - if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { - binding.llEmployeeDiscount.setVisibility(View.VISIBLE); - binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount()); - } else { - binding.llEmployeeDiscount.setVisibility(View.GONE); - } + if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + binding.llEmployeeDiscount.setVisibility(View.VISIBLE); + binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount()); + } else { + binding.llEmployeeDiscount.setVisibility(View.GONE); + } - binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "—"); - binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0)); + binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "—"); + binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0)); - SpinnerUtils.setSelectionByValue(binding.spinnerPaymentMethod, sale.getPaymentMethod()); + SpinnerUtils.setSelectionByValue(binding.spinnerPaymentMethod, sale.getPaymentMethod()); - // Display items - if (sale.getItems() != null) { - binding.llSaleItems.removeAllViews(); - for (SaleDTO.SaleItemDTO item : sale.getItems()) { - addItemRow(item.getProductName(), - Math.abs(item.getQuantity()), - item.getUnitPrice()); - } + if (sale.getItems() != null) { + binding.llSaleItems.removeAllViews(); + for (SaleDTO.SaleItemDTO item : sale.getItems()) { + addItemRow(item.getProductName(), Math.abs(item.getQuantity()), item.getUnitPrice()); } } } @@ -194,31 +167,46 @@ public class SaleDetailFragment extends Fragment { return; } int qty; - try { - qty = Integer.parseInt(qtyStr); - } catch (Exception e) { + try { qty = Integer.parseInt(qtyStr); } + catch (Exception e) { binding.etSaleQuantity.setError("Invalid quantity"); return; } - ProductDTO product = productList.get(binding.spinnerSaleProduct.getSelectedItemPosition() - 1); + ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerSaleProduct.getSelectedItemPosition() - 1); - // Check if product already in cart - for (SaleDTO.SaleItemDTO existing : cartItems) { + for (SaleDTO.SaleItemDTO existing : viewModel.getCartItems().getValue()) { if (existing.getProdId().equals(product.getProdId())) { Toast.makeText(getContext(), "Product already added", Toast.LENGTH_SHORT).show(); return; } } - SaleDTO.SaleItemDTO item = new SaleDTO.SaleItemDTO(product.getProdId(), qty); - cartItems.add(item); - addItemRow(product.getProdName(), qty, product.getProdPrice()); - updateTotal(); + viewModel.addToCart(new SaleDTO.SaleItemDTO(product.getProdId(), qty)); binding.etSaleQuantity.setText(""); }); } + private void renderCartItems() { + binding.llSaleItems.removeAllViews(); + List items = viewModel.getCartItems().getValue(); + List products = viewModel.getProductList().getValue(); + if (items == null || products == null) return; + + for (SaleDTO.SaleItemDTO item : items) { + String name = "Unknown"; + BigDecimal price = BigDecimal.ZERO; + for (ProductDTO p : products) { + if (p.getProdId().equals(item.getProdId())) { + name = p.getProdName(); + price = p.getProdPrice(); + break; + } + } + addItemRow(name, item.getQuantity(), price); + } + } + private void addItemRow(String name, int qty, BigDecimal price) { if (getContext() == null) return; LinearLayout row = new LinearLayout(getContext()); @@ -226,18 +214,15 @@ public class SaleDetailFragment extends Fragment { row.setPadding(0, 8, 0, 8); TextView tvName = new TextView(getContext()); - tvName.setLayoutParams(new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)); + tvName.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)); tvName.setText(name); TextView tvQty = new TextView(getContext()); - tvQty.setLayoutParams(new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvQty.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); tvQty.setText("x" + qty); TextView tvPrice = new TextView(getContext()); - tvPrice.setLayoutParams(new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvPrice.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); tvPrice.setText(price != null ? "$" + price : ""); row.addView(tvName); @@ -247,16 +232,7 @@ public class SaleDetailFragment extends Fragment { } private void updateTotal() { - BigDecimal total = BigDecimal.ZERO; - for (SaleDTO.SaleItemDTO item : cartItems) { - for (ProductDTO p : productList) { - if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) { - total = total.add(p.getProdPrice() - .multiply(BigDecimal.valueOf(item.getQuantity()))); - break; - } - } - } + BigDecimal total = viewModel.calculateSubtotal(); binding.tvSaleSubtotal.setText("$" + total); binding.tvSaleDetailTotal.setText("Total: $" + total); } @@ -266,40 +242,28 @@ public class SaleDetailFragment extends Fragment { Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return; } - if (cartItems.isEmpty()) { + if (viewModel.getCartItems().getValue() == null || viewModel.getCartItems().getValue().isEmpty()) { Toast.makeText(getContext(), "Add at least one item", Toast.LENGTH_SHORT).show(); return; } - DropdownDTO store = storeList.get(binding.spinnerSaleStore.getSelectedItemPosition() - 1); + DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerSaleStore.getSelectedItemPosition() - 1); String payment = PAYMENT_METHODS[binding.spinnerPaymentMethod.getSelectedItemPosition()]; - // Optional customer Long customerId = null; if (binding.spinnerSaleCustomer.getSelectedItemPosition() > 0) { - customerId = customerList.get(binding.spinnerSaleCustomer.getSelectedItemPosition() - 1) - .getId(); + customerId = viewModel.getCustomerList().getValue().get(binding.spinnerSaleCustomer.getSelectedItemPosition() - 1).getId(); } - SaleDTO dto = new SaleDTO( - store.getId(), - payment, - cartItems, - false, - null, - customerId); + SaleDTO dto = new SaleDTO(store.getId(), payment, viewModel.getCartItems().getValue(), false, null, customerId); - saleViewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { + viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { - switch (resource.status) { - case SUCCESS: - Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); - navigateBack(); - break; - case ERROR: - Log.e("SALE_SAVE", "Error: " + resource.message); - DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message); - break; + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message); } } }); @@ -309,7 +273,7 @@ public class SaleDetailFragment extends Fragment { DialogUtils.showConfirmDialog(requireContext(), "Process Refund", "Are you sure you want to process a refund for this sale?", () -> { Bundle args = new Bundle(); - args.putLong("saleId", saleId); + args.putLong("saleId", viewModel.getSaleId()); NavHostFragment.findNavController(this).navigate(R.id.nav_refund, args); }); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index 49c51141..7fdaae9b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -20,7 +20,7 @@ import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.viewmodels.ServiceViewModel; +import com.example.petstoremobile.viewmodels.ServiceDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -31,15 +31,12 @@ import dagger.hilt.android.AndroidEntryPoint; public class ServiceDetailFragment extends Fragment { private FragmentServiceDetailBinding binding; - private long serviceId; - private boolean isEditing = false; - - private ServiceViewModel viewModel; + private ServiceDetailViewModel viewModel; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ServiceViewModel.class); + viewModel = new ViewModelProvider(this).get(ServiceDetailViewModel.class); } @Override @@ -53,10 +50,8 @@ public class ServiceDetailFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - //get controls from layout and display the view depending on the mode handleArguments(); - //set button click listeners binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSaveService.setOnClickListener(v -> saveService()); binding.btnDeleteService.setOnClickListener(v -> deleteService()); @@ -68,63 +63,44 @@ public class ServiceDetailFragment extends Fragment { binding = null; } - /** - * Handles the saving of service data (adding or updating). - */ private void saveService() { - // Validates all fields using InputValidator if (!InputValidator.isNotEmpty(binding.etServiceName, "Service Name")) return; if (!InputValidator.isNotEmpty(binding.etServiceDesc, "Description")) return; if (!InputValidator.isPositiveInteger(binding.etServiceDuration, "Duration")) return; if (!InputValidator.isPositiveDecimal(binding.etServicePrice, "Price")) return; - //get all the values from the fields String name = binding.etServiceName.getText().toString().trim(); String desc = binding.etServiceDesc.getText().toString().trim(); int duration = Integer.parseInt(binding.etServiceDuration.getText().toString().trim()); double price = Double.parseDouble(binding.etServicePrice.getText().toString().trim()); - //create a service object to send to the API ServiceDTO serviceDTO = new ServiceDTO(); serviceDTO.setServiceName(name); serviceDTO.setServiceDesc(desc); serviceDTO.setServiceDuration(duration); serviceDTO.setServicePrice(price); - //check if the service is being edited or added - if (isEditing) { - // Update existing service - serviceDTO.setServiceId(serviceId); - viewModel.updateService(serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) serviceId); + viewModel.saveService(serviceDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + if (viewModel.isEditing()) { + ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) viewModel.getServiceId()); Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - viewModel.createService(serviceDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { + } else { ActivityLogger.log(requireContext(), "Added new Service: " + name); Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Displays a confirmation dialog and handles the deletion of a service. - */ private void deleteService() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () -> - viewModel.deleteService(serviceId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteService().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) serviceId); + ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) viewModel.getServiceId()); Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -133,30 +109,20 @@ public class ServiceDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Handles arguments passed to the fragment to determine if it's in edit or add mode. - */ private void handleArguments() { - // Service is being edited if the bundle contains a serviceId if (getArguments() != null && getArguments().containsKey("serviceId")) { - // Get service data from arguments and populate fields - isEditing = true; - serviceId = getArguments().getLong("serviceId"); + long serviceId = getArguments().getLong("serviceId"); + viewModel.setServiceId(serviceId); binding.tvMode.setText("Edit Service"); binding.tvServiceId.setText("ID: " + serviceId); binding.btnDeleteService.setVisibility(View.VISIBLE); loadServiceData(); } else { - // Service is being added - // Set default values for add a new service - isEditing = false; + viewModel.setServiceId(-1); binding.tvMode.setText("Add Service"); binding.tvServiceId.setVisibility(View.GONE); binding.btnDeleteService.setVisibility(View.GONE); @@ -164,11 +130,8 @@ public class ServiceDetailFragment extends Fragment { } } - /** - * Fetches specific service details from the backend using the ID. - */ private void loadServiceData() { - viewModel.getServiceById(serviceId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadService().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { ServiceDTO s = resource.data; 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 508282bc..0013a277 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 @@ -1,7 +1,6 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; -import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; @@ -12,16 +11,15 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentStaffDetailBinding; import com.example.petstoremobile.dtos.EmployeeDTO; -import com.example.petstoremobile.viewmodels.EmployeeViewModel; +import com.example.petstoremobile.viewmodels.StaffDetailViewModel; +import com.example.petstoremobile.utils.Resource; import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class StaffDetailFragment extends Fragment { private FragmentStaffDetailBinding binding; - private EmployeeViewModel employeeViewModel; - private long employeeId = -1; - private boolean isEditing = false; + private StaffDetailViewModel viewModel; private final String[] ROLES = {"STAFF", "ADMIN"}; private final String[] STATUSES = {"Active", "Inactive"}; @@ -30,7 +28,7 @@ public class StaffDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentStaffDetailBinding.inflate(inflater, container, false); - employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class); + viewModel = new ViewModelProvider(this).get(StaffDetailViewModel.class); setupSpinners(); handleArguments(); @@ -51,8 +49,8 @@ public class StaffDetailFragment extends Fragment { private void handleArguments() { Bundle a = getArguments(); if (a != null && a.getBoolean("isEditing", false)) { - isEditing = true; - employeeId = a.getLong("employeeId", -1); + long employeeId = a.getLong("employeeId", -1); + viewModel.setEmployeeId(employeeId, true); binding.tvStaffMode.setText("Edit Staff Account"); binding.tvStaffId.setText("ID: " + employeeId); @@ -64,7 +62,6 @@ public class StaffDetailFragment extends Fragment { binding.etStaffPhone.setText(a.getString("phone", "")); binding.btnDeleteStaff.setVisibility(View.VISIBLE); - // Pre-fill role String role = a.getString("role", "STAFF"); for (int i = 0; i < ROLES.length; i++) { if (ROLES[i].equals(role)) { @@ -73,13 +70,11 @@ public class StaffDetailFragment extends Fragment { } } - // Pre-fill status boolean active = a.getBoolean("active", true); binding.spinnerStaffStatus.setSelection(active ? 0 : 1); } else { - isEditing = false; - employeeId = -1; + viewModel.setEmployeeId(-1, false); binding.tvStaffMode.setText("Add Staff Account"); binding.btnDeleteStaff.setVisibility(View.GONE); binding.tvStaffId.setVisibility(View.GONE); @@ -97,12 +92,11 @@ public class StaffDetailFragment extends Fragment { String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()]; boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0; - // Validation if (username.isEmpty()) { binding.etStaffUsername.setError("Required"); return; } - if (!isEditing && password.isEmpty()) { + if (!viewModel.isEditing() && password.isEmpty()) { binding.etStaffPassword.setError("Required for new account"); return; } - if (!isEditing && password.length() < 6) { + if (!viewModel.isEditing() && password.length() < 6) { binding.etStaffPassword.setError("At least 6 characters"); return; } if (firstName.isEmpty()) { binding.etStaffFirstName.setError("Required"); return; } @@ -121,35 +115,16 @@ public class StaffDetailFragment extends Fragment { active ); - if (isEditing && employeeId > 0) { - employeeViewModel.updateEmployee(employeeId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - Toast.makeText(getContext(), "Updated successfully", Toast.LENGTH_SHORT).show(); - navigateBack(); - break; - case ERROR: - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); - break; - } + viewModel.saveEmployee(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), viewModel.isEditing() ? "Updated successfully" : "Staff account created", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); } - }); - } else { - employeeViewModel.createEmployee(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - Toast.makeText(getContext(), "Staff account created", Toast.LENGTH_SHORT).show(); - navigateBack(); - break; - case ERROR: - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); - break; - } - } - }); - } + } + }); } private void confirmDelete() { @@ -157,16 +132,13 @@ public class StaffDetailFragment extends Fragment { .setTitle("Delete Staff Account?") .setMessage("This will permanently delete this staff account.") .setPositiveButton("Yes", (d, w) -> - employeeViewModel.deleteEmployee(employeeId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteEmployee().observe(getViewLifecycleOwner(), resource -> { if (resource != null) { - switch (resource.status) { - case SUCCESS: - navigateBack(); - break; - case ERROR: - Toast.makeText(getContext(), "Delete failed: " + resource.message, - Toast.LENGTH_SHORT).show(); - break; + if (resource.status == Resource.Status.SUCCESS) { + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, + Toast.LENGTH_SHORT).show(); } } })) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index 4935cb8b..62c7c381 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -20,7 +20,7 @@ import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.SupplierViewModel; +import com.example.petstoremobile.viewmodels.SupplierDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -31,15 +31,12 @@ import dagger.hilt.android.AndroidEntryPoint; public class SupplierDetailFragment extends Fragment { private FragmentSupplierDetailBinding binding; - private long supId; - private boolean isEditing = false; - - private SupplierViewModel viewModel; + private SupplierDetailViewModel viewModel; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + viewModel = new ViewModelProvider(this).get(SupplierDetailViewModel.class); } @Override @@ -53,12 +50,9 @@ public class SupplierDetailFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // Add phone number formatting (CA) and limit length to 14 characters UIUtils.formatPhoneInput(binding.etSupPhone); - handleArguments(); - //set button click listeners binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSaveSupplier.setOnClickListener(v -> saveSupplier()); binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); @@ -70,25 +64,19 @@ public class SupplierDetailFragment extends Fragment { binding = null; } - /** - * Handles the saving of supplier data (adding or updating). - */ private void saveSupplier() { - // Validates all fields using InputValidator if (!InputValidator.isNotEmpty(binding.etSupCompany, "Company Name")) return; if (!InputValidator.isNotEmpty(binding.etSupContactFirstName, "First Name")) return; if (!InputValidator.isNotEmpty(binding.etSupContactLastName, "Last Name")) return; if (!InputValidator.isValidEmail(binding.etSupEmail)) return; if (!InputValidator.isValidPhone(binding.etSupPhone)) return; - //get all the values from the fields String company = binding.etSupCompany.getText().toString().trim(); String firstName = binding.etSupContactFirstName.getText().toString().trim(); String lastName = binding.etSupContactLastName.getText().toString().trim(); String email = binding.etSupEmail.getText().toString().trim(); String phone = binding.etSupPhone.getText().toString().trim(); - //create a supplier object to send to the API SupplierDTO supplierDTO = new SupplierDTO(); supplierDTO.setSupCompany(company); supplierDTO.setSupContactFirstName(firstName); @@ -96,41 +84,27 @@ public class SupplierDetailFragment extends Fragment { supplierDTO.setSupEmail(email); supplierDTO.setSupPhone(phone); - //check if the supplier is being edited or added - if (isEditing) { - // Update existing supplier - supplierDTO.setSupId(supId); - viewModel.updateSupplier(supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) supId); + viewModel.saveSupplier(supplierDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + if (viewModel.isEditing()) { + ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) viewModel.getSupId()); Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - // Add new supplier - viewModel.createSupplier(supplierDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { + } else { ActivityLogger.log(requireContext(), "Added new Supplier: " + company); Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Displays a confirmation dialog and handles the deletion of a supplier. - */ private void deleteSupplier() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () -> - viewModel.deleteSupplier(supId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteSupplier().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) supId); + ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) viewModel.getSupId()); Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -139,31 +113,21 @@ public class SupplierDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Handles arguments passed to the fragment to determine if it's in edit or add mode. - */ private void handleArguments() { - // Supplier is being edited if the bundle contains a supId if (getArguments() != null && getArguments().containsKey("supId")) { - // Get supplier data from arguments and populate fields - isEditing = true; - supId = getArguments().getLong("supId"); + long supId = getArguments().getLong("supId"); + viewModel.setSupId(supId); binding.tvMode.setText("Edit Supplier"); binding.tvSupId.setText("ID: " + supId); binding.tvSupId.setVisibility(View.VISIBLE); binding.btnDeleteSupplier.setVisibility(View.VISIBLE); loadSupplierData(); } else { - // Supplier is being added - // Set default values for add a new supplier - isEditing = false; + viewModel.setSupId(-1); binding.tvMode.setText("Add Supplier"); binding.tvSupId.setVisibility(View.GONE); binding.btnDeleteSupplier.setVisibility(View.GONE); @@ -171,11 +135,8 @@ public class SupplierDetailFragment extends Fragment { } } - /** - * Fetches specific supplier details from the backend using the ID. - */ private void loadSupplierData() { - viewModel.getSupplierById(supId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadSupplier().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { SupplierDTO s = resource.data; 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 new file mode 100644 index 00000000..f6e24cd6 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java @@ -0,0 +1,102 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.dtos.DropdownDTO; +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.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AdoptionDetailViewModel extends ViewModel { + private final AdoptionRepository adoptionRepository; + private final PetRepository petRepository; + private final CustomerRepository customerRepository; + private final StoreRepository storeRepository; + + private long adoptionId = -1; + private boolean isEditing = false; + + private final MutableLiveData> petList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> employeeList = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public AdoptionDetailViewModel(AdoptionRepository adoptionRepository, PetRepository petRepository, + CustomerRepository customerRepository, StoreRepository storeRepository) { + this.adoptionRepository = adoptionRepository; + this.petRepository = petRepository; + this.customerRepository = customerRepository; + this.storeRepository = storeRepository; + } + + public void setAdoptionId(long id) { + this.adoptionId = id; + this.isEditing = id != -1; + } + + public long getAdoptionId() { + return adoptionId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> loadAdoption() { + return adoptionRepository.getAdoptionById(adoptionId); + } + + public LiveData>> loadPets() { + return petRepository.getAdoptionPets(); + } + + public LiveData>> loadCustomers() { + return customerRepository.getCustomerDropdowns(); + } + + public LiveData>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData>> loadEmployees(Long storeId) { + return storeRepository.getStoreEmployees(storeId); + } + + public LiveData> saveAdoption(AdoptionDTO dto) { + if (isEditing) { + return adoptionRepository.updateAdoption(adoptionId, dto); + } else { + return adoptionRepository.createAdoption(dto); + } + } + + public LiveData> deleteAdoption() { + return adoptionRepository.deleteAdoption(adoptionId); + } + + public void setPetList(List list) { petList.setValue(list); } + public LiveData> getPetList() { return petList; } + + public void setCustomerList(List list) { customerList.setValue(list); } + public LiveData> getCustomerList() { return customerList; } + + public void setStoreList(List list) { storeList.setValue(list); } + public LiveData> getStoreList() { return storeList; } + + public void setEmployeeList(List list) { employeeList.setValue(list); } + public LiveData> getEmployeeList() { return employeeList; } +} 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 new file mode 100644 index 00000000..a76785af --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java @@ -0,0 +1,79 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.DropdownDTO; +import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.repositories.InventoryRepository; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class InventoryDetailViewModel extends ViewModel { + private final InventoryRepository inventoryRepository; + private final StoreRepository storeRepository; + private final ProductRepository productRepository; + + private long inventoryId = -1; + private boolean isEditing = false; + + private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public InventoryDetailViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository, ProductRepository productRepository) { + this.inventoryRepository = inventoryRepository; + this.storeRepository = storeRepository; + this.productRepository = productRepository; + } + + public void setInventoryId(long id) { + this.inventoryId = id; + this.isEditing = id != -1; + } + + public long getInventoryId() { return inventoryId; } + public boolean isEditing() { return isEditing; } + + public LiveData> loadInventory() { + return inventoryRepository.getInventoryById(inventoryId); + } + + public LiveData>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData>> loadProducts() { + return productRepository.getAllProducts(null, null, 0, 500, "prodName"); + } + + public LiveData> saveInventory(InventoryDTO dto) { + if (isEditing) { + return inventoryRepository.updateInventory(inventoryId, dto); + } else { + return inventoryRepository.createInventory(dto); + } + } + + public LiveData> deleteInventory() { + return inventoryRepository.deleteInventory(inventoryId); + } + + 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; } +} 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 new file mode 100644 index 00000000..00c074e9 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java @@ -0,0 +1,94 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.DropdownDTO; +import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class PetDetailViewModel extends ViewModel { + private final PetRepository petRepository; + private final CustomerRepository customerRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData petState = new MutableLiveData<>(); + private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); + + private long petId = -1; + private boolean isEditing = false; + + @Inject + public PetDetailViewModel(PetRepository petRepository, CustomerRepository customerRepository, StoreRepository storeRepository) { + this.petRepository = petRepository; + this.customerRepository = customerRepository; + this.storeRepository = storeRepository; + } + + public void setPetId(long id) { + this.petId = id; + this.isEditing = id != -1; + } + + public long getPetId() { + return petId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> loadPet() { + return petRepository.getPetById(petId); + } + + public LiveData>> loadCustomers() { + return customerRepository.getCustomerDropdowns(); + } + + public LiveData>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData> savePet(PetDTO petDTO) { + if (isEditing) { + petDTO.setPetId(petId); + return petRepository.updatePet(petId, petDTO); + } else { + return petRepository.createPet(petDTO); + } + } + + public LiveData> deletePet() { + return petRepository.deletePet(petId); + } + + public void setCustomerList(List list) { + customerList.setValue(list); + } + + public LiveData> getCustomerList() { + return customerList; + } + + public void setStoreList(List list) { + storeList.setValue(list); + } + + public LiveData> getStoreList() { + return storeList; + } +} 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 new file mode 100644 index 00000000..9ec0628a --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java @@ -0,0 +1,85 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.repositories.CategoryRepository; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import okhttp3.MultipartBody; + +@HiltViewModel +public class ProductDetailViewModel extends ViewModel { + private final ProductRepository productRepository; + private final CategoryRepository categoryRepository; + + private final MutableLiveData> categoryList = new MutableLiveData<>(new ArrayList<>()); + private long prodId = -1; + private boolean isEditing = false; + + @Inject + public ProductDetailViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) { + this.productRepository = productRepository; + this.categoryRepository = categoryRepository; + } + + public void setProdId(long id) { + this.prodId = id; + this.isEditing = id != -1; + } + + public long getProdId() { + return prodId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData>> loadCategories() { + return categoryRepository.getAllCategories(0, 100); + } + + public LiveData> loadProduct() { + return productRepository.getProductById(prodId); + } + + public LiveData> saveProduct(ProductDTO dto) { + if (isEditing) { + return productRepository.updateProduct(prodId, dto); + } else { + return productRepository.createProduct(dto); + } + } + + public LiveData> deleteProduct() { + return productRepository.deleteProduct(prodId); + } + + public LiveData> uploadProductImage(MultipartBody.Part image) { + return productRepository.uploadProductImage(prodId, image); + } + + public LiveData> deleteProductImage() { + return productRepository.deleteProductImage(prodId); + } + + public void setCategoryList(List list) { + categoryList.setValue(list); + } + + public LiveData> getCategoryList() { + return categoryList; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java new file mode 100644 index 00000000..552a99fc --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java @@ -0,0 +1,78 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.dtos.ProductSupplierDTO; +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.repositories.ProductSupplierRepository; +import com.example.petstoremobile.repositories.SupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ProductSupplierDetailViewModel extends ViewModel { + private final ProductSupplierRepository psRepository; + private final ProductRepository productRepository; + private final SupplierRepository supplierRepository; + + private boolean isEditing = false; + private long editProductId = -1; + private long editSupplierId = -1; + + private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> supplierList = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public ProductSupplierDetailViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) { + this.psRepository = psRepository; + this.productRepository = productRepository; + this.supplierRepository = supplierRepository; + } + + public void setEditMode(long productId, long supplierId) { + this.isEditing = true; + this.editProductId = productId; + this.editSupplierId = supplierId; + } + + public boolean isEditing() { return isEditing; } + public long getEditProductId() { return editProductId; } + public long getEditSupplierId() { return editSupplierId; } + + public LiveData>> loadProducts() { + return productRepository.getAllProducts(null, null, 0, 200, "prodName"); + } + + public LiveData>> loadSuppliers() { + return supplierRepository.getAllSuppliers(0, 200, null, "supCompany"); + } + + public LiveData> saveProductSupplier(ProductSupplierDTO dto) { + if (isEditing) { + return psRepository.updateProductSupplier(editProductId, editSupplierId, dto); + } else { + return psRepository.createProductSupplier(dto); + } + } + + public LiveData> deleteProductSupplier() { + return psRepository.deleteProductSupplier(editProductId, editSupplierId); + } + + public void setProductList(List list) { productList.setValue(list); } + public LiveData> getProductList() { return productList; } + + public void setSupplierList(List list) { supplierList.setValue(list); } + public LiveData> getSupplierList() { return supplierList; } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java new file mode 100644 index 00000000..436cfa4c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java @@ -0,0 +1,26 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PurchaseOrderDTO; +import com.example.petstoremobile.repositories.PurchaseOrderRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class PurchaseOrderDetailViewModel extends ViewModel { + private final PurchaseOrderRepository repository; + + @Inject + public PurchaseOrderDetailViewModel(PurchaseOrderRepository repository) { + this.repository = repository; + } + + public LiveData> loadPurchaseOrder(long id) { + return repository.getPurchaseOrderById(id); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java new file mode 100644 index 00000000..d2b13732 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java @@ -0,0 +1,167 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.repositories.SaleRepository; +import com.example.petstoremobile.utils.Resource; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class RefundViewModel extends ViewModel { + private final SaleRepository saleRepository; + + private final MutableLiveData> allSales = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData currentSale = new MutableLiveData<>(); + private final MutableLiveData> availableItems = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> refundCart = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public RefundViewModel(SaleRepository saleRepository) { + this.saleRepository = saleRepository; + } + + public LiveData>> loadAllSales() { + return saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc"); + } + + public void setAllSales(List sales) { + allSales.setValue(sales); + } + + public List getAllSalesList() { + return allSales.getValue(); + } + + public void setCurrentSale(SaleDTO sale) { + currentSale.setValue(sale); + buildRefundableItems(); + } + + public SaleDTO getCurrentSale() { + return currentSale.getValue(); + } + + public LiveData> getAvailableItems() { + return availableItems; + } + + public LiveData> getRefundCart() { + return refundCart; + } + + private void buildRefundableItems() { + SaleDTO sale = currentSale.getValue(); + List sales = allSales.getValue(); + if (sale == null || sales == null || sale.getItems() == null) { + availableItems.setValue(new ArrayList<>()); + return; + } + + Map alreadyRefunded = new HashMap<>(); + for (SaleDTO s : sales) { + if (Boolean.TRUE.equals(s.getIsRefund()) + && sale.getSaleId().equals(s.getOriginalSaleId()) + && s.getItems() != null) { + for (SaleDTO.SaleItemDTO item : s.getItems()) { + if (item.getProdId() != null && item.getQuantity() != null) { + alreadyRefunded.merge(item.getProdId(), + Math.abs(item.getQuantity()), Integer::sum); + } + } + } + } + + List items = new ArrayList<>(); + for (SaleDTO.SaleItemDTO item : sale.getItems()) { + if (item.getProdId() == null || item.getQuantity() == null) continue; + int refunded = alreadyRefunded.getOrDefault(item.getProdId(), 0); + int remaining = item.getQuantity() - refunded; + if (remaining > 0) { + items.add(new RefundItem( + item.getProdId(), + item.getProductName() != null ? item.getProductName() : "Unknown", + remaining, + item.getUnitPrice() + )); + } + } + availableItems.setValue(items); + refundCart.setValue(new ArrayList<>()); + } + + public void addToCart(RefundItem item, int qty) { + List cart = new ArrayList<>(refundCart.getValue()); + boolean merged = false; + for (int i = 0; i < cart.size(); i++) { + if (cart.get(i).prodId == item.prodId) { + RefundItem existing = cart.get(i); + cart.set(i, new RefundItem(existing.prodId, existing.productName, existing.quantity + qty, existing.unitPrice)); + merged = true; + break; + } + } + if (!merged) { + cart.add(new RefundItem(item.prodId, item.productName, qty, item.unitPrice)); + } + refundCart.setValue(cart); + } + + public void removeFromCart(RefundItem item) { + List cart = new ArrayList<>(refundCart.getValue()); + cart.remove(item); + refundCart.setValue(cart); + } + + public LiveData> submitRefund(String paymentMethod) { + SaleDTO sale = currentSale.getValue(); + List cart = refundCart.getValue(); + if (sale == null || cart == null || cart.isEmpty()) return null; + + List items = new ArrayList<>(); + for (RefundItem item : cart) { + items.add(new SaleDTO.SaleItemDTO(item.prodId, -item.quantity)); + } + + SaleDTO dto = new SaleDTO( + sale.getStoreId(), + paymentMethod, + items, + true, + sale.getSaleId(), + null + ); + + return saleRepository.createSale(dto); + } + + public static class RefundItem { + public long prodId; + public String productName; + public int quantity; + public BigDecimal unitPrice; + + public RefundItem(long prodId, String productName, int quantity, BigDecimal unitPrice) { + this.prodId = prodId; + this.productName = productName; + this.quantity = quantity; + this.unitPrice = unitPrice; + } + + public BigDecimal getTotal() { + return unitPrice != null ? unitPrice.multiply(BigDecimal.valueOf(quantity)) : BigDecimal.ZERO; + } + } +} 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 new file mode 100644 index 00000000..bf84ab32 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java @@ -0,0 +1,109 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.DropdownDTO; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.repositories.SaleRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SaleDetailViewModel extends ViewModel { + private final SaleRepository saleRepository; + private final StoreRepository storeRepository; + private final CustomerRepository customerRepository; + private final ProductRepository productRepository; + + private long saleId = -1; + private boolean viewOnly = false; + + private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> cartItems = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public SaleDetailViewModel(SaleRepository saleRepository, StoreRepository storeRepository, + CustomerRepository customerRepository, ProductRepository productRepository) { + this.saleRepository = saleRepository; + this.storeRepository = storeRepository; + this.customerRepository = customerRepository; + this.productRepository = productRepository; + } + + public void setSaleId(long id, boolean viewOnly) { + this.saleId = id; + this.viewOnly = viewOnly; + } + + public long getSaleId() { return saleId; } + public boolean isViewOnly() { return viewOnly; } + + public LiveData> loadSaleDetails() { + return saleRepository.getSaleById(saleId); + } + + public LiveData>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData>> loadCustomers() { + return customerRepository.getCustomerDropdowns(); + } + + public LiveData>> loadProducts() { + return productRepository.getAllProducts(null, null, 0, 200, null); + } + + public LiveData> createSale(SaleDTO sale) { + return saleRepository.createSale(sale); + } + + public void setStoreList(List list) { storeList.setValue(list); } + public LiveData> getStoreList() { return storeList; } + + public void setCustomerList(List list) { customerList.setValue(list); } + public LiveData> getCustomerList() { return customerList; } + + public void setProductList(List list) { productList.setValue(list); } + public LiveData> getProductList() { return productList; } + + public void addToCart(SaleDTO.SaleItemDTO item) { + List currentCart = new ArrayList<>(cartItems.getValue()); + currentCart.add(item); + cartItems.setValue(currentCart); + } + + public LiveData> getCartItems() { return cartItems; } + + public BigDecimal calculateSubtotal() { + BigDecimal total = BigDecimal.ZERO; + List items = cartItems.getValue(); + List products = productList.getValue(); + if (items != null && products != null) { + for (SaleDTO.SaleItemDTO item : items) { + for (ProductDTO p : products) { + if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) { + total = total.add(p.getProdPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); + break; + } + } + } + } + return total; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java new file mode 100644 index 00000000..fca74229 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java @@ -0,0 +1,54 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.repositories.ServiceRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ServiceDetailViewModel extends ViewModel { + private final ServiceRepository repository; + private long serviceId = -1; + private boolean isEditing = false; + + @Inject + public ServiceDetailViewModel(ServiceRepository repository) { + this.repository = repository; + } + + public void setServiceId(long id) { + this.serviceId = id; + this.isEditing = id != -1; + } + + public long getServiceId() { + return serviceId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> loadService() { + return repository.getServiceById(serviceId); + } + + public LiveData> saveService(ServiceDTO dto) { + if (isEditing) { + dto.setServiceId(serviceId); + return repository.updateService(serviceId, dto); + } else { + return repository.createService(dto); + } + } + + public LiveData> deleteService() { + return repository.deleteService(serviceId); + } +} 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 new file mode 100644 index 00000000..91162405 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java @@ -0,0 +1,49 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.repositories.EmployeeRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class StaffDetailViewModel extends ViewModel { + private final EmployeeRepository repository; + private long employeeId = -1; + private boolean isEditing = false; + + @Inject + public StaffDetailViewModel(EmployeeRepository repository) { + this.repository = repository; + } + + public void setEmployeeId(long id, boolean isEditing) { + this.employeeId = id; + this.isEditing = isEditing; + } + + public long getEmployeeId() { + return employeeId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> saveEmployee(EmployeeDTO dto) { + if (isEditing && employeeId > 0) { + return repository.updateEmployee(employeeId, dto); + } else { + return repository.createEmployee(dto); + } + } + + public LiveData> deleteEmployee() { + return repository.deleteEmployee(employeeId); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java new file mode 100644 index 00000000..591beb52 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java @@ -0,0 +1,54 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.repositories.SupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SupplierDetailViewModel extends ViewModel { + private final SupplierRepository repository; + private long supId = -1; + private boolean isEditing = false; + + @Inject + public SupplierDetailViewModel(SupplierRepository repository) { + this.repository = repository; + } + + public void setSupId(long id) { + this.supId = id; + this.isEditing = id != -1; + } + + public long getSupId() { + return supId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> loadSupplier() { + return repository.getSupplierById(supId); + } + + public LiveData> saveSupplier(SupplierDTO dto) { + if (isEditing) { + dto.setSupId(supId); + return repository.updateSupplier(supId, dto); + } else { + return repository.createSupplier(dto); + } + } + + public LiveData> deleteSupplier() { + return repository.deleteSupplier(supId); + } +} From 6a3730ca04d9994ad84d760b0d7432fed8bf5c59 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:44:04 -0600 Subject: [PATCH 15/33] refactored viewmodels for listfragments --- .../fragments/ChatFragment.java | 368 ++++-------------- .../listfragments/AdoptionFragment.java | 135 ++----- .../listfragments/AnalyticsFragment.java | 291 +++++--------- .../listfragments/AppointmentFragment.java | 146 ++----- .../listfragments/InventoryFragment.java | 153 ++------ .../fragments/listfragments/PetFragment.java | 158 +++----- .../listfragments/ProductFragment.java | 139 +++---- .../ProductSupplierFragment.java | 148 ++----- .../listfragments/PurchaseOrderFragment.java | 119 ++---- .../fragments/listfragments/SaleFragment.java | 109 ++---- .../listfragments/ServiceFragment.java | 94 +---- .../listfragments/StaffFragment.java | 80 ++-- .../listfragments/SupplierFragment.java | 87 +---- .../PetProfileFragment.java | 33 +- .../viewmodels/AdoptionListViewModel.java | 83 ++++ .../viewmodels/AnalyticsViewModel.java | 163 ++++++++ .../viewmodels/AppointmentListViewModel.java | 65 ++++ .../viewmodels/ChatListViewModel.java | 132 +++++++ .../viewmodels/InventoryListViewModel.java | 81 ++++ .../viewmodels/PetListViewModel.java | 69 ++++ .../viewmodels/PetProfileViewModel.java | 35 ++ .../viewmodels/ProductListViewModel.java | 60 +++ .../ProductSupplierListViewModel.java | 77 ++++ .../PurchaseOrderListViewModel.java | 60 +++ .../viewmodels/SaleListViewModel.java | 77 ++++ .../viewmodels/ServiceListViewModel.java | 68 ++++ .../viewmodels/StaffListViewModel.java | 71 ++++ .../viewmodels/SupplierListViewModel.java | 51 +++ 28 files changed, 1626 insertions(+), 1526 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index b13edd67..1b858b08 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -18,19 +18,17 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import com.bumptech.glide.Glide; -import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ChatAdapter; import com.example.petstoremobile.adapters.MessageAdapter; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentChatBinding; import com.example.petstoremobile.dtos.ConversationDTO; import com.example.petstoremobile.dtos.MessageDTO; -import com.example.petstoremobile.dtos.SendMessageRequest; import com.example.petstoremobile.models.Chat; import com.example.petstoremobile.models.Message; import com.example.petstoremobile.services.ChatNotificationService; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.viewmodels.ChatViewModel; +import com.example.petstoremobile.viewmodels.ChatListViewModel; import com.example.petstoremobile.websocket.StompChatManager; import java.util.*; @@ -47,59 +45,44 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis private static final String TAG = "ChatFragment"; private FragmentChatBinding binding; - private ChatViewModel viewModel; + private ChatListViewModel viewModel; - // Adapters private ChatAdapter chatAdapter; private MessageAdapter messageAdapter; - // Data private final List chatList = new ArrayList<>(); private final List messageList = new ArrayList<>(); - private final Map customerNames = new HashMap<>(); private Uri pendingAttachmentUri; @Inject TokenManager tokenManager; @Inject @Named("baseUrl") String baseUrl; - // chat - private Long currentUserId; private Long activeConversationId; private StompChatManager stompChatManager; private ActivityResultLauncher attachmentLauncher; - /** - * Initializes the attachment launcher to handle file selection from the gallery. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ChatViewModel.class); + viewModel = new ViewModelProvider(this).get(ChatListViewModel.class); attachmentLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { Uri uri = result.getData().getData(); - if (uri != null) { - showAttachmentPreview(uri); - } + if (uri != null) showAttachmentPreview(uri); } } ); } - /** - * Inflates the layout, initializes UI components, and sets up click listeners for messaging. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentChatBinding.inflate(inflater, container, false); binding.btnHamburger.setOnClickListener(v -> binding.chatDrawerLayout.openDrawer(GravityCompat.START)); - // Set editor action listener for message field to send when enter is pressed binding.etMessage.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_NULL) { binding.btnSend.performClick(); @@ -108,35 +91,26 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis return false; }); - //When the send button is clicked check if there is an attachment and send using the correct helper function binding.btnSend.setOnClickListener(v -> { - if (pendingAttachmentUri != null) { - sendWithAttachment(pendingAttachmentUri); - } else { - sendMessage(); - } + if (pendingAttachmentUri != null) sendWithAttachment(pendingAttachmentUri); + else sendMessage(); }); - //When the attachment button is clicked open the file picker binding.btnAttach.setOnClickListener(v -> selectAttachment()); binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment()); setupRecyclerViews(); + observeViewModel(); loadInitialData(); return binding.getRoot(); } - /** - * Configures the RecyclerViews for the conversation list and the message history. - */ private void setupRecyclerViews() { - // Set up Drawer menu to select conversation chatAdapter = new ChatAdapter(chatList, this); binding.rvChatList.setLayoutManager(new LinearLayoutManager(getContext())); binding.rvChatList.setAdapter(chatAdapter); - // set up RecyclerView for selected chat to show messages messageAdapter = new MessageAdapter(messageList, null); LinearLayoutManager lm = new LinearLayoutManager(getContext()); lm.setStackFromEnd(true); @@ -145,26 +119,48 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis setConversationActive(false); } - /** - * Loads authentication tokens and user info, then initializes the Stomp WebSocket connection. - */ + private void observeViewModel() { + viewModel.getChatList().observe(getViewLifecycleOwner(), list -> { + chatList.clear(); + chatList.addAll(list); + chatAdapter.notifyDataSetChanged(); + + if (activeConversationId != null) { + for (Chat chat : list) { + if (chat.getChatId().equals(String.valueOf(activeConversationId))) { + binding.tvChatTitle.setText(chat.getCustomerName()); + break; + } + } + } + }); + + viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> { + messageList.clear(); + messageList.addAll(list); + messageAdapter.notifyDataSetChanged(); + scrollToBottom(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + // Can show a progress bar if needed + }); + } + private void loadInitialData() { String token = tokenManager.getToken(); - currentUserId = tokenManager.getUserId(); + Long currentUserId = tokenManager.getUserId(); String role = tokenManager.getRole(); messageAdapter.setCurrentUserId(currentUserId); messageAdapter.setToken(token); - // if token exist then connect to websocket if (token != null) { stompChatManager = new StompChatManager(token, role, baseUrl); stompChatManager.setMessageListener(this); stompChatManager.setConversationListener(this); stompChatManager.setConnectionListener(this); stompChatManager.connect(); - } else { - Log.e(TAG, "No token found"); } if (getArguments() != null && getArguments().containsKey("conversation_id")) { @@ -172,65 +168,17 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } else if (getActivity() != null && getActivity().getIntent().hasExtra("conversation_id")) { activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1); getActivity().getIntent().removeExtra("conversation_id"); - getActivity().getIntent().removeExtra("navigate_to"); } - loadCustomers(); + viewModel.loadCustomers(); + + if (activeConversationId != null) { + setConversationActive(true); + if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId); + viewModel.loadMessageHistory(activeConversationId); + } } - /** - * Fetches a list of customers from the ViewModel to display customer names for the chat list. - */ - private void loadCustomers() { - viewModel.getAllCustomers(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - resource.data.getContent().forEach(c -> customerNames.put(c.getCustomerId(), c.getFullName())); - loadConversations(); - } - }); - } - - /** - * Retrieves all conversations for the current user through the ViewModel and populates the chat drawer. - */ - private void loadConversations() { - viewModel.getAllConversations().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - chatList.clear(); - for (ConversationDTO dto : resource.data) { - String name = customerNames.getOrDefault( - dto.getCustomerId(), "Customer #" + dto.getCustomerId()); - chatList.add(new Chat(String.valueOf(dto.getId()), - name, dto.getLastMessage(), - dto.getCustomerId(), dto.getStaffId())); - } - chatAdapter.notifyDataSetChanged(); - - if (activeConversationId != null) { - setConversationActive(true); - // Update title to customer name of active conversation - for (Chat chat : chatList) { - if (chat.getChatId().equals(String.valueOf(activeConversationId))) { - binding.tvChatTitle.setText(chat.getCustomerName()); - break; - } - } - if (stompChatManager != null) { - stompChatManager.subscribeToConversation(activeConversationId); - } - loadMessageHistory(activeConversationId); - } else { - messageList.clear(); - messageAdapter.notifyDataSetChanged(); - setConversationActive(false); - } - } - }); - } - - /** - * Handles selection of a chat from the drawer, updating the UI and subscribing to the WebSocket. - */ @Override public void onChatClick(Chat chat) { activeConversationId = Long.parseLong(chat.getChatId()); @@ -238,75 +186,35 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis binding.tvChatTitle.setText(chat.getCustomerName()); binding.chatDrawerLayout.closeDrawer(GravityCompat.START); - if (stompChatManager != null) { - stompChatManager.subscribeToConversation(activeConversationId); - } - - loadMessageHistory(activeConversationId); + if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId); + viewModel.loadMessageHistory(activeConversationId); } - /** - * Fetches the full message history for a specific conversation from the ViewModel. - */ - private void loadMessageHistory(Long conversationId) { - viewModel.getMessages(conversationId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - messageList.clear(); - for (MessageDTO dto : resource.data) { - messageList.add(dtoToModel(dto)); - } - messageAdapter.notifyDataSetChanged(); - scrollToBottom(); - } - }); - } - - /** - * Sends a plain text message to the currently active conversation through the ViewModel. - */ private void sendMessage() { - //check if a chat is selected if (activeConversationId == null) return; - - //get the message from text field String text = binding.etMessage.getText().toString().trim(); if (text.isEmpty()) return; - //clear text field after sending binding.etMessage.setText(""); - - //calls viewmodel to send the message - viewModel.sendMessage(activeConversationId, new SendMessageRequest(text)).observe(getViewLifecycleOwner(), resource -> { + viewModel.sendMessage(activeConversationId, text).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - messageList.add(dtoToModel(resource.data)); - messageAdapter.notifyItemInserted(messageList.size() - 1); - scrollToBottom(); - loadConversations(); + viewModel.addMessageLocally(resource.data); + viewModel.loadConversations(); } }); } - /** - * Launches a file picker intent to select an attachment for the message. - */ private void selectAttachment() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); attachmentLauncher.launch(intent); } - /** - * Displays a preview of the selected attachment in the UI. - */ private void showAttachmentPreview(Uri uri) { pendingAttachmentUri = uri; binding.layoutAttachmentPreview.setVisibility(View.VISIBLE); - String mimeType = requireContext().getContentResolver().getType(uri); - String fileName = getFileName(uri); - binding.tvPreviewName.setText(fileName); - - // If the file is an image, display a thumbnail of the image as well + binding.tvPreviewName.setText(getFileName(uri)); if (mimeType != null && mimeType.startsWith("image/")) { binding.ivPreview.setVisibility(View.VISIBLE); Glide.with(this).load(uri).into(binding.ivPreview); @@ -315,183 +223,83 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } - /** - * Clears the current attachment selection and hides the preview UI. - */ private void removeAttachment() { pendingAttachmentUri = null; binding.layoutAttachmentPreview.setVisibility(View.GONE); } - /** - * Show the display name of the file from its Uri. - */ private String getFileName(Uri uri) { String result = null; if (uri.getScheme().equals("content")) { try (Cursor cursor = requireContext().getContentResolver().query(uri, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); - if (index != -1) { - result = cursor.getString(index); - } + if (index != -1) result = cursor.getString(index); } } } if (result == null) { result = uri.getPath(); int cut = result.lastIndexOf('/'); - if (cut != -1) { - result = result.substring(cut + 1); - } + if (cut != -1) result = result.substring(cut + 1); } return result; } - /** - * Handles sending a message that includes a file attachment via the ViewModel. - */ private void sendWithAttachment(Uri uri) { - if (activeConversationId == null) return; - String text = binding.etMessage.getText().toString().trim(); - binding.etMessage.setText(""); - removeAttachment(); - - if (!text.isEmpty()) { - binding.etMessage.setText(text); - } Toast.makeText(requireContext(), "File attachments are not supported", Toast.LENGTH_SHORT).show(); + removeAttachment(); } - /** - * Callback triggered when a new message is received via the WebSocket. - */ @Override public void onMessageReceived(MessageDTO dto) { - //if there is no active selected conversation or the message received is for another chat, then just update the preview of last message - if (activeConversationId == null || !activeConversationId.equals(dto.getConversationId())) { - updateConversationPreview(dto.getConversationId(), dto.getContent()); - return; - } - updateConversationPreview(dto.getConversationId(), dto.getContent()); - - if (currentUserId != null && currentUserId.equals(dto.getSenderId())) return; - - //else add the message to the active chat if it's not from the current user - messageList.add(dtoToModel(dto)); requireActivity().runOnUiThread(() -> { - messageAdapter.notifyItemInserted(messageList.size() - 1); - scrollToBottom(); + if (activeConversationId != null && activeConversationId.equals(dto.getConversationId())) { + if (!tokenManager.getUserId().equals(dto.getSenderId())) { + viewModel.addMessageLocally(dto); + } + } + viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), "")); + // Re-load coversations to get correct names if needed or just let local update handle it + viewModel.loadConversations(); }); } - /** - * Callback triggered when a conversation is created or updated via the WebSocket. - */ @Override public void onConversationUpdated(ConversationDTO dto) { requireActivity().runOnUiThread(() -> { - boolean updated = false; - String name = customerNames.getOrDefault( - dto.getCustomerId(), "Customer #" + dto.getCustomerId()); - - for (int i = 0; i < chatList.size(); i++) { - Chat existing = chatList.get(i); - if (existing.getChatId().equals(String.valueOf(dto.getId()))) { - chatList.set(i, new Chat( - String.valueOf(dto.getId()), - name, - dto.getLastMessage(), - dto.getCustomerId(), - dto.getStaffId() - )); - chatAdapter.notifyItemChanged(i); - updated = true; - break; - } - } - - if (!updated) { - chatList.add(0, new Chat( - String.valueOf(dto.getId()), - name, - dto.getLastMessage(), - dto.getCustomerId(), - dto.getStaffId() - )); - chatAdapter.notifyItemInserted(0); - } - + viewModel.updateConversationLocally(dto); if (activeConversationId != null && activeConversationId.equals(dto.getId())) { setConversationActive(true); - binding.tvChatTitle.setText(name); + binding.tvChatTitle.setText(viewModel.getCustomerName(dto.getCustomerId())); } }); } - /** - * Callback triggered when the WebSocket connection is successfully opened. - */ @Override public void onSocketOpened() { - if (!isAdded()) { - return; - } + if (!isAdded()) return; requireActivity().runOnUiThread(() -> { - loadConversations(); - if (activeConversationId != null) { - loadMessageHistory(activeConversationId); - } + viewModel.loadConversations(); + if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId); }); } - /** - * Callback triggered when the WebSocket connection is closed. - */ @Override public void onSocketClosed() { - if (!isAdded()) { - return; - } - requireActivity().runOnUiThread(this::loadConversations); + if (!isAdded()) return; + requireActivity().runOnUiThread(viewModel::loadConversations); } - /** - * Callback triggered when a WebSocket connection error occurs. - */ @Override public void onSocketError() { - if (!isAdded()) { - return; - } + if (!isAdded()) return; requireActivity().runOnUiThread(() -> { - loadConversations(); - if (activeConversationId != null) { - loadMessageHistory(activeConversationId); - } + viewModel.loadConversations(); + if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId); }); } - /** - * Converts a MessageDTO into a Message object. - */ - private Message dtoToModel(MessageDTO dto) { - Message m = new Message(); - m.setId(dto.getId()); - m.setConversationId(dto.getConversationId()); - m.setSenderId(dto.getSenderId()); - m.setContent(dto.getContent()); - m.setTimestamp(dto.getTimestamp()); - m.setIsRead(dto.getIsRead()); - m.setAttachmentUrl(dto.getAttachmentUrl()); - m.setAttachmentName(dto.getAttachmentName()); - m.setAttachmentType(dto.getAttachmentType()); - return m; - } - - /** - * Scrolls the message history RecyclerView to the most recent message. - */ private void scrollToBottom() { if (!messageList.isEmpty()) { binding.rvMessages.post(() -> @@ -499,35 +307,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } - /** - * Updates the preview snippet of the last message for a specific conversation in the drawer. - */ - private void updateConversationPreview(Long conversationId, String lastMessage) { - if (conversationId == null) { - return; - } - requireActivity().runOnUiThread(() -> { - for (int i = 0; i < chatList.size(); i++) { - Chat existing = chatList.get(i); - if (existing.getChatId().equals(String.valueOf(conversationId))) { - Chat updated = new Chat( - existing.getChatId(), - existing.getCustomerName(), - lastMessage, - existing.getCustomerId(), - existing.getStaffId() - ); - chatList.set(i, updated); - chatAdapter.notifyItemChanged(i); - return; - } - } - }); - } - - /** - * Toggles the UI state based on whether a conversation is currently selected. - */ private void setConversationActive(boolean active) { binding.btnSend.setEnabled(active); binding.etMessage.setEnabled(active); @@ -537,9 +316,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis ChatNotificationService.activeConversationIdInUi = null; removeAttachment(); if (binding != null && binding.tvChatTitle != null) binding.tvChatTitle.setText("Customer Chat"); - if (stompChatManager != null) { - stompChatManager.clearConversationSubscription(); - } + if (stompChatManager != null) stompChatManager.clearConversationSubscription(); messageList.clear(); messageAdapter.notifyDataSetChanged(); binding.etMessage.setText(""); @@ -550,9 +327,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } - /** - * Disconnects the WebSocket manager when the fragment view is destroyed. - */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index f2a01b5d..39317495 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -24,9 +24,8 @@ import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.AdoptionViewModel; +import com.example.petstoremobile.viewmodels.AdoptionListViewModel; import com.example.petstoremobile.utils.EventDecorator; -import com.example.petstoremobile.viewmodels.StoreViewModel; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; @@ -46,28 +45,19 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop private FragmentAdoptionBinding binding; private List adoptionList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private AdoptionAdapter adapter; - private AdoptionViewModel adoptionViewModel; - private StoreViewModel storeViewModel; + private AdoptionListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; private CalendarDay selectedCalendarDay; private boolean isMonthMode = false; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); - /** - * Initializes the fragment and its ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(AdoptionListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, Search, SwipeRefresh, and Calendar. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -81,6 +71,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop setupCalendar(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1)); @@ -91,6 +82,24 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop return binding.getRoot(); } + private void observeViewModel() { + viewModel.getAdoptions().observe(getViewLifecycleOwner(), list -> { + adoptionList.clear(); + adoptionList.addAll(list); + updateCalendarDecorators(); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshAdoption.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -99,27 +108,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop binding.btnBulkDelete, adapter, "adoption", - adoptionViewModel::bulkDeleteAdoptions, + viewModel::bulkDeleteAdoptions, this::loadAdoptions ); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - @Override public void onResume() { super.onResume(); loadAdoptions(); - loadStoreData(); + viewModel.loadStores(); } - /** - * Toggles the calendar display between week and month modes. - */ private void toggleCalendarMode() { isMonthMode = !isMonthMode; binding.calendarViewAdoption.state().edit() @@ -127,17 +127,11 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop .commit(); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption, binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption); } - /** - * Sets up the date selection listener for the calendar. - */ private void setupCalendar() { binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> { if (selected) { @@ -154,9 +148,6 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop }); } - /** - * Updates the calendar decorators to highlight days with adoptions. - */ private void updateCalendarDecorators() { HashSet datesWithAdoptions = new HashSet<>(); for (AdoptionDTO adoption : adoptionList) { @@ -177,67 +168,37 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions)); } - /** - * Initializes the RecyclerView for displaying adoptions. - */ private void setupRecyclerView() { adapter = new AdoptionAdapter(adoptionList, this); binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAdoptions.setAdapter(adapter); } - /** - * Sets up the search bar for filtering - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions); } - /** - * Configures the status filter spinner. - */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Completed", "Pending", "Cancelled"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, this::loadAdoptions); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Sets up the SwipeRefreshLayout to reload adoption data. - */ private void setupSwipeRefresh() { binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions); } - /** - * Fetches the adoption list from the server through the ViewModel. - */ private void loadAdoptions() { String query = binding.etSearchAdoption.getText().toString().trim(); String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses"; Long storeId = null; - if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId(); } String selectedDateString = null; @@ -249,52 +210,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop if (status.equals("All Statuses")) status = null; else status = status.toUpperCase(); - adoptionViewModel.getAllAdoptions(0, 500, query, status, storeId, selectedDateString, null).observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshAdoption.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshAdoption.setRefreshing(false); - if (resource.data != null) { - adoptionList.clear(); - adoptionList.addAll(resource.data.getContent()); - updateCalendarDecorators(); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshAdoption.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load adoptions: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("AdoptionFragment", "Error loading adoptions: " + resource.message); - break; - } - }); + viewModel.loadAdoptions(true, query, status, storeId); } - /** - * Navigates to the adoption detail screen for a specific adoption or to create a new one. - */ private void openDetail(int position) { Bundle args = new Bundle(); - if (position != -1) { AdoptionDTO a = adoptionList.get(position); args.putLong("adoptionId", a.getAdoptionId()); } - NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args); } - /** - * Handles item click in the adoption list. - */ @Override public void onAdoptionClick(int position) { openDetail(position); } @@ -304,4 +231,10 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop bulkDeleteHandler.onSelectionChanged(selectedCount); } } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java index 2e2b2592..ef26db92 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java @@ -2,16 +2,14 @@ package com.example.petstoremobile.fragments.listfragments; import android.graphics.Color; import android.os.Bundle; -import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.example.petstoremobile.databinding.FragmentAnalyticsBinding; -import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.SaleViewModel; +import com.example.petstoremobile.viewmodels.AnalyticsViewModel; import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.math.RoundingMode; @@ -21,226 +19,131 @@ import java.util.*; public class AnalyticsFragment extends Fragment { private FragmentAnalyticsBinding binding; - private SaleViewModel saleViewModel; + private AnalyticsViewModel viewModel; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentAnalyticsBinding.inflate(inflater, container, false); - saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); + viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class); - loadAnalytics(); + observeViewModel(); + viewModel.loadAnalytics(); - binding.btnRefreshAnalytics.setOnClickListener(v -> loadAnalytics()); + binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics()); UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this); return binding.getRoot(); } + private void observeViewModel() { + viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + if (loading) { + binding.tvTotalRevenue.setText("Loading..."); + binding.tvTotalTransactions.setText("..."); + binding.tvAvgTransaction.setText("..."); + binding.tvTotalItems.setText("..."); + } + }); + + viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> { + if (error != null) showError(error); + }); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - private void loadAnalytics() { - // Clear all sections + private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) { + if (data == null) return; + + // Summary + binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP)); + binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions)); + binding.tvAvgTransaction.setText("$" + data.avgTransaction); + binding.tvTotalItems.setText(String.valueOf(data.totalItems)); + + // Top Revenue Products binding.llTopRevenue.removeAllViews(); - binding.llTopQuantity.removeAllViews(); - binding.llPaymentMethods.removeAllViews(); - binding.llEmployeePerformance.removeAllViews(); - binding.llDailyRevenue.removeAllViews(); - - - saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc") - .observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - if (resource.data != null) { - computeAndDisplay(resource.data.getContent()); - } - break; - case ERROR: - Log.e("Analytics", resource.message != null ? resource.message : "Error loading sales"); - showError("Failed to load sales data"); - break; - case LOADING: - binding.tvTotalRevenue.setText("Loading..."); - binding.tvTotalTransactions.setText("..."); - binding.tvAvgTransaction.setText("..."); - binding.tvTotalItems.setText("..."); - break; - } - } - }); - } - - private void computeAndDisplay(List sales) { - // Filter out refunds for most metrics - List regularSales = new ArrayList<>(); - for (SaleDTO s : sales) { - if (!Boolean.TRUE.equals(s.getIsRefund())) - regularSales.add(s); - } - - // ── Summary ────────────────────────────────────────── - BigDecimal totalRevenue = BigDecimal.ZERO; - int totalItems = 0; - - for (SaleDTO s : regularSales) { - if (s.getTotalAmount() != null) - totalRevenue = totalRevenue.add(s.getTotalAmount()); - if (s.getItems() != null) { - for (SaleDTO.SaleItemDTO item : s.getItems()) { - if (item.getQuantity() != null) - totalItems += Math.abs(item.getQuantity()); - } + if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) { + BigDecimal maxRevenue = data.topRevenueProducts.get(0).getValue(); + if (maxRevenue.compareTo(BigDecimal.ZERO) == 0) maxRevenue = BigDecimal.ONE; + for (Map.Entry e : data.topRevenueProducts) { + addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35"); } - } - - int totalTx = regularSales.size(); - BigDecimal avgTx = totalTx > 0 - ? totalRevenue.divide(BigDecimal.valueOf(totalTx), 2, RoundingMode.HALF_UP) - : BigDecimal.ZERO; - - binding.tvTotalRevenue.setText("$" + totalRevenue.setScale(2, RoundingMode.HALF_UP)); - binding.tvTotalTransactions.setText(String.valueOf(totalTx)); - binding.tvAvgTransaction.setText("$" + avgTx); - binding.tvTotalItems.setText(String.valueOf(totalItems)); - - // ── Top Products by Revenue ─────────────────────────── - Map revenueByProduct = new LinkedHashMap<>(); - Map quantityByProduct = new LinkedHashMap<>(); - - for (SaleDTO s : regularSales) { - if (s.getItems() != null) { - for (SaleDTO.SaleItemDTO item : s.getItems()) { - String name = item.getProductName() != null ? item.getProductName() : "Unknown"; - int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0; - BigDecimal lineTotal = item.getUnitPrice() != null - ? item.getUnitPrice().multiply(BigDecimal.valueOf(qty)) - : BigDecimal.ZERO; - - revenueByProduct.merge(name, lineTotal, BigDecimal::add); - quantityByProduct.merge(name, qty, Integer::sum); - } - } - } - - // Sort by revenue desc, take top 5 - List> topRevenue = new ArrayList<>(revenueByProduct.entrySet()); - topRevenue.sort((a, b) -> b.getValue().compareTo(a.getValue())); - BigDecimal maxRevenue = topRevenue.isEmpty() ? BigDecimal.ONE : topRevenue.get(0).getValue(); - - binding.llTopRevenue.removeAllViews(); - for (int i = 0; i < Math.min(5, topRevenue.size()); i++) { - Map.Entry e = topRevenue.get(i); - addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35"); - } - if (topRevenue.isEmpty()) + } else { addEmptyRow(binding.llTopRevenue, "No data"); + } - // Sort by quantity desc, take top 5 - List> topQuantity = new ArrayList<>(quantityByProduct.entrySet()); - topQuantity.sort((a, b) -> b.getValue() - a.getValue()); - int maxQty = topQuantity.isEmpty() ? 1 : topQuantity.get(0).getValue(); - + // Top Quantity Products binding.llTopQuantity.removeAllViews(); - for (int i = 0; i < Math.min(5, topQuantity.size()); i++) { - Map.Entry e = topQuantity.get(i); - addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units", - (float) e.getValue() / maxQty, "#4ecdc4"); - } - if (topQuantity.isEmpty()) - addEmptyRow(binding.llTopQuantity, "No data"); - - // ── Payment Methods ─────────────────────────────────── - Map paymentCount = new LinkedHashMap<>(); - for (SaleDTO s : regularSales) { - String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown"; - paymentCount.merge(method, 1, Integer::sum); - } - - int maxPayment = paymentCount.values().stream().max(Integer::compare).orElse(1); - String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" }; - int ci = 0; - binding.llPaymentMethods.removeAllViews(); - for (Map.Entry e : paymentCount.entrySet()) { - addBarRow(binding.llPaymentMethods, e.getKey(), - e.getValue() + " transactions", - (float) e.getValue() / maxPayment, - paymentColors[ci % paymentColors.length]); - ci++; - } - if (paymentCount.isEmpty()) - addEmptyRow(binding.llPaymentMethods, "No data"); - - // ── Employee Performance ────────────────────────────── - Map employeeRevenue = new LinkedHashMap<>(); - for (SaleDTO s : regularSales) { - String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown"; - if (s.getTotalAmount() != null) - employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add); - } - - List> empList = new ArrayList<>(employeeRevenue.entrySet()); - empList.sort((a, b) -> b.getValue().compareTo(a.getValue())); - BigDecimal maxEmp = empList.isEmpty() ? BigDecimal.ONE : empList.get(0).getValue(); - - binding.llEmployeePerformance.removeAllViews(); - for (Map.Entry e : empList) { - addBarRow(binding.llEmployeePerformance, e.getKey(), - "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxEmp.floatValue(), - "#1a759f"); - } - if (empList.isEmpty()) - addEmptyRow(binding.llEmployeePerformance, "No data"); - - // ── Daily Revenue (last 7 days) ─────────────────────── - Map dailyRevenue = new TreeMap<>(); - - // Initialize last 7 days - Calendar cal = Calendar.getInstance(); - for (int i = 6; i >= 0; i--) { - Calendar day = Calendar.getInstance(); - day.add(Calendar.DAY_OF_YEAR, -i); - String key = String.format("%04d-%02d-%02d", - day.get(Calendar.YEAR), - day.get(Calendar.MONTH) + 1, - day.get(Calendar.DAY_OF_MONTH)); - dailyRevenue.put(key, BigDecimal.ZERO); - } - - for (SaleDTO s : regularSales) { - if (s.getSaleDate() != null && s.getTotalAmount() != null) { - String date = s.getSaleDate().length() >= 10 - ? s.getSaleDate().substring(0, 10) - : s.getSaleDate(); - if (dailyRevenue.containsKey(date)) { - dailyRevenue.merge(date, s.getTotalAmount(), BigDecimal::add); - } + if (data.topQuantityProducts != null && !data.topQuantityProducts.isEmpty()) { + int maxQty = data.topQuantityProducts.get(0).getValue(); + if (maxQty == 0) maxQty = 1; + for (Map.Entry e : data.topQuantityProducts) { + addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units", + (float) e.getValue() / maxQty, "#4ecdc4"); } + } else { + addEmptyRow(binding.llTopQuantity, "No data"); } - BigDecimal maxDaily = dailyRevenue.values().stream() - .max(BigDecimal::compareTo).orElse(BigDecimal.ONE); - if (maxDaily.compareTo(BigDecimal.ZERO) == 0) - maxDaily = BigDecimal.ONE; + // Payment Methods + binding.llPaymentMethods.removeAllViews(); + if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) { + int maxPayment = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1); + String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" }; + int ci = 0; + for (Map.Entry e : data.paymentMethodStats) { + addBarRow(binding.llPaymentMethods, e.getKey(), + e.getValue() + " transactions", + (float) e.getValue() / maxPayment, + paymentColors[ci % paymentColors.length]); + ci++; + } + } else { + addEmptyRow(binding.llPaymentMethods, "No data"); + } + // Employee Performance + binding.llEmployeePerformance.removeAllViews(); + if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) { + BigDecimal maxEmp = data.employeePerformance.get(data.employeePerformance.size() - 1).getValue(); + if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; + // Sorting is ascending from VM for some reason? Let's check VM... it says b.getValue().compareTo(a.getValue()) which is DESC. + // Wait, computeAnalytics sorts them... let's assume DESC as per VM code. + maxEmp = data.employeePerformance.get(0).getValue(); + if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; + + for (Map.Entry e : data.employeePerformance) { + addBarRow(binding.llEmployeePerformance, e.getKey(), + "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxEmp.floatValue(), + "#1a759f"); + } + } else { + addEmptyRow(binding.llEmployeePerformance, "No data"); + } + + // Daily Revenue binding.llDailyRevenue.removeAllViews(); - for (Map.Entry e : dailyRevenue.entrySet()) { - // Show just MM-DD - String label = e.getKey().length() >= 10 - ? e.getKey().substring(5) - : e.getKey(); - addBarRow(binding.llDailyRevenue, label, - "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxDaily.floatValue(), - "#ff6b35"); + if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) { + BigDecimal maxDaily = data.dailyRevenue.stream().map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE); + if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE; + for (Map.Entry e : data.dailyRevenue) { + String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey(); + addBarRow(binding.llDailyRevenue, label, + "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxDaily.floatValue(), + "#ff6b35"); + } } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index b921d867..8610790e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -14,7 +14,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AppointmentAdapter; @@ -25,10 +24,9 @@ import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.AppointmentViewModel; +import com.example.petstoremobile.viewmodels.AppointmentListViewModel; import com.example.petstoremobile.utils.EventDecorator; import com.example.petstoremobile.viewmodels.AuthViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; @@ -48,11 +46,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private FragmentAppointmentBinding binding; private List appointmentList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private AppointmentAdapter adapter; - private AppointmentViewModel appointmentViewModel; - private StoreViewModel storeViewModel; + private AppointmentListViewModel viewModel; private AuthViewModel authViewModel; private BulkDeleteHandler bulkDeleteHandler; @@ -61,20 +57,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private Long currentUserId = null; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); - /** - * Initializes the fragment and its associated ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(AppointmentListViewModel.class); authViewModel = new ViewModelProvider(this).get(AuthViewModel.class); } - /** - * Sets up the fragment's UI, including RecyclerView, search, swipe-to-refresh, and calendar. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -89,6 +78,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. setupFilterToggle(); setupMyAppointmentFilter(); setupBulkDelete(); + observeViewModel(); binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); @@ -101,6 +91,24 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. return binding.getRoot(); } + private void observeViewModel() { + viewModel.getAppointments().observe(getViewLifecycleOwner(), list -> { + appointmentList.clear(); + appointmentList.addAll(list); + updateCalendarDecorators(); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshAppointment.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -109,27 +117,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. binding.btnBulkDelete, adapter, "appointment", - appointmentViewModel::bulkDeleteAppointments, + viewModel::bulkDeleteAppointments, this::loadAppointmentData ); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - @Override public void onResume() { super.onResume(); loadAppointmentData(); - loadStoreData(); + viewModel.loadStores(); } - /** - * Toggles the calendar between week and month display modes. - */ private void toggleCalendarMode() { isMonthMode = !isMonthMode; binding.calendarView.state().edit() @@ -137,18 +136,12 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. .commit(); } - /** - * Sets up the "My Appointments" filter button. - */ private void setupMyAppointmentFilter() { binding.btnMyAppointments.setOnClickListener(v -> { loadAppointmentData(); }); } - /** - * Fetches current user info to get the employeeId. - */ private void loadCurrentUserInfo() { authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -157,17 +150,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment, binding.spinnerStatus, binding.spinnerStore); } - /** - * Sets up the date selection listener for the calendar. - */ private void setupCalendar() { binding.calendarView.setOnDateChangedListener((widget, date, selected) -> { if (selected) { @@ -184,17 +171,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } - /** - * Updates calendar indicators to highlight dates that have scheduled appointments. - */ private void updateCalendarDecorators() { HashSet datesWithAppointments = new HashSet<>(); - SimpleDateFormat displayFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); for (AppointmentDTO appointment : appointmentList) { try { - //Get the appointment date - Date date = displayFormat.parse(appointment.getAppointmentDate()); - //if the date is not null, add it to the hashset + Date date = dateFormat.parse(appointment.getAppointmentDate()); if (date != null) { Calendar cal = Calendar.getInstance(); cal.setTime(date); @@ -204,56 +185,27 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. Log.e("AppointmentFragment", "Error parsing date: " + appointment.getAppointmentDate()); } } - //update the indicators to the calendar binding.calendarView.removeDecorators(); binding.calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments)); } - /** - * Configures the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchAppointment, this::loadAppointmentData); } - /** - * Configures the status filter spinner. - */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadAppointmentData); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadAppointmentData); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Initializes the SwipeRefreshLayout to allow manual data refreshing. - */ private void setupSwipeRefresh() { binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData); } - /** - * Navigates to the appointment detail screen for editing or creating an appointment. - */ private void openAppointmentDetails(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -263,9 +215,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args); } - /** - * Handles item click in the appointment list. - */ @Override public void onAppointmentClick(int position) { openAppointmentDetails(position); @@ -278,16 +227,14 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. } } - /** - * Fetches appointment data from the server with all active filters. - */ private void loadAppointmentData() { String query = binding.etSearchAppointment.getText().toString().trim(); String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } String selectedDateString = null; @@ -304,41 +251,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. if (status.equals("All Statuses")) status = null; else status = status.toUpperCase(); - appointmentViewModel.getAllAppointments(0, 500, query, status, storeId, selectedDateString, employeeId).observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshAppointment.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshAppointment.setRefreshing(false); - if (resource.data != null) { - appointmentList.clear(); - appointmentList.addAll(resource.data.getContent()); - updateCalendarDecorators(); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshAppointment.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load appointments: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("AppointmentFragment", "Error loading appointments: " + resource.message); - break; - } - }); + viewModel.loadAppointments(query, status, storeId, selectedDateString, employeeId); } - /** - * Initializes the RecyclerView for displaying appointments. - */ private void setupRecyclerView() { adapter = new AppointmentAdapter(appointmentList, this); binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAppointments.setAdapter(adapter); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 259a0a9e..6cefedb8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -1,7 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -22,8 +21,7 @@ import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.InventoryViewModel; -import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.viewmodels.InventoryListViewModel; import com.example.petstoremobile.utils.SpinnerUtils; import java.util.ArrayList; @@ -34,33 +32,18 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener { - private static final String TAG = "InventoryFragment"; - private static final int PAGE_SIZE = 20; - private FragmentInventoryBinding binding; private final List inventoryList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private InventoryAdapter adapter; - private InventoryViewModel viewModel; + private InventoryListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; - // Pagination - private int currentPage = 0; - private boolean isLastPage = false; - private boolean isLoading = false; - - /** - * Initializes the fragment and its ViewModel. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(InventoryViewModel.class); + viewModel = new ViewModelProvider(this).get(InventoryListViewModel.class); } - /** - * Sets up the fragment's UI components, including the inventory list and search. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -72,8 +55,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); + loadInventory(true); - loadStoreData(); binding.fabAddInventory.setOnClickListener(v -> openDetail(null)); @@ -82,6 +66,23 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn return binding.getRoot(); } + private void observeViewModel() { + viewModel.getInventory().observe(getViewLifecycleOwner(), list -> { + inventoryList.clear(); + inventoryList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshInventory.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -95,49 +96,30 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn ); } + @Override + public void onResume() { + super.onResume(); + viewModel.loadStores(); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore); } - /** - * Sets up the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchInventory, () -> loadInventory(true)); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadInventory(true)); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - viewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Initializes the RecyclerView with a layout manager, and adapter. - */ private void setupRecyclerView() { adapter = new InventoryAdapter(inventoryList, this); binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext())); @@ -146,105 +128,45 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn binding.recyclerViewInventory.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - if (dy <= 0) - return; + if (dy <= 0) return; LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewInventory.getLayoutManager(); - if (lm == null) - return; + if (lm == null) return; int visible = lm.getChildCount(); int total = lm.getItemCount(); int firstVis = lm.findFirstVisibleItemPosition(); - if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { loadInventory(false); } } }); } - /** - * Sets up the SwipeRefreshLayout to reload the first page of inventory items. - */ private void setupSwipeRefresh() { binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true)); } - /** - * Fetches a page of inventory items from the API. - */ private void loadInventory(boolean reset) { - if (isLoading) return; - - if (reset) { - currentPage = 0; - isLastPage = false; - } - - // Search text from input String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : ""; if (query.isEmpty()) query = null; Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } - //Load all inventory items from the backend using viewModel - viewModel.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - isLoading = true; - binding.swipeRefreshInventory.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - isLoading = false; - binding.swipeRefreshInventory.setRefreshing(false); - if (resource.data != null) { - if (reset) inventoryList.clear(); - inventoryList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - isLastPage = resource.data.isLast(); - if (!isLastPage) currentPage++; - } - break; - case ERROR: - // Hide loading indicator and toast error message - isLoading = false; - binding.swipeRefreshInventory.setRefreshing(false); - Log.e(TAG, "Error: " + resource.message); - Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show(); - break; - } - }); + viewModel.loadInventory(reset, query, storeId); } - /** - * Navigates to the inventory detail screen for a specific item or to add a new one. - */ private void openDetail(InventoryDTO inv) { Bundle args = new Bundle(); - if (inv != null) { args.putLong("inventoryId", inv.getInventoryId()); } - NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args); } - /** - * Reloads inventory data when changes occur. - */ - public void onInventoryChanged() { - loadInventory(true); - } - - /** - * Handles item click in the inventory list. - */ @Override public void onInventoryClick(int position) { if (position >= 0 && position < inventoryList.size()) { @@ -252,9 +174,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } } - /** - * Updates the bulk deletion UI visibility and count when items are selected or deselected. - */ @Override public void onSelectionChanged(int selectedCount) { if (bulkDeleteHandler != null) { 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 87a1a54c..bc1b9a6f 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 @@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -25,8 +24,7 @@ import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.PetListViewModel; import java.util.ArrayList; import java.util.List; @@ -40,28 +38,19 @@ import dagger.hilt.android.AndroidEntryPoint; public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener { private FragmentPetBinding binding; private List petList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private PetAdapter adapter; - private PetViewModel viewModel; - private StoreViewModel storeViewModel; + private PetListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; - /** - * Initializes the fragment and its associated ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PetViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(PetListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -75,6 +64,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); binding.fabAddPet.setOnClickListener(v -> openPetDetails()); @@ -83,6 +73,23 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen return binding.getRoot(); } + private void observeViewModel() { + viewModel.getPets().observe(getViewLifecycleOwner(), list -> { + petList.clear(); + petList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshPet.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -96,83 +103,62 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen ); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - - /** - * Reloads data every time the fragment becomes visible. - */ @Override public void onResume() { super.onResume(); loadPetData(); - loadStoreData(); + viewModel.loadStores(); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet, binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore); } - /** - * Configures the search bar. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchPet, this::loadPetData); } - /** - * Configures the status filter spinner. - */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData); } - /** - * Configures the species filter spinner with species. - */ private void setupSpeciesFilter() { String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, species, this::loadPetData); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Sets up the SwipeRefreshLayout. - */ private void setupSwipeRefresh() { binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData); } - /** - * Navigates to the pet profile screen. - */ + private void loadPetData() { + String query = binding.etSearchPet.getText().toString().trim(); + String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; + String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species"; + + Long storeId = null; + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + } + + viewModel.loadPets(query, status, species, storeId); + } + + private void setupRecyclerView() { + adapter = new PetAdapter(petList, this); + adapter.setBaseUrl(baseUrl); + adapter.setToken(tokenManager.getToken()); + binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewPets.setAdapter(adapter); + } + private void openPetProfile(int position) { Bundle args = new Bundle(); PetDTO pet = petList.get(position); @@ -180,9 +166,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args); } - /** - * Navigates to the pet detail screen. - */ private void openPetDetails() { NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail); } @@ -199,54 +182,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } } - /** - * Fetches pet data from the server with all active filters. - */ - private void loadPetData() { - String query = binding.etSearchPet.getText().toString().trim(); - String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; - String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species"; - - Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); - } - - if (status.equals("All Statuses")) status = null; - if (species.equals("All Species")) species = null; - - viewModel.getAllPets(0, 100, query, status, species, storeId, null, "petName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - switch (resource.status) { - case LOADING: - binding.swipeRefreshPet.setRefreshing(true); - break; - case SUCCESS: - binding.swipeRefreshPet.setRefreshing(false); - if (resource.data != null) { - petList.clear(); - petList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - binding.swipeRefreshPet.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load pets: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("PetFragment", "Error loading pets: " + resource.message); - break; - } - }); - } - - /** - * Initializes the RecyclerView. - */ - private void setupRecyclerView() { - adapter = new PetAdapter(petList, this); - adapter.setBaseUrl(baseUrl); - adapter.setToken(tokenManager.getToken()); - binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext())); - binding.recyclerViewPets.setAdapter(adapter); + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } } 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 6ae3d349..84f076a2 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 @@ -9,21 +9,18 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; 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.ProductDTO; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.ProductViewModel; +import com.example.petstoremobile.viewmodels.ProductListViewModel; import java.util.ArrayList; import java.util.List; @@ -38,24 +35,17 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc private FragmentProductBinding binding; private List productList = new ArrayList<>(); - private List categoryList = new ArrayList<>(); private ProductAdapter adapter; - private ProductViewModel viewModel; + private ProductListViewModel viewModel; @Inject @Named("baseUrl") String baseUrl; - /** - * Initializes the fragment and its associated ProductViewModel. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ProductViewModel.class); + viewModel = new ViewModelProvider(this).get(ProductListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -66,6 +56,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc setupCategoryFilter(); setupSwipeRefresh(); setupFilterToggle(); + observeViewModel(); binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1)); @@ -74,67 +65,67 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc return binding.getRoot(); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; + private void observeViewModel() { + viewModel.getProducts().observe(getViewLifecycleOwner(), list -> { + productList.clear(); + productList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getCategories().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, list, + CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshProduct.setRefreshing(loading); + }); } - /** - * Reloads data every time the fragment becomes visible. - */ @Override public void onResume() { super.onResume(); loadProductData(); - loadCategoryData(); + viewModel.loadCategories(); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchProduct, binding.spinnerCategory); } - /** - * Configures the search bar for triggering data load from backend. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData); } - /** - * Configures the category filter spinner. - */ private void setupCategoryFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData); } - /** - * Fetches category data to populate the category filter. - */ - private void loadCategoryData() { - viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - categoryList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, categoryList, - CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId); - } - }); - } - - /** - * Sets up the SwipeRefreshLayout. - */ private void setupSwipeRefresh() { binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData); } - /** - * Navigates to the product detail screen. - */ + private void loadProductData() { + String query = binding.etSearchProduct.getText().toString().trim(); + if (query.isEmpty()) query = null; + + Long categoryId = null; + List categories = viewModel.getCategories().getValue(); + if (binding.spinnerCategory.getSelectedItemPosition() > 0 && categories != null && !categories.isEmpty()) { + categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId(); + } + + viewModel.loadProducts(query, categoryId); + } + + private void setupRecyclerView() { + adapter = new ProductAdapter(productList, this); + adapter.setBaseUrl(baseUrl); + binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewProducts.setAdapter(adapter); + } + private void openProductDetails(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -149,51 +140,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc openProductDetails(position); } - /** - * Fetches product data from the server with search query, category, and sorting. - */ - private void loadProductData() { - String query = binding.etSearchProduct.getText().toString().trim(); - if (query.isEmpty()) query = null; - - Long categoryId = null; - if (binding.spinnerCategory.getSelectedItemPosition() > 0 && !categoryList.isEmpty()) { - categoryId = categoryList.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId(); - } - - viewModel.getAllProducts(query, categoryId, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - switch (resource.status) { - case LOADING: - binding.swipeRefreshProduct.setRefreshing(true); - break; - case SUCCESS: - binding.swipeRefreshProduct.setRefreshing(false); - if (resource.data != null) { - productList.clear(); - productList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - binding.swipeRefreshProduct.setRefreshing(false); - if (getContext() != null) { - Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show(); - } - Log.e("ProductFragment", "Error loading products: " + resource.message); - break; - } - }); - } - - /** - * Initializes the RecyclerView. - */ - private void setupRecyclerView() { - adapter = new ProductAdapter(productList, this); - adapter.setBaseUrl(baseUrl); - binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); - binding.recyclerViewProducts.setAdapter(adapter); + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index e1db78b6..6a066528 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -1,11 +1,9 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -21,12 +19,9 @@ import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; -import com.example.petstoremobile.viewmodels.ProductViewModel; -import com.example.petstoremobile.viewmodels.SupplierViewModel; +import com.example.petstoremobile.viewmodels.ProductSupplierListViewModel; import java.util.ArrayList; import java.util.List; @@ -39,29 +34,17 @@ public class ProductSupplierFragment extends Fragment private FragmentProductSupplierBinding binding; private List psList = new ArrayList<>(); - private List productList = new ArrayList<>(); - private List supplierList = new ArrayList<>(); private ProductSupplierAdapter adapter; - private ProductSupplierViewModel viewModel; - private ProductViewModel productViewModel; - private SupplierViewModel supplierViewModel; + private ProductSupplierListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; - /** - * Initializes the fragment and its associated ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class); - productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); - supplierViewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + viewModel = new ViewModelProvider(this).get(ProductSupplierListViewModel.class); } - /** - * Sets up the fragment's UI components, including the RecyclerView, search, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -74,6 +57,7 @@ public class ProductSupplierFragment extends Fragment setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); binding.fabAddPS.setOnClickListener(v -> openDetail(-1)); @@ -82,6 +66,28 @@ public class ProductSupplierFragment extends Fragment return binding.getRoot(); } + private void observeViewModel() { + viewModel.getProductSuppliers().observe(getViewLifecycleOwner(), list -> { + psList.clear(); + psList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getProducts().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, list, + ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId); + }); + + viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, list, + SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshPS.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -95,136 +101,65 @@ public class ProductSupplierFragment extends Fragment ); } + @Override + public void onResume() { + super.onResume(); + loadData(); + viewModel.loadFilterData(); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Reloads data every time the fragment becomes visible. - */ - @Override - public void onResume() { - super.onResume(); - loadData(); - loadFilterData(); - } - - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPS, binding.spinnerProduct, binding.spinnerSupplier); } - /** - * Initializes the RecyclerView with a layout manager and adapter for product-supplier data. - */ private void setupRecyclerView() { adapter = new ProductSupplierAdapter(psList, this); binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPS.setAdapter(adapter); } - /** - * Configures the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchPS, this::loadData); } - /** - * Configures the product filter spinner. - */ private void setupProductFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData); } - /** - * Configures the supplier filter spinner. - */ private void setupSupplierFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData); } - /** - * Fetches products and suppliers to populate the filters. - */ - private void loadFilterData() { - productViewModel.getAllProducts(null, null, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - productList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, productList, - ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId); - } - }); - - supplierViewModel.getAllSuppliers(0, 100, null, "supCompany").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - supplierList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, supplierList, - SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId); - } - }); - } - - /** - * Sets up the SwipeRefreshLayout to allow manual reloading of product-supplier data. - */ private void setupSwipeRefresh() { binding.swipeRefreshPS.setOnRefreshListener(this::loadData); } - /** - * Fetches product-supplier data from the server through the ViewModel with search query and filters. - */ private void loadData() { String query = binding.etSearchPS.getText().toString().trim(); if (query.isEmpty()) query = null; Long productId = null; - if (binding.spinnerProduct.getSelectedItemPosition() > 0 && !productList.isEmpty()) { - productId = productList.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId(); + List products = viewModel.getProducts().getValue(); + if (binding.spinnerProduct.getSelectedItemPosition() > 0 && products != null && !products.isEmpty()) { + productId = products.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId(); } Long supplierId = null; - if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && !supplierList.isEmpty()) { - supplierId = supplierList.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId(); + List suppliers = viewModel.getSuppliers().getValue(); + if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && suppliers != null && !suppliers.isEmpty()) { + supplierId = suppliers.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId(); } - viewModel.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshPS.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshPS.setRefreshing(false); - if (resource.data != null) { - psList.clear(); - psList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshPS.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("PSFragment", "Error loading: " + resource.message); - break; - } - }); + viewModel.loadProductSuppliers(query, productId, supplierId); } - /** - * Navigates to the product-supplier detail screen for a specific item or to add a new record. - */ private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -235,9 +170,6 @@ public class ProductSupplierFragment extends Fragment NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args); } - /** - * Handles item click in the product-supplier list. - */ @Override public void onProductSupplierClick(int position) { openDetail(position); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index b27c9c1f..64dc0ea5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -1,11 +1,9 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -19,11 +17,9 @@ import com.example.petstoremobile.adapters.PurchaseOrderAdapter; import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.PurchaseOrderListViewModel; import java.util.ArrayList; import java.util.List; @@ -36,24 +32,15 @@ public class PurchaseOrderFragment extends Fragment private FragmentPurchaseOrderBinding binding; private List poList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private PurchaseOrderAdapter adapter; - private PurchaseOrderViewModel viewModel; - private StoreViewModel storeViewModel; + private PurchaseOrderListViewModel viewModel; - /** - * Initializes the fragment and its associated ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(PurchaseOrderListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -64,121 +51,72 @@ public class PurchaseOrderFragment extends Fragment setupStoreFilter(); setupSwipeRefresh(); setupFilterToggle(); + observeViewModel(); UIUtils.setupHamburgerMenu(binding.btnHamburgerPO, this); return binding.getRoot(); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; + private void observeViewModel() { + viewModel.getPurchaseOrders().observe(getViewLifecycleOwner(), list -> { + poList.clear(); + poList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshPO.setRefreshing(loading); + }); } - /** - * Reloads data every time the fragment becomes visible. - */ @Override public void onResume() { super.onResume(); loadData(); - loadStoreData(); + viewModel.loadStores(); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO, binding.spinnerStore); } - /** - * Configures the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchPO, this::loadData); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadData); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Initializes the RecyclerView with a layout manager and adapter. - */ private void setupRecyclerView() { adapter = new PurchaseOrderAdapter(poList, this); binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPO.setAdapter(adapter); } - /** - * Sets up the SwipeRefreshLayout to allow manual reloading of purchase order data. - */ private void setupSwipeRefresh() { binding.swipeRefreshPO.setOnRefreshListener(this::loadData); } - /** - * Fetches purchase order data from the server with active filters and updates the UI. - */ private void loadData() { String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : ""; if (query.isEmpty()) query = null; Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } - viewModel.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshPO.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshPO.setRefreshing(false); - if (resource.data != null) { - poList.clear(); - poList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshPO.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load purchase orders: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("POFragment", "Error loading purchase orders: " + resource.message); - break; - } - }); + viewModel.loadPurchaseOrders(query, storeId); } - /** - * Navigates to the purchase order detail screen for a specific record. - */ private void openDetail(int position) { Bundle args = new Bundle(); PurchaseOrderDTO po = poList.get(position); @@ -186,11 +124,14 @@ public class PurchaseOrderFragment extends Fragment NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args); } - /** - * Handles item click in the purchase order list. - */ @Override public void onPurchaseOrderClick(int position) { openDetail(position); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index 850717e4..fef5d994 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -8,7 +8,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -19,11 +18,9 @@ import com.example.petstoremobile.adapters.SaleAdapter; import com.example.petstoremobile.databinding.FragmentSaleBinding; import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.SaleViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.SaleListViewModel; import java.util.ArrayList; import java.util.List; @@ -33,20 +30,10 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { - private static final String TAG = "SaleFragment"; - private static final int PAGE_SIZE = 200; - private FragmentSaleBinding binding; private final List saleList = new ArrayList<>(); - private final List storeList = new ArrayList<>(); private SaleAdapter adapter; - private SaleViewModel saleViewModel; - private StoreViewModel storeViewModel; - - // Pagination - private int currentPage = 0; - private boolean isLastPage = false; - private boolean isLoading = false; + private SaleListViewModel viewModel; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -58,8 +45,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(SaleListViewModel.class); setupRecyclerView(); setupSearch(); @@ -67,6 +53,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis setupPaymentMethodFilter(); setupSwipeRefresh(); setupFilterToggle(); + observeViewModel(); + loadSales(true); UIUtils.setupHamburgerMenu(binding.btnHamburger, this); @@ -78,10 +66,27 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis NavHostFragment.findNavController(this).navigate(R.id.nav_refund)); } + private void observeViewModel() { + viewModel.getSales().observe(getViewLifecycleOwner(), list -> { + saleList.clear(); + saleList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshSale.setRefreshing(loading); + }); + } + @Override public void onResume() { super.onResume(); - loadStoreData(); + viewModel.loadStores(); } private void setupFilterToggle() { @@ -93,28 +98,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true)); } - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList.clear(); - storeList.addAll(resource.data.getContent()); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId); - } - }); - } - private void setupPaymentMethodFilter() { String[] paymentMethods = {"Payments", "Cash", "Card"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerPaymentMethod, paymentMethods, () -> loadSales(true)); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - private void setupRecyclerView() { adapter = new SaleAdapter(saleList, this); binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); @@ -129,7 +117,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis int visible = lm.getChildCount(); int total = lm.getItemCount(); int firstVis = lm.findFirstVisibleItemPosition(); - if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { loadSales(false); } } @@ -146,13 +135,6 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } private void loadSales(boolean reset) { - if (isLoading) return; - - if (reset) { - currentPage = 0; - isLastPage = false; - } - String query = binding.etSearchSale != null ? binding.etSearchSale.getText().toString().trim() : ""; if (query.isEmpty()) query = null; @@ -162,39 +144,12 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } - saleViewModel.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - switch (resource.status) { - case LOADING: - isLoading = true; - binding.swipeRefreshSale.setRefreshing(true); - break; - case SUCCESS: - isLoading = false; - binding.swipeRefreshSale.setRefreshing(false); - if (resource.data != null) { - if (reset) saleList.clear(); - saleList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - isLastPage = resource.data.isLast(); - if (!isLastPage) currentPage++; - } - break; - case ERROR: - isLoading = false; - binding.swipeRefreshSale.setRefreshing(false); - Log.e(TAG, "Error loading sales: " + resource.message); - if (getContext() != null) { - Toast.makeText(getContext(), "Failed to load sales: " + resource.message, Toast.LENGTH_SHORT).show(); - } - break; - } - }); + viewModel.loadSales(reset, query, paymentMethod, storeId); } @Override @@ -210,4 +165,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail, args); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index 77136d4e..3a1b45a1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -1,7 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -21,7 +20,7 @@ import com.example.petstoremobile.databinding.FragmentServiceBinding; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.ServiceViewModel; +import com.example.petstoremobile.viewmodels.ServiceListViewModel; import java.util.ArrayList; import java.util.List; @@ -34,32 +33,18 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener { - private static final String TAG = "ServiceFragment"; - private static final int PAGE_SIZE = 20; - private FragmentServiceBinding binding; private final List serviceList = new ArrayList<>(); private ServiceAdapter adapter; - private ServiceViewModel viewModel; + private ServiceListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; - // Pagination - private int currentPage = 0; - private boolean isLastPage = false; - private boolean isLoading = false; - - /** - * Initializes the fragment and its associated ViewModel. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ServiceViewModel.class); + viewModel = new ViewModelProvider(this).get(ServiceListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -70,15 +55,27 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); + loadServices(true); - binding.fabAddService.setOnClickListener(v -> openDetail(null)); - UIUtils.setupHamburgerMenu(binding.btnHamburger, this); return binding.getRoot(); } + private void observeViewModel() { + viewModel.getServices().observe(getViewLifecycleOwner(), list -> { + serviceList.clear(); + serviceList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshService.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -98,23 +95,14 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic binding = null; } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchService); } - /** - * Sets up the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchService, () -> loadServices(true)); } - /** - * Initializes the RecyclerView with a layout manager and adapter. - */ private void setupRecyclerView() { adapter = new ServiceAdapter(serviceList, this); binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext())); @@ -129,66 +117,24 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic int visible = lm.getChildCount(); int total = lm.getItemCount(); int firstVis = lm.findFirstVisibleItemPosition(); - if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { loadServices(false); } } }); } - /** - * Sets up the SwipeRefreshLayout. - */ private void setupSwipeRefresh() { binding.swipeRefreshService.setOnRefreshListener(() -> loadServices(true)); } - /** - * Fetches a page of services from the API. - */ private void loadServices(boolean reset) { - if (isLoading) return; - - if (reset) { - currentPage = 0; - isLastPage = false; - } - String query = binding.etSearchService.getText().toString().trim(); if (query.isEmpty()) query = null; - - viewModel.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - switch (resource.status) { - case LOADING: - isLoading = true; - binding.swipeRefreshService.setRefreshing(true); - break; - case SUCCESS: - isLoading = false; - binding.swipeRefreshService.setRefreshing(false); - if (resource.data != null) { - if (reset) serviceList.clear(); - serviceList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - isLastPage = resource.data.isLast(); - if (!isLastPage) currentPage++; - } - break; - case ERROR: - isLoading = false; - binding.swipeRefreshService.setRefreshing(false); - Log.e(TAG, "Error: " + resource.message); - Toast.makeText(getContext(), "Failed to load services: " + resource.message, Toast.LENGTH_SHORT).show(); - break; - } - }); + viewModel.loadServices(reset, query); } - /** - * Navigates to the service detail screen. - */ private void openDetail(ServiceDTO service) { Bundle args = new Bundle(); if (service != null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java index 383d702a..8407d3f6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java @@ -1,7 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; @@ -14,7 +13,7 @@ import com.example.petstoremobile.adapters.EmployeeAdapter; import com.example.petstoremobile.databinding.FragmentStaffBinding; import com.example.petstoremobile.dtos.EmployeeDTO; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.EmployeeViewModel; +import com.example.petstoremobile.viewmodels.StaffListViewModel; import dagger.hilt.android.AndroidEntryPoint; import java.util.*; @@ -22,21 +21,22 @@ import java.util.*; public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener { private FragmentStaffBinding binding; - private EmployeeViewModel employeeViewModel; - private List employeeList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private StaffListViewModel viewModel; + private List staffList = new ArrayList<>(); private EmployeeAdapter adapter; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentStaffBinding.inflate(inflater, container, false); - employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class); + viewModel = new ViewModelProvider(this).get(StaffListViewModel.class); setupRecyclerView(); setupSearch(); setupSwipeRefresh(); - loadStaff(); + observeViewModel(); + + viewModel.loadStaff(); binding.fabAddStaff.setOnClickListener(v -> openDetail(-1)); @@ -46,70 +46,36 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye return binding.getRoot(); } + private void observeViewModel() { + viewModel.getFilteredEmployees().observe(getViewLifecycleOwner(), list -> { + staffList.clear(); + staffList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshStaff.setRefreshing(loading); + }); + } + private void setupRecyclerView() { - adapter = new EmployeeAdapter(filteredList, this); + adapter = new EmployeeAdapter(staffList, this); binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewStaff.setAdapter(adapter); } private void setupSearch() { - UIUtils.attachSearch(binding.etSearchStaff, () -> filter(binding.etSearchStaff.getText().toString())); + UIUtils.attachSearch(binding.etSearchStaff, () -> viewModel.filter(binding.etSearchStaff.getText().toString())); } private void setupSwipeRefresh() { - binding.swipeRefreshStaff.setOnRefreshListener(this::loadStaff); - } - - private void filter(String query) { - filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(employeeList); - } else { - String lower = query.toLowerCase(); - for (EmployeeDTO e : employeeList) { - if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower)) - || (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower)) - || (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower)) - || (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) { - filteredList.add(e); - } - } - } - adapter.notifyDataSetChanged(); - } - - private void loadStaff() { - binding.swipeRefreshStaff.setRefreshing(true); - employeeViewModel.getAllEmployees(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - binding.swipeRefreshStaff.setRefreshing(false); - if (resource.data != null) { - employeeList.clear(); - employeeList.addAll(resource.data.getContent()); - filter(binding != null ? binding.etSearchStaff.getText().toString() : ""); - } - break; - case ERROR: - binding.swipeRefreshStaff.setRefreshing(false); - if (getContext() != null) { - Toast.makeText(getContext(), resource.message != null ? resource.message : "Failed to load staff", - Toast.LENGTH_SHORT).show(); - } - break; - case LOADING: - binding.swipeRefreshStaff.setRefreshing(true); - break; - } - } - }); + binding.swipeRefreshStaff.setOnRefreshListener(viewModel::loadStaff); } private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { - EmployeeDTO e = filteredList.get(position); + EmployeeDTO e = staffList.get(position); args.putLong("employeeId", e.getEmployeeId()); args.putString("username", e.getUsername() != null ? e.getUsername() : ""); args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : ""); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index eca755bb..78d43bd6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -9,20 +9,17 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SupplierAdapter; import com.example.petstoremobile.databinding.FragmentSupplierBinding; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.SupplierViewModel; +import com.example.petstoremobile.viewmodels.SupplierListViewModel; import java.util.ArrayList; import java.util.List; @@ -35,21 +32,15 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp private FragmentSupplierBinding binding; private List supplierList = new ArrayList<>(); private SupplierAdapter adapter; - private SupplierViewModel viewModel; + private SupplierListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; - /** - * Initializes the fragment and its associated SupplierViewModel. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + viewModel = new ViewModelProvider(this).get(SupplierListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -60,9 +51,10 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); + loadSupplierData(); - //Add button to opens the add dialog binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1)); UIUtils.setupHamburgerMenu(binding.btnHamburger, this); @@ -70,6 +62,18 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp return binding.getRoot(); } + private void observeViewModel() { + viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> { + supplierList.clear(); + supplierList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshSupplier.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -89,47 +93,27 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp binding = null; } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSupplier); } - /** - * Configures the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchSupplier, this::loadSupplierData); } - /** - * Sets up the SwipeRefreshLayout to allow manual reloading of supplier data. - */ private void setupSwipeRefresh() { binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData); } - /** - * Navigates to the supplier detail screen for editing an existing record or adding a new one. - */ private void openSupplierDetails(int position) { - //Make a bundle to pass data to the detail fragment Bundle args = new Bundle(); - - //if editing a supplier, add the supplier id to the bundle if (position != -1) { SupplierDTO supplier = supplierList.get(position); args.putLong("supId", supplier.getSupId()); } - NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args); } - - /** - * Handles item click in the supplier list. - */ @Override public void onSupplierClick(int position) { openSupplierDetails(position); @@ -142,47 +126,12 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp } } - /** - * Fetches all supplier data from the server through the ViewModel and updates the UI. - */ private void loadSupplierData() { String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : ""; if (query.isEmpty()) query = null; - - //Load suppliers from the backend with query and default sort - viewModel.getAllSuppliers(0, 100, query, "supCompany").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshSupplier.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshSupplier.setRefreshing(false); - if (resource.data != null) { - supplierList.clear(); - supplierList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshSupplier.setRefreshing(false); - if (getContext() != null) { - Toast.makeText(getContext(), "Failed to load suppliers: " + resource.message, Toast.LENGTH_SHORT).show(); - } - Log.e("SupplierFragment", "Error loading suppliers: " + resource.message); - break; - } - }); + viewModel.loadSuppliers(query); } - /** - * Initializes the RecyclerView with a layout manager and adapter for displaying suppliers. - */ private void setupRecyclerView() { adapter = new SupplierAdapter(supplierList, this); binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext())); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 371e5c20..ee11fb4e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -23,7 +23,7 @@ import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.viewmodels.PetViewModel; +import com.example.petstoremobile.viewmodels.PetProfileViewModel; import java.io.File; import java.util.Locale; @@ -46,17 +46,13 @@ public class PetProfileFragment extends Fragment { @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; - private PetViewModel viewModel; + private PetProfileViewModel viewModel; private ImagePickerHelper imagePickerHelper; - - /** - * Initializes activity launchers for gallery, camera, and permissions. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PetViewModel.class); + viewModel = new ViewModelProvider(this).get(PetProfileViewModel.class); imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() { @Override @@ -71,34 +67,27 @@ public class PetProfileFragment extends Fragment { }); } - /** - * Inflates the layout using view binding, initializes views, and sets up click listeners. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentPetProfileBinding.inflate(inflater, container, false); - // Set pet details to display if (getArguments() != null) { petId = getArguments().getLong("petId"); loadPetData(); loadPetImage((int) petId); } - //set button click listeners binding.btnBack.setOnClickListener(v -> { NavHostFragment.findNavController(this).popBackStack(); }); - //Make the edit button go to the pet detail view binding.btnEditPet.setOnClickListener(v -> { Bundle args = new Bundle(); args.putLong("petId", petId); NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, args); }); - //Make change photo button ask user to select a new photo binding.btnChangePhoto.setOnClickListener(v -> { imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage); }); @@ -112,9 +101,6 @@ public class PetProfileFragment extends Fragment { binding = null; } - /** - * Fetches current pet data from the backend and updates the UI. - */ private void loadPetData() { viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -133,7 +119,6 @@ public class PetProfileFragment extends Fragment { String status = pet.getPetStatus(); - // Display owner name only if the pet is Adopted or Owned if ("Adopted".equalsIgnoreCase(status) || "Owned".equalsIgnoreCase(status)) { binding.layoutPetOwner.setVisibility(View.VISIBLE); if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) { @@ -145,7 +130,6 @@ public class PetProfileFragment extends Fragment { binding.layoutPetOwner.setVisibility(View.GONE); } - // Display store name only if the pet is Adopted or Available if ("Available".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) { binding.layoutPetStore.setVisibility(View.VISIBLE); if (pet.getStoreName() != null && !pet.getStoreName().isEmpty()) { @@ -162,9 +146,6 @@ public class PetProfileFragment extends Fragment { }); } - /** - * Fetches and displays the pet\'s image from the server. - */ private void loadPetImage(int petId) { String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId); String token = tokenManager.getToken(); @@ -182,19 +163,14 @@ public class PetProfileFragment extends Fragment { }); } - /** - * Uploads a selected or captured image a pet photo through the ViewModel. - */ private void uploadPetImage(Uri uri) { try { File file = FileUtils.getFileFromUri(requireContext(), uri); if (file == null) return; - // Create RequestBody for file upload RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); - // Use ViewModel to upload image viewModel.uploadPetImage(petId, body).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status != Resource.Status.LOADING) { if (resource.status == Resource.Status.SUCCESS) { @@ -210,9 +186,6 @@ public class PetProfileFragment extends Fragment { } } - /** - * Sends a request to the ViewModel to remove the current pet photo. - */ private void deletePetImage() { viewModel.deletePetImage(petId).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status != Resource.Status.LOADING) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java new file mode 100644 index 00000000..683c79b4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java @@ -0,0 +1,83 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.AdoptionRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AdoptionListViewModel extends ViewModel { + private final AdoptionRepository adoptionRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> adoptions = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + + @Inject + public AdoptionListViewModel(AdoptionRepository adoptionRepository, StoreRepository storeRepository) { + this.adoptionRepository = adoptionRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getAdoptions() { return adoptions; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadAdoptions(boolean reset, String query, String status, Long storeId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + if ("All Statuses".equals(status)) status = null; + + isLoading.setValue(true); + adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, "adoptionDate,desc").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + List currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue()); + currentList.addAll(resource.data.getContent()); + adoptions.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeleteAdoptions(List ids) { + return adoptionRepository.bulkDeleteAdoptions(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java new file mode 100644 index 00000000..76c039ac --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java @@ -0,0 +1,163 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.repositories.SaleRepository; +import com.example.petstoremobile.utils.Resource; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AnalyticsViewModel extends ViewModel { + private final SaleRepository saleRepository; + + private final MutableLiveData analyticsData = new MutableLiveData<>(); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + private final MutableLiveData errorMessage = new MutableLiveData<>(); + + @Inject + public AnalyticsViewModel(SaleRepository saleRepository) { + this.saleRepository = saleRepository; + } + + public LiveData getAnalyticsData() { return analyticsData; } + public LiveData getIsLoading() { return isLoading; } + public LiveData getErrorMessage() { return errorMessage; } + + public void loadAnalytics() { + isLoading.setValue(true); + errorMessage.setValue(null); + saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + computeAnalytics(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + errorMessage.setValue(resource.message); + isLoading.setValue(false); + } + } + }); + } + + private void computeAnalytics(List sales) { + List regularSales = new ArrayList<>(); + for (SaleDTO s : sales) { + if (!Boolean.TRUE.equals(s.getIsRefund())) + regularSales.add(s); + } + + AnalyticsData data = new AnalyticsData(); + + // Summary + BigDecimal totalRevenue = BigDecimal.ZERO; + int totalItems = 0; + for (SaleDTO s : regularSales) { + if (s.getTotalAmount() != null) totalRevenue = totalRevenue.add(s.getTotalAmount()); + if (s.getItems() != null) { + for (SaleDTO.SaleItemDTO item : s.getItems()) { + if (item.getQuantity() != null) totalItems += Math.abs(item.getQuantity()); + } + } + } + data.totalRevenue = totalRevenue; + data.totalTransactions = regularSales.size(); + data.avgTransaction = data.totalTransactions > 0 + ? totalRevenue.divide(BigDecimal.valueOf(data.totalTransactions), 2, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + data.totalItems = totalItems; + + // Product Maps + Map revenueByProduct = new LinkedHashMap<>(); + Map quantityByProduct = new LinkedHashMap<>(); + Map paymentCount = new LinkedHashMap<>(); + Map employeeRevenue = new LinkedHashMap<>(); + + for (SaleDTO s : regularSales) { + // Payments + String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown"; + paymentCount.merge(method, 1, Integer::sum); + + // Employee + String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown"; + if (s.getTotalAmount() != null) employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add); + + // Items + if (s.getItems() != null) { + for (SaleDTO.SaleItemDTO item : s.getItems()) { + String name = item.getProductName() != null ? item.getProductName() : "Unknown"; + int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0; + BigDecimal lineTotal = item.getUnitPrice() != null + ? item.getUnitPrice().multiply(BigDecimal.valueOf(qty)) + : BigDecimal.ZERO; + revenueByProduct.merge(name, lineTotal, BigDecimal::add); + quantityByProduct.merge(name, qty, Integer::sum); + } + } + } + + // Sort Top Revenue + data.topRevenueProducts = new ArrayList<>(revenueByProduct.entrySet()); + data.topRevenueProducts.sort((a, b) -> b.getValue().compareTo(a.getValue())); + if (data.topRevenueProducts.size() > 5) data.topRevenueProducts = data.topRevenueProducts.subList(0, 5); + + // Sort Top Quantity + data.topQuantityProducts = new ArrayList<>(quantityByProduct.entrySet()); + data.topQuantityProducts.sort((a, b) -> b.getValue() - a.getValue()); + if (data.topQuantityProducts.size() > 5) data.topQuantityProducts = data.topQuantityProducts.subList(0, 5); + + // Payment Stats + data.paymentMethodStats = new ArrayList<>(paymentCount.entrySet()); + + // Employee Performance + data.employeePerformance = new ArrayList<>(employeeRevenue.entrySet()); + data.employeePerformance.sort((a, b) -> b.getValue().compareTo(a.getValue())); + + // Daily Revenue (last 7 days) + Map dailyMap = new TreeMap<>(); + for (int i = 6; i >= 0; i--) { + Calendar day = Calendar.getInstance(); + day.add(Calendar.DAY_OF_YEAR, -i); + String key = String.format("%04d-%02d-%02d", + day.get(Calendar.YEAR), day.get(Calendar.MONTH) + 1, day.get(Calendar.DAY_OF_MONTH)); + dailyMap.put(key, BigDecimal.ZERO); + } + for (SaleDTO s : regularSales) { + if (s.getSaleDate() != null && s.getTotalAmount() != null) { + String date = s.getSaleDate().length() >= 10 ? s.getSaleDate().substring(0, 10) : s.getSaleDate(); + if (dailyMap.containsKey(date)) dailyMap.merge(date, s.getTotalAmount(), BigDecimal::add); + } + } + data.dailyRevenue = new ArrayList<>(dailyMap.entrySet()); + + analyticsData.setValue(data); + } + + public static class AnalyticsData { + public BigDecimal totalRevenue; + public int totalTransactions; + public BigDecimal avgTransaction; + public int totalItems; + public List> topRevenueProducts; + public List> topQuantityProducts; + public List> paymentMethodStats; + public List> employeePerformance; + public List> dailyRevenue; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java new file mode 100644 index 00000000..8bdaf699 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java @@ -0,0 +1,65 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.AppointmentRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AppointmentListViewModel extends ViewModel { + private final AppointmentRepository appointmentRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> appointments = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public AppointmentListViewModel(AppointmentRepository appointmentRepository, StoreRepository storeRepository) { + this.appointmentRepository = appointmentRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getAppointments() { return appointments; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + + public void loadAppointments(String query, String status, Long storeId, String date, Long employeeId) { + isLoading.setValue(true); + appointmentRepository.getAllAppointments(0, 500, query, status, storeId, date, employeeId).observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + appointments.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeleteAppointments(List ids) { + return appointmentRepository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java new file mode 100644 index 00000000..0aa8021f --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java @@ -0,0 +1,132 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.ConversationDTO; +import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.MessageDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SendMessageRequest; +import com.example.petstoremobile.models.Chat; +import com.example.petstoremobile.models.Message; +import com.example.petstoremobile.repositories.ChatRepository; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ChatListViewModel extends ViewModel { + private final ChatRepository chatRepository; + private final CustomerRepository customerRepository; + + private final MutableLiveData> chatList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> messageList = new MutableLiveData<>(new ArrayList<>()); + private final Map customerNames = new HashMap<>(); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public ChatListViewModel(ChatRepository chatRepository, CustomerRepository customerRepository) { + this.chatRepository = chatRepository; + this.customerRepository = customerRepository; + } + + public LiveData> getChatList() { return chatList; } + public LiveData> getMessageList() { return messageList; } + public LiveData getIsLoading() { return isLoading; } + + public void loadCustomers() { + customerRepository.getAllCustomers(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + for (CustomerDTO c : resource.data.getContent()) { + customerNames.put(c.getCustomerId(), c.getFullName()); + } + loadConversations(); + } + }); + } + + public void loadConversations() { + isLoading.setValue(true); + chatRepository.getAllConversations().observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + List chats = new ArrayList<>(); + for (ConversationDTO dto : resource.data) { + String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + chats.add(new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId())); + } + chatList.setValue(chats); + isLoading.setValue(false); + } else if (resource != null && resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + }); + } + + public void loadMessageHistory(Long conversationId) { + chatRepository.getMessages(conversationId).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + List messages = new ArrayList<>(); + for (MessageDTO dto : resource.data) { + messages.add(dtoToModel(dto)); + } + messageList.setValue(messages); + } + }); + } + + public LiveData> sendMessage(Long conversationId, String text) { + return chatRepository.sendMessage(conversationId, new SendMessageRequest(text)); + } + + public void addMessageLocally(MessageDTO dto) { + List current = new ArrayList<>(messageList.getValue()); + current.add(dtoToModel(dto)); + messageList.setValue(current); + } + + public void updateConversationLocally(ConversationDTO dto) { + List current = new ArrayList<>(chatList.getValue()); + boolean updated = false; + String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + + for (int i = 0; i < current.size(); i++) { + if (current.get(i).getChatId().equals(String.valueOf(dto.getId()))) { + current.set(i, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId())); + updated = true; + break; + } + } + if (!updated) { + current.add(0, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId())); + } + chatList.setValue(current); + } + + private Message dtoToModel(MessageDTO dto) { + Message m = new Message(); + m.setId(dto.getId()); + m.setConversationId(dto.getConversationId()); + m.setSenderId(dto.getSenderId()); + m.setContent(dto.getContent()); + m.setTimestamp(dto.getTimestamp()); + m.setIsRead(dto.getIsRead()); + m.setAttachmentUrl(dto.getAttachmentUrl()); + m.setAttachmentName(dto.getAttachmentName()); + m.setAttachmentType(dto.getAttachmentType()); + return m; + } + + public String getCustomerName(Long customerId) { + return customerNames.getOrDefault(customerId, "Customer #" + customerId); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java new file mode 100644 index 00000000..c91f8337 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java @@ -0,0 +1,81 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.InventoryRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class InventoryListViewModel extends ViewModel { + private final InventoryRepository inventoryRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> inventory = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + + @Inject + public InventoryListViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository) { + this.inventoryRepository = inventoryRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getInventory() { return inventory; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadInventory(boolean reset, String query, Long storeId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + isLoading.setValue(true); + inventoryRepository.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + List currentList = reset ? new ArrayList<>() : new ArrayList<>(inventory.getValue()); + currentList.addAll(resource.data.getContent()); + inventory.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeleteInventory(List ids) { + return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids)); + } +} 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 new file mode 100644 index 00000000..8a567450 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java @@ -0,0 +1,69 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class PetListViewModel extends ViewModel { + private final PetRepository petRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> pets = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public PetListViewModel(PetRepository petRepository, StoreRepository storeRepository) { + this.petRepository = petRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getPets() { return pets; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + + public void loadPets(String query, String status, String species, Long storeId) { + if ("All Statuses".equals(status)) status = null; + if ("All Species".equals(species)) species = null; + + isLoading.setValue(true); + petRepository.getAllPets(0, 100, query, status, species, storeId, null, "petName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + pets.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeletePets(List ids) { + return petRepository.bulkDeletePets(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java new file mode 100644 index 00000000..1fb75f1c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java @@ -0,0 +1,35 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import okhttp3.MultipartBody; + +@HiltViewModel +public class PetProfileViewModel extends ViewModel { + private final PetRepository repository; + + @Inject + public PetProfileViewModel(PetRepository repository) { + this.repository = repository; + } + + public LiveData> getPetById(Long id) { + return repository.getPetById(id); + } + + public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { + return repository.uploadPetImage(id, image); + } + + public LiveData> deletePetImage(Long id) { + return repository.deletePetImage(id); + } +} 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 new file mode 100644 index 00000000..ecd2d238 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java @@ -0,0 +1,60 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.repositories.CategoryRepository; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ProductListViewModel extends ViewModel { + private final ProductRepository productRepository; + private final CategoryRepository categoryRepository; + + private final MutableLiveData> products = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> categories = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public ProductListViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) { + this.productRepository = productRepository; + this.categoryRepository = categoryRepository; + } + + public LiveData> getProducts() { return products; } + public LiveData> getCategories() { return categories; } + public LiveData getIsLoading() { return isLoading; } + + public void loadProducts(String query, Long categoryId) { + isLoading.setValue(true); + productRepository.getAllProducts(query, categoryId, 0, 100, "prodName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + products.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadCategories() { + categoryRepository.getAllCategories(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + categories.setValue(resource.data.getContent()); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java new file mode 100644 index 00000000..cad846c4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java @@ -0,0 +1,77 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.dtos.ProductSupplierDTO; +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.repositories.ProductSupplierRepository; +import com.example.petstoremobile.repositories.SupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ProductSupplierListViewModel extends ViewModel { + private final ProductSupplierRepository psRepository; + private final ProductRepository productRepository; + private final SupplierRepository supplierRepository; + + private final MutableLiveData> productSuppliers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> products = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> suppliers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public ProductSupplierListViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) { + this.psRepository = psRepository; + this.productRepository = productRepository; + this.supplierRepository = supplierRepository; + } + + public LiveData> getProductSuppliers() { return productSuppliers; } + public LiveData> getProducts() { return products; } + public LiveData> getSuppliers() { return suppliers; } + public LiveData getIsLoading() { return isLoading; } + + public void loadProductSuppliers(String query, Long productId, Long supplierId) { + isLoading.setValue(true); + psRepository.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + productSuppliers.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadFilterData() { + productRepository.getAllProducts(null, null, 0, 100, "prodName").observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + products.setValue(resource.data.getContent()); + } + }); + + supplierRepository.getAllSuppliers(0, 100, null, "supCompany").observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + suppliers.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeleteProductSuppliers(List ids) { + return psRepository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java new file mode 100644 index 00000000..438f4198 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java @@ -0,0 +1,60 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PurchaseOrderDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.PurchaseOrderRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class PurchaseOrderListViewModel extends ViewModel { + private final PurchaseOrderRepository purchaseOrderRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> purchaseOrders = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public PurchaseOrderListViewModel(PurchaseOrderRepository purchaseOrderRepository, StoreRepository storeRepository) { + this.purchaseOrderRepository = purchaseOrderRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getPurchaseOrders() { return purchaseOrders; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + + public void loadPurchaseOrders(String query, Long storeId) { + isLoading.setValue(true); + purchaseOrderRepository.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + purchaseOrders.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java new file mode 100644 index 00000000..a364a7d8 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java @@ -0,0 +1,77 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.SaleRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SaleListViewModel extends ViewModel { + private final SaleRepository saleRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> sales = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + + @Inject + public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository) { + this.saleRepository = saleRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getSales() { return sales; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadSales(boolean reset, String query, String paymentMethod, Long storeId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + isLoading.setValue(true); + saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + List currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue()); + currentList.addAll(resource.data.getContent()); + sales.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java new file mode 100644 index 00000000..d0fa121b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java @@ -0,0 +1,68 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.repositories.ServiceRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ServiceListViewModel extends ViewModel { + private final ServiceRepository repository; + + private final MutableLiveData> services = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + + @Inject + public ServiceListViewModel(ServiceRepository repository) { + this.repository = repository; + } + + public LiveData> getServices() { return services; } + public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadServices(boolean reset, String query) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + isLoading.setValue(true); + repository.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + List currentList = reset ? new ArrayList<>() : new ArrayList<>(services.getValue()); + currentList.addAll(resource.data.getContent()); + services.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public LiveData> bulkDeleteServices(List ids) { + return repository.bulkDeleteServices(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java new file mode 100644 index 00000000..1bd317ca --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java @@ -0,0 +1,71 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.repositories.EmployeeRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class StaffListViewModel extends ViewModel { + private final EmployeeRepository repository; + + private final MutableLiveData> employees = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> filteredEmployees = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + private String lastQuery = ""; + + @Inject + public StaffListViewModel(EmployeeRepository repository) { + this.repository = repository; + } + + public LiveData> getFilteredEmployees() { return filteredEmployees; } + public LiveData getIsLoading() { return isLoading; } + + public void loadStaff() { + isLoading.setValue(true); + repository.getAllEmployees(0, 100).observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + employees.setValue(resource.data.getContent()); + filter(lastQuery); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void filter(String query) { + this.lastQuery = query; + List all = employees.getValue(); + if (all == null) return; + + if (query.isEmpty()) { + filteredEmployees.setValue(new ArrayList<>(all)); + } else { + List filtered = new ArrayList<>(); + String lower = query.toLowerCase(); + for (EmployeeDTO e : all) { + if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower)) + || (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower)) + || (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower)) + || (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) { + filtered.add(e); + } + } + filteredEmployees.setValue(filtered); + } + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java new file mode 100644 index 00000000..072ad3bd --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java @@ -0,0 +1,51 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.repositories.SupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SupplierListViewModel extends ViewModel { + private final SupplierRepository repository; + + private final MutableLiveData> suppliers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public SupplierListViewModel(SupplierRepository repository) { + this.repository = repository; + } + + public LiveData> getSuppliers() { return suppliers; } + public LiveData getIsLoading() { return isLoading; } + + public void loadSuppliers(String query) { + isLoading.setValue(true); + repository.getAllSuppliers(0, 100, query, "supCompany").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + suppliers.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public LiveData> bulkDeleteSuppliers(List ids) { + return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids)); + } +} From 872042de5af0ce37f351d8c3479310652dcd212b Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:17:11 -0600 Subject: [PATCH 16/33] deleted unused viewmodels --- .../viewmodels/AdoptionViewModel.java | 68 ------------- .../viewmodels/AppointmentViewModel.java | 42 -------- .../viewmodels/ChatViewModel.java | 58 ----------- .../viewmodels/CustomerViewModel.java | 47 --------- .../viewmodels/EmployeeViewModel.java | 43 -------- .../viewmodels/InventoryViewModel.java | 90 ----------------- .../viewmodels/PetViewModel.java | 98 ------------------- .../viewmodels/ProductSupplierViewModel.java | 58 ----------- .../viewmodels/ProductViewModel.java | 84 ---------------- .../viewmodels/PurchaseOrderViewModel.java | 37 ------- .../viewmodels/SaleViewModel.java | 35 ------- .../viewmodels/ServiceViewModel.java | 68 ------------- .../viewmodels/StoreViewModel.java | 47 --------- .../viewmodels/SupplierViewModel.java | 68 ------------- .../viewmodels/UserViewModel.java | 27 ----- 15 files changed, 870 deletions(-) delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/EmployeeViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java deleted file mode 100644 index 12eb9779..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.AdoptionDTO; -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.repositories.AdoptionRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class AdoptionViewModel extends ViewModel { - private final AdoptionRepository repository; - - @Inject - public AdoptionViewModel(AdoptionRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all adoptions with filters. - */ - public LiveData>> getAllAdoptions(int page, int size, String query, String status, Long storeId, String date, Long employeeId) { - return repository.getAllAdoptions(page, size, query, status, storeId, date, employeeId); - } - - /** - * Retrieves a single adoption by its ID. - */ - public LiveData> getAdoptionById(Long id) { - return repository.getAdoptionById(id); - } - - /** - * Creates a new adoption record. - */ - public LiveData> createAdoption(AdoptionDTO adoption) { - return repository.createAdoption(adoption); - } - - /** - * Updates an existing adoption record by ID. - */ - public LiveData> updateAdoption(Long id, AdoptionDTO adoption) { - return repository.updateAdoption(id, adoption); - } - - /** - * Deletes an adoption record by ID. - */ - public LiveData> deleteAdoption(Long id) { - return repository.deleteAdoption(id); - } - - /** - * Deletes multiple adoption records. - */ - public LiveData> bulkDeleteAdoptions(List ids) { - return repository.bulkDeleteAdoptions(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java deleted file mode 100644 index 5605a8d1..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.AppointmentDTO; -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.repositories.AppointmentRepository; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class AppointmentViewModel extends ViewModel { - private final AppointmentRepository repository; - - @Inject - public AppointmentViewModel(AppointmentRepository repository) { - this.repository = repository; - } - - // API CRUD - - /** - * Fetches a paginated list of all appointments with optional filters. - */ - public LiveData>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date, Long employeeId) { - return repository.getAllAppointments(page, size, query, status, storeId, date, employeeId); - } - - /** - * Deletes multiple appointment records. - */ - public LiveData> bulkDeleteAppointments(List ids) { - return repository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java deleted file mode 100644 index 2b516490..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.ConversationDTO; -import com.example.petstoremobile.dtos.CustomerDTO; -import com.example.petstoremobile.dtos.MessageDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.SendMessageRequest; -import com.example.petstoremobile.repositories.ChatRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; -/** - * ViewModel for managing chat-related UI state and data operations. - */ -@HiltViewModel -public class ChatViewModel extends ViewModel { - private final ChatRepository repository; - - @Inject - public ChatViewModel(ChatRepository repository) { - this.repository = repository; - } - - /** - * Retrieves all chat conversations for the current user. - */ - public LiveData>> getAllConversations() { - return repository.getAllConversations(); - } - - /** - * Retrieves the message history for a specific conversation. - */ - public LiveData>> getMessages(Long conversationId) { - return repository.getMessages(conversationId); - } - - /** - * Sends a plain text message to a conversation. - */ - public LiveData> sendMessage(Long conversationId, SendMessageRequest request) { - return repository.sendMessage(conversationId, request); - } - - /** - * Fetches a paginated list of customers. - */ - public LiveData>> getAllCustomers(int page, int size) { - return repository.getAllCustomers(page, size); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java deleted file mode 100644 index d916ff75..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.CustomerDTO; -import com.example.petstoremobile.dtos.DropdownDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.repositories.CustomerRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class CustomerViewModel extends ViewModel { - private final CustomerRepository repository; - - @Inject - public CustomerViewModel(CustomerRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all customers. - */ - public LiveData>> getAllCustomers(int page, int size) { - return repository.getAllCustomers(page, size); - } - - /** - * Retrieves a single customer by their ID. - */ - public LiveData> getCustomerById(Long id) { - return repository.getCustomerById(id); - } - - /** - * Retrieves a list of customer dropdowns from the repository. - */ - public LiveData>> getCustomerDropdowns() { - return repository.getCustomerDropdowns(); - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/EmployeeViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/EmployeeViewModel.java deleted file mode 100644 index 5454269e..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/EmployeeViewModel.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.EmployeeDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.repositories.EmployeeRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class EmployeeViewModel extends ViewModel { - private final EmployeeRepository employeeRepository; - - @Inject - public EmployeeViewModel(EmployeeRepository employeeRepository) { - this.employeeRepository = employeeRepository; - } - - public LiveData>> getAllEmployees(int page, int size) { - return employeeRepository.getAllEmployees(page, size); - } - - public LiveData> getEmployeeById(Long id) { - return employeeRepository.getEmployeeById(id); - } - - public LiveData> createEmployee(EmployeeDTO dto) { - return employeeRepository.createEmployee(dto); - } - - public LiveData> updateEmployee(Long id, EmployeeDTO dto) { - return employeeRepository.updateEmployee(id, dto); - } - - public LiveData> deleteEmployee(Long id) { - return employeeRepository.deleteEmployee(id); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java deleted file mode 100644 index c7ccc070..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.CategoryDTO; -import com.example.petstoremobile.dtos.InventoryDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.repositories.CategoryRepository; -import com.example.petstoremobile.repositories.InventoryRepository; -import com.example.petstoremobile.repositories.StoreRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class InventoryViewModel extends ViewModel { - private final InventoryRepository inventoryRepository; - private final CategoryRepository categoryRepository; - private final StoreRepository storeRepository; - - @Inject - public InventoryViewModel(InventoryRepository inventoryRepository, CategoryRepository categoryRepository, StoreRepository storeRepository) { - this.inventoryRepository = inventoryRepository; - this.categoryRepository = categoryRepository; - this.storeRepository = storeRepository; - } - - /** - * Retrieves a paginated list of inventory items, with optional filtering and sorting. - */ - public LiveData>> getAllInventory(String query, Long storeId, int page, int size, String sort) { - return inventoryRepository.getAllInventory(query, storeId, page, size, sort); - } - - /** - * Retrieves a single inventory item by its ID. - */ - public LiveData> getInventoryById(Long id) { - return inventoryRepository.getInventoryById(id); - } - - /** - * Creates a new inventory record. - */ - public LiveData> createInventory(InventoryDTO request) { - return inventoryRepository.createInventory(request); - } - - /** - * Updates an existing inventory record by ID. - */ - public LiveData> updateInventory(Long id, InventoryDTO request) { - return inventoryRepository.updateInventory(id, request); - } - - /** - * Deletes an inventory record by ID. - */ - public LiveData> deleteInventory(Long id) { - return inventoryRepository.deleteInventory(id); - } - - /** - * Deletes multiple inventory records in a single request. - */ - public LiveData> bulkDeleteInventory(List ids) { - return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids)); - } - - /** - * Retrieves a paginated list of categories. - */ - public LiveData>> getAllCategories(int page, int size) { - return categoryRepository.getAllCategories(page, size); - } - - /** - * Retrieves a paginated list of stores. - */ - public LiveData>> getAllStores(int page, int size) { - return storeRepository.getAllStores(page, size); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java deleted file mode 100644 index 76770392..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -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.repositories.PetRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; -import okhttp3.MultipartBody; - -@HiltViewModel -public class PetViewModel extends ViewModel { - private final PetRepository repository; - - @Inject - public PetViewModel(PetRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of pets with filters. - */ - public LiveData>> getAllPets(int page, int size, String query, String status, String species, Long storeId, Long customerId, String sort) { - return repository.getAllPets(page, size, query, status, species, storeId, customerId, sort); - } - - /** - * Retrieves a list of pets for a specific customer from the repository. - */ - public LiveData>> getCustomerPets(Long customerId) { - return repository.getCustomerPets(customerId); - } - - /** - * Retrieves a list of pets available for adoption from the repository. - */ - public LiveData>> getAdoptionPets() { - return repository.getAdoptionPets(); - } - - /** - * Retrieves a single pet by its ID. - */ - public LiveData> getPetById(Long id) { - return repository.getPetById(id); - } - - /** - * Creates a new pet record. - */ - public LiveData> createPet(PetDTO pet) { - return repository.createPet(pet); - } - - /** - * Updates an existing pet record by ID. - */ - public LiveData> updatePet(Long id, PetDTO pet) { - return repository.updatePet(id, pet); - } - - /** - * Deletes a pet record by ID. - */ - public LiveData> deletePet(Long id) { - return repository.deletePet(id); - } - - /** - * Deletes multiple pet records. - */ - public LiveData> bulkDeletePets(List ids) { - return repository.bulkDeletePets(new BulkDeleteRequest(ids)); - } - - /** - * Uploads an image for a specific pet. - */ - public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { - return repository.uploadPetImage(id, image); - } - - /** - * Deletes the image associated with a specific pet. - */ - public LiveData> deletePetImage(Long id) { - return repository.deletePetImage(id); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java deleted file mode 100644 index f4302225..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.ProductSupplierDTO; -import com.example.petstoremobile.repositories.ProductSupplierRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class ProductSupplierViewModel extends ViewModel { - private final ProductSupplierRepository repository; - - @Inject - public ProductSupplierViewModel(ProductSupplierRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all product-supplier relationships. - */ - public LiveData>> getAllProductSuppliers(int page, int size, String query, Long productId, Long supplierId, String sort) { - return repository.getAllProductSuppliers(page, size, query, productId, supplierId, sort); - } - - /** - * Creates a new product-supplier relationship. - */ - public LiveData> createProductSupplier(ProductSupplierDTO dto) { - return repository.createProductSupplier(dto); - } - - /** - * Updates an existing product-supplier relationship. - */ - public LiveData> updateProductSupplier(Long productId, Long supplierId, ProductSupplierDTO dto) { - return repository.updateProductSupplier(productId, supplierId, dto); - } - - /** - * Deletes a product-supplier relationship by product and supplier IDs. - */ - public LiveData> deleteProductSupplier(Long productId, Long supplierId) { - return repository.deleteProductSupplier(productId, supplierId); - } - - public LiveData> bulkDeleteProductSuppliers(List ids) { - return repository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java deleted file mode 100644 index b44c08eb..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.CategoryDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.ProductDTO; -import com.example.petstoremobile.repositories.CategoryRepository; -import com.example.petstoremobile.repositories.ProductRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; -import okhttp3.MultipartBody; - -@HiltViewModel -public class ProductViewModel extends ViewModel { - private final ProductRepository productRepository; - private final CategoryRepository categoryRepository; - - @Inject - public ProductViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) { - this.productRepository = productRepository; - this.categoryRepository = categoryRepository; - } - - /** - * Retrieves a paginated list of products, optionally filtered by a query string, category and sorted. - */ - public LiveData>> getAllProducts(String query, Long categoryId, int page, int size, String sort) { - return productRepository.getAllProducts(query, categoryId, page, size, sort); - } - - /** - * Retrieves a single product by its ID. - */ - public LiveData> getProductById(Long id) { - return productRepository.getProductById(id); - } - - /** - * Creates a new product. - */ - public LiveData> createProduct(ProductDTO product) { - return productRepository.createProduct(product); - } - - /** - * Updates an existing product by ID. - */ - public LiveData> updateProduct(Long id, ProductDTO product) { - return productRepository.updateProduct(id, product); - } - - /** - * Deletes a product by its ID. - */ - public LiveData> deleteProduct(Long id) { - return productRepository.deleteProduct(id); - } - - /** - * Uploads an image for a specific product. - */ - public LiveData> uploadProductImage(Long id, MultipartBody.Part image) { - return productRepository.uploadProductImage(id, image); - } - - /** - * Deletes the image associated with a specific product. - */ - public LiveData> deleteProductImage(Long id) { - return productRepository.deleteProductImage(id); - } - - /** - * Retrieves a paginated list of all product categories. - */ - public LiveData>> getAllCategories(int page, int size) { - return categoryRepository.getAllCategories(page, size); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java deleted file mode 100644 index d9a24e5e..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.PurchaseOrderDTO; -import com.example.petstoremobile.repositories.PurchaseOrderRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class PurchaseOrderViewModel extends ViewModel { - private final PurchaseOrderRepository repository; - - @Inject - public PurchaseOrderViewModel(PurchaseOrderRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all purchase orders. - */ - public LiveData>> getAllPurchaseOrders(int page, int size, String query, Long storeId, String sort) { - return repository.getAllPurchaseOrders(page, size, query, storeId, sort); - } - - /** - * Retrieves a single purchase order by its ID. - */ - public LiveData> getPurchaseOrderById(Long id) { - return repository.getPurchaseOrderById(id); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java deleted file mode 100644 index a02d3382..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.SaleDTO; -import com.example.petstoremobile.repositories.SaleRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class SaleViewModel extends ViewModel { - private final SaleRepository saleRepository; - - @Inject - public SaleViewModel(SaleRepository saleRepository) { - this.saleRepository = saleRepository; - } - - public LiveData>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, String sortBy) { - return saleRepository.getAllSales(page, size, query, paymentMethod, storeId, sortBy); - } - - public LiveData> getSaleById(Long id) { - return saleRepository.getSaleById(id); - } - - public LiveData> createSale(SaleDTO sale) { - return saleRepository.createSale(sale); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java deleted file mode 100644 index ebd5c3b6..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.ServiceDTO; -import com.example.petstoremobile.repositories.ServiceRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class ServiceViewModel extends ViewModel { - private final ServiceRepository repository; - - @Inject - public ServiceViewModel(ServiceRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all services. - */ - public LiveData>> getAllServices(int page, int size, String query, String sort) { - return repository.getAllServices(page, size, query, sort); - } - - /** - * Retrieves a single service by its ID. - */ - public LiveData> getServiceById(Long id) { - return repository.getServiceById(id); - } - - /** - * Creates a new service. - */ - public LiveData> createService(ServiceDTO service) { - return repository.createService(service); - } - - /** - * Updates an existing service by ID. - */ - public LiveData> updateService(Long id, ServiceDTO service) { - return repository.updateService(id, service); - } - - /** - * Deletes a service by ID. - */ - public LiveData> deleteService(Long id) { - return repository.deleteService(id); - } - - /** - * Deletes multiple services. - */ - public LiveData> bulkDeleteServices(List ids) { - return repository.bulkDeleteServices(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java deleted file mode 100644 index 0388c646..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.DropdownDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.repositories.StoreRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class StoreViewModel extends ViewModel { - private final StoreRepository repository; - - @Inject - public StoreViewModel(StoreRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all stores. - */ - public LiveData>> getAllStores(int page, int size) { - return repository.getAllStores(page, size); - } - - /** - * Fetches a list of store dropdowns from the repository. - */ - public LiveData>> getStoreDropdowns() { - return repository.getStoreDropdowns(); - } - - /** - * Fetches a list of employees for a specific store. - */ - public LiveData>> getStoreEmployees(Long storeId) { - return repository.getStoreEmployees(storeId); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java deleted file mode 100644 index 1486a562..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.SupplierDTO; -import com.example.petstoremobile.repositories.SupplierRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class SupplierViewModel extends ViewModel { - private final SupplierRepository repository; - - @Inject - public SupplierViewModel(SupplierRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all suppliers. - */ - public LiveData>> getAllSuppliers(int page, int size, String query, String sort) { - return repository.getAllSuppliers(page, size, query, sort); - } - - /** - * Retrieves a single supplier by its ID. - */ - public LiveData> getSupplierById(Long id) { - return repository.getSupplierById(id); - } - - /** - * Creates a new supplier record. - */ - public LiveData> createSupplier(SupplierDTO supplier) { - return repository.createSupplier(supplier); - } - - /** - * Updates an existing supplier record by ID. - */ - public LiveData> updateSupplier(Long id, SupplierDTO supplier) { - return repository.updateSupplier(id, supplier); - } - - /** - * Deletes a supplier record by ID. - */ - public LiveData> deleteSupplier(Long id) { - return repository.deleteSupplier(id); - } - - /** - * Deletes multiple supplier records. - */ - public LiveData> bulkDeleteSuppliers(List ids) { - return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java deleted file mode 100644 index d839f6c4..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.UserDTO; -import com.example.petstoremobile.repositories.UserRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class UserViewModel extends ViewModel { - private final UserRepository userRepository; - - @Inject - public UserViewModel(UserRepository userRepository) { - this.userRepository = userRepository; - } - - public LiveData>> getUsers(String role, int page, int size) { - return userRepository.getUsers(role, page, size); - } -} From 01be4a7620136c62617e53f7494c5fec3ff211cd Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:24:20 -0600 Subject: [PATCH 17/33] bug fix --- .../com/example/petstoremobile/dtos/ConversationDTO.java | 8 ++++++++ .../petstoremobile/viewmodels/AdoptionListViewModel.java | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java index 316aa467..3a7ea42e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java @@ -12,6 +12,14 @@ public class ConversationDTO { public ConversationDTO() {} + public ConversationDTO(Long id, Long customerId, Long staffId, String lastMessage, String status) { + this.id = id; + this.customerId = customerId; + this.staffId = staffId; + this.lastMessage = lastMessage; + this.status = status; + } + public Long getId() { return id; } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java index 683c79b4..6ebe7a99 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java @@ -53,7 +53,7 @@ public class AdoptionListViewModel extends ViewModel { if ("All Statuses".equals(status)) status = null; isLoading.setValue(true); - adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, "adoptionDate,desc").observeForever(resource -> { + adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, "adoptionDate,desc", null).observeForever(resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { List currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue()); From 83eda836712fb348f896c45f39a7ca201b128e2a Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:37:24 -0600 Subject: [PATCH 18/33] fixed bug again --- .../fragments/listfragments/AdoptionFragment.java | 2 +- .../petstoremobile/viewmodels/AdoptionListViewModel.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index 39317495..2d687c6e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -210,7 +210,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop if (status.equals("All Statuses")) status = null; else status = status.toUpperCase(); - viewModel.loadAdoptions(true, query, status, storeId); + viewModel.loadAdoptions(true, query, status, storeId, selectedDateString, null); } private void openDetail(int position) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java index 6ebe7a99..7c0b72b8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java @@ -42,7 +42,7 @@ public class AdoptionListViewModel extends ViewModel { public LiveData getIsLoading() { return isLoading; } public boolean isLastPage() { return isLastPage; } - public void loadAdoptions(boolean reset, String query, String status, Long storeId) { + public void loadAdoptions(boolean reset, String query, String status, Long storeId, String date, Long employeeId) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; if (reset) { @@ -53,7 +53,7 @@ public class AdoptionListViewModel extends ViewModel { if ("All Statuses".equals(status)) status = null; isLoading.setValue(true); - adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, "adoptionDate,desc", null).observeForever(resource -> { + adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, date, employeeId).observeForever(resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { List currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue()); From 6e21e4fd6c60ab40b7334bc3291c75ff7cf855b5 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Thu, 9 Apr 2026 15:51:21 -0600 Subject: [PATCH 19/33] Refine GUI Behavior --- .../controllers/AppointmentController.java | 15 ++++++++++++- .../controllers/SaleController.java | 19 ++++++++++++++++ .../AppointmentDialogController.java | 19 +++++++++++++--- .../petshopdesktop/util/TableViewSupport.java | 2 -- .../modelviews/adoption-view.fxml | 2 +- .../modelviews/analytics-view.fxml | 2 +- .../modelviews/appointment-view.fxml | 2 +- .../modelviews/inventory-view.fxml | 2 +- .../petshopdesktop/modelviews/pet-view.fxml | 2 +- .../modelviews/product-supplier-view.fxml | 2 +- .../modelviews/product-view.fxml | 2 +- .../modelviews/purchase-order-view.fxml | 2 +- .../petshopdesktop/modelviews/sale-view.fxml | 22 +++++++++---------- .../modelviews/service-view.fxml | 2 +- .../modelviews/staff-accounts-view.fxml | 2 +- .../modelviews/supplier-view.fxml | 2 +- 16 files changed, 71 insertions(+), 28 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index 3cc4a904..8027bfe7 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -263,7 +263,20 @@ public class AppointmentController { response.getEmployeeName() != null ? response.getEmployeeName() : "", response.getAppointmentDate() != null ? response.getAppointmentDate().toString() : "", response.getAppointmentTime() != null ? response.getAppointmentTime().toString() : "", - response.getAppointmentStatus() != null ? response.getAppointmentStatus() : "" + normalizeAppointmentStatus(response.getAppointmentStatus()) ); } + + private String normalizeAppointmentStatus(String status) { + if (status == null) { + return "Booked"; + } + return switch (status.trim().toLowerCase()) { + case "booked" -> "Booked"; + case "completed" -> "Completed"; + case "missed" -> "Missed"; + case "cancelled", "canceled" -> "Cancelled"; + default -> "Booked"; + }; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index 7e6573de..9bff8ac6 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -11,6 +11,7 @@ import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; +import javafx.scene.control.TableCell; import javafx.scene.control.Label; import javafx.scene.control.SelectionMode; import javafx.scene.control.Spinner; @@ -156,6 +157,8 @@ public class SaleController { colCartQty.setCellValueFactory(new PropertyValueFactory<>("quantity")); colCartUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); colCartTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + colCartUnitPrice.setCellFactory(column -> currencyCell()); + colCartTotal.setCellFactory(column -> currencyCell()); tvCart.setItems(cartItems); tvCart.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); @@ -169,6 +172,8 @@ public class SaleController { colSaleUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); colSaleTotal.setCellValueFactory(new PropertyValueFactory<>("total")); colSalePaymentType.setCellValueFactory(new PropertyValueFactory<>("paymentMethod")); + colSaleUnitPrice.setCellFactory(column -> currencyCell()); + colSaleTotal.setCellFactory(column -> currencyCell()); filteredSales = new FilteredList<>(saleItems, s -> true); TableViewSupport.bindSortedItems(tvSales, filteredSales); @@ -539,6 +544,20 @@ public class SaleController { lblCartTotal.setText(currency.format(total)); } + private TableCell currencyCell() { + return new TableCell<>() { + @Override + protected void updateItem(Double value, boolean empty) { + super.updateItem(value, empty); + if (empty || value == null) { + setText(null); + } else { + setText(currency.format(value)); + } + } + }; + } + private void setCreateSaleControlsDisabled(boolean disabled) { cbProduct.setDisable(disabled); spQuantity.setDisable(disabled); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java index 495813ac..1629187a 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -49,7 +49,7 @@ public class AppointmentDialogController { private ObservableList statusList = FXCollections.observableArrayList( - "Booked", "Completed", "Cancelled", "Missed" + "Booked", "Completed", "Missed", "Cancelled" ); public void setMode(String mode) { @@ -182,7 +182,7 @@ public class AppointmentDialogController { "Parsing appointment date"); } - cbAppointmentStatus.setValue(appt.getAppointmentStatus()); + cbAppointmentStatus.setValue(normalizeAppointmentStatus(appt.getAppointmentStatus())); try { LocalTime time = LocalTime.parse(appt.getAppointmentTime()); @@ -230,7 +230,7 @@ public class AppointmentDialogController { request.setEmployeeId(cbEmployee.getValue().getId()); request.setAppointmentDate(dpAppointmentDate.getValue()); request.setAppointmentTime(appointmentTime); - request.setAppointmentStatus(cbAppointmentStatus.getValue()); + request.setAppointmentStatus(normalizeAppointmentStatus(cbAppointmentStatus.getValue())); new Thread(() -> { try { @@ -451,4 +451,17 @@ public class AppointmentDialogController { } }).start(); } + + private String normalizeAppointmentStatus(String status) { + if (status == null) { + return "Booked"; + } + return switch (status.trim().toLowerCase()) { + case "booked" -> "Booked"; + case "completed" -> "Completed"; + case "missed" -> "Missed"; + case "cancelled", "canceled" -> "Cancelled"; + default -> "Booked"; + }; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java b/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java index bd18548e..80d19dde 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java +++ b/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java @@ -32,11 +32,9 @@ public final class TableViewSupport { } label.setText(message); label.setVisible(true); - label.setManaged(true); PauseTransition delay = new PauseTransition(Duration.seconds(1.5)); delay.setOnFinished(event -> { label.setVisible(false); - label.setManaged(false); }); delay.playFromStart(); } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml index 0c9c5e4a..0a7f00bd 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml @@ -73,7 +73,7 @@ - -