From 155c64a729e3e37848db404aa485120acf3ffb1e Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:06:07 -0600 Subject: [PATCH] added adoption search and filter andriod and backend --- .../adapters/AdoptionAdapter.java | 2 +- .../petstoremobile/api/AdoptionApi.java | 7 +- .../listfragments/AdoptionFragment.java | 150 +++++++++++++----- .../repositories/AdoptionRepository.java | 4 +- .../petstoremobile/utils/SpinnerUtils.java | 26 +++ .../viewmodels/AdoptionViewModel.java | 6 +- .../src/main/res/layout/fragment_adoption.xml | 114 ++++++++++--- .../controller/AdoptionController.java | 3 +- .../repository/AdoptionRepository.java | 4 +- .../backend/service/AdoptionService.java | 3 +- 10 files changed, 244 insertions(+), 75 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java index 8a8c3979..5be70654 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java @@ -86,7 +86,7 @@ public class AdoptionAdapter extends RecyclerView.Adapter> getAllAdoptions( @Query("page") int page, - @Query("size") int size); + @Query("size") int size, + @Query("q") String query, + @Query("status") String status, + @Query("storeId") Long storeId, + @Query("date") String date, + @Query("employeeId") Long employeeId); @GET("api/v1/adoptions/{id}") Call getAdoptionById(@Path("id") Long id); 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 74c54c50..f4757314 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 @@ -2,9 +2,14 @@ package com.example.petstoremobile.fragments.listfragments; import android.graphics.Color; import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; 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.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; @@ -16,16 +21,25 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AdoptionAdapter; import com.example.petstoremobile.databinding.FragmentAdoptionBinding; import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.BulkDeleteHandler; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.AdoptionViewModel; import com.example.petstoremobile.utils.EventDecorator; -import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.viewmodels.StoreViewModel; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; + import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; import dagger.hilt.android.AndroidEntryPoint; @@ -34,21 +48,23 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop private FragmentAdoptionBinding binding; private List adoptionList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private List storeList = new ArrayList<>(); private AdoptionAdapter adapter; - private AdoptionViewModel viewModel; + private AdoptionViewModel adoptionViewModel; + private StoreViewModel storeViewModel; 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 ViewModel. + * Initializes the fragment and its ViewModels. */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); + adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); + storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); } /** @@ -61,10 +77,12 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop setupRecyclerView(); setupSearch(); + setupStatusFilter(); + setupStoreFilter(); setupSwipeRefresh(); setupCalendar(); + setupFilterToggle(); setupBulkDelete(); - loadAdoptions(); binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1)); @@ -91,7 +109,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop binding.btnBulkDelete, adapter, "adoption", - viewModel::bulkDeleteAdoptions, + adoptionViewModel::bulkDeleteAdoptions, this::loadAdoptions ); } @@ -102,6 +120,13 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop binding = null; } + @Override + public void onResume() { + super.onResume(); + loadAdoptions(); + loadStoreData(); + } + /** * Toggles the calendar display between week and month modes. */ @@ -112,6 +137,28 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop .commit(); } + /** + * Sets up the filter toggle button to show/hide the filter layout. + */ + private void setupFilterToggle() { + binding.btnToggleFilterAdoption.setOnClickListener(v -> { + if (binding.layoutFilterAdoption.getVisibility() == View.GONE) { + binding.layoutFilterAdoption.setVisibility(View.VISIBLE); + binding.btnToggleFilterAdoption.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); + } else { + binding.layoutFilterAdoption.setVisibility(View.GONE); + binding.btnToggleFilterAdoption.setImageResource(android.R.drawable.ic_menu_search); + + // Reset filters when closing + binding.etSearchAdoption.setText(""); + binding.spinnerStatusAdoption.setSelection(0); + binding.spinnerStoreAdoption.setSelection(0); + selectedCalendarDay = null; + binding.calendarViewAdoption.clearSelection(); + } + }); + } + /** * Sets up the date selection listener for the calendar. */ @@ -127,7 +174,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop } else { selectedCalendarDay = null; } - filter(binding.etSearchAdoption.getText().toString()); + loadAdoptions(); }); } @@ -158,7 +205,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop * Initializes the RecyclerView for displaying adoptions. */ private void setupRecyclerView() { - adapter = new AdoptionAdapter(filteredList, this); + adapter = new AdoptionAdapter(adoptionList, this); binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAdoptions.setAdapter(adapter); } @@ -167,11 +214,39 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop * Sets up the search bar for filtering */ private void setupSearch() { - binding.etSearchAdoption.addTextChangedListener(new android.text.TextWatcher() { - public void beforeTextChanged(CharSequence s, int a, int b, int c) {} - public void afterTextChanged(android.text.Editable s) {} - public void onTextChanged(CharSequence s, int a, int b, int c) { - filter(s.toString()); + binding.etSearchAdoption.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + loadAdoptions(); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + /** + * 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); } }); } @@ -184,11 +259,16 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop } /** - * Filters the adoption list based on search query and selected calendar date. + * Fetches the adoption list from the server through the ViewModel. */ - private void filter(String query) { - filteredList.clear(); - String lowerQuery = query.toLowerCase(); + 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(); + } String selectedDateString = null; if (selectedCalendarDay != null) { @@ -196,28 +276,10 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay()); } - for (AdoptionDTO a : adoptionList) { - boolean matchesSearch = query.isEmpty() || - (a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lowerQuery)) || - (a.getPetName() != null && a.getPetName().toLowerCase().contains(lowerQuery)) || - (a.getAdoptionStatus() != null && a.getAdoptionStatus().toLowerCase().contains(lowerQuery)); + if (status.equals("All Statuses")) status = null; + else status = status.toUpperCase(); - boolean matchesDate = (selectedDateString == null) || - (a.getAdoptionDate() != null && a.getAdoptionDate().startsWith(selectedDateString)); - - if (matchesSearch && matchesDate) { - filteredList.add(a); - } - } - adapter.notifyDataSetChanged(); - } - - /** - * Fetches the adoption list from the server through the ViewModel. - */ - private void loadAdoptions() { - //Load all adoptions from the backend using viewModel - viewModel.getAllAdoptions(0, 500).observe(getViewLifecycleOwner(), resource -> { + 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 @@ -233,7 +295,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop adoptionList.clear(); adoptionList.addAll(resource.data.getContent()); updateCalendarDecorators(); - filter(binding.etSearchAdoption != null ? binding.etSearchAdoption.getText().toString() : ""); + adapter.notifyDataSetChanged(); } break; case ERROR: @@ -253,7 +315,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop Bundle args = new Bundle(); if (position != -1) { - AdoptionDTO a = filteredList.get(position); + AdoptionDTO a = adoptionList.get(position); args.putLong("adoptionId", a.getAdoptionId()); } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java index 53d34c9f..f09f85b4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java @@ -24,8 +24,8 @@ public class AdoptionRepository extends BaseRepository { /** * Retrieves a paginated list of all adoptions from the API. */ - public LiveData>> getAllAdoptions(int page, int size) { - return executeCall(adoptionApi.getAllAdoptions(page, size)); + public LiveData>> getAllAdoptions(int page, int size, String query, String status, Long storeId, String date, Long employeeId) { + return executeCall(adoptionApi.getAllAdoptions(page, size, query, status, storeId, date, employeeId)); } /** 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 0041a0b2..b0aae8b8 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 @@ -1,6 +1,8 @@ package com.example.petstoremobile.utils; import android.content.Context; +import android.view.View; +import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Spinner; @@ -70,6 +72,30 @@ public class SpinnerUtils { } } + /** + * 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); + setupFilterSpinner(spinner, onSelectionChanged); + } + + /** + * Attaches an item selected listener to a spinner that triggers a callback. + */ + public static void setupFilterSpinner(Spinner spinner, Runnable onSelectionChanged) { + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + onSelectionChanged.run(); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + /** * Sets the selection of a spinner based on a string value. */ 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 index 0287c85c..12eb9779 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java @@ -25,10 +25,10 @@ public class AdoptionViewModel extends ViewModel { } /** - * Fetches a paginated list of all adoptions. + * Fetches a paginated list of all adoptions with filters. */ - public LiveData>> getAllAdoptions(int page, int size) { - return repository.getAllAdoptions(page, size); + 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); } /** diff --git a/android/app/src/main/res/layout/fragment_adoption.xml b/android/app/src/main/res/layout/fragment_adoption.xml index 5004f137..f222f6e7 100644 --- a/android/app/src/main/res/layout/fragment_adoption.xml +++ b/android/app/src/main/res/layout/fragment_adoption.xml @@ -12,6 +12,7 @@ android:orientation="vertical"> + android:textStyle="bold" + android:layout_marginStart="8dp"/> + + - + android:orientation="vertical" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:visibility="gone" + android:background="@color/primary_dark" + android:elevation="4dp"> - + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java index 0e833dd5..a8795b87 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -35,6 +35,7 @@ public class AdoptionController { @RequestParam(required = false) String q, @RequestParam(required = false) Long customerId, @RequestParam(required = false) String status, + @RequestParam(required = false) Long storeId, Pageable pageable) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String role = authentication.getAuthorities().stream() @@ -48,7 +49,7 @@ public class AdoptionController { effectiveCustomerId = user.getId(); } - return ResponseEntity.ok(adoptionService.getAllAdoptions(q, effectiveCustomerId, status, pageable)); + return ResponseEntity.ok(adoptionService.getAllAdoptions(q, effectiveCustomerId, status, storeId, pageable)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java index 42f1d578..2c93c7c7 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java @@ -20,11 +20,13 @@ public interface AdoptionRepository extends JpaRepository { "LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%'))" + ")) AND " + "(:customerId IS NULL OR a.customer.id = :customerId) AND " + - "(:status IS NULL OR LOWER(a.adoptionStatus) = LOWER(:status))") + "(:status IS NULL OR LOWER(a.adoptionStatus) = LOWER(:status)) AND " + + "(:storeId IS NULL OR a.sourceStore.storeId = :storeId)") Page searchAdoptions( @Param("q") String query, @Param("customerId") Long customerId, @Param("status") String status, + @Param("storeId") Long storeId, Pageable pageable); Optional findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(Long petId, String adoptionStatus); diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 5f76947d..08121355 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -38,7 +38,7 @@ public class AdoptionService { this.storeRepository = storeRepository; } - public Page getAllAdoptions(String query, Long customerId, String status, Pageable pageable) { + public Page getAllAdoptions(String query, Long customerId, String status, Long storeId, Pageable pageable) { String normalizedQuery = normalizeFilter(query); String normalizedStatus = normalizeFilter(status); @@ -46,6 +46,7 @@ public class AdoptionService { normalizedQuery, customerId, normalizedStatus, + storeId, pageable );