From 195c4605f064484cf84e0f4a90a0f2dbc5fef6a6 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:48:24 -0600 Subject: [PATCH] changed backend so can sortBy productName and added search to productSupplier --- .../api/ProductSupplierApi.java | 6 +- .../fragments/listfragments/PetFragment.java | 2 +- .../ProductSupplierFragment.java | 162 ++++++++++++++---- .../ProductSupplierRepository.java | 6 +- .../viewmodels/ProductSupplierViewModel.java | 4 +- .../res/layout/fragment_product_supplier.xml | 97 +++++++++-- .../controller/ProductSupplierController.java | 4 +- .../repository/ProductSupplierRepository.java | 12 +- .../service/ProductSupplierService.java | 43 ++++- 9 files changed, 270 insertions(+), 66 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java index 7ec9eb07..67a0e7f2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java @@ -10,7 +10,11 @@ public interface ProductSupplierApi { @GET("api/v1/product-suppliers") Call> getAllProductSuppliers( @Query("page") int page, - @Query("size") int size); + @Query("size") int size, + @Query("q") String query, + @Query("productId") Long productId, + @Query("supplierId") Long supplierId, + @Query("sort") String sort); @GET("api/v1/product-suppliers/{productId}/{supplierId}") Call getProductSupplierById( 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 bcb19fa8..f6b46b0e 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 @@ -144,7 +144,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen * Configures the status filter spinner. */ private void setupStatusFilter() { - String[] statuses = {"All Statuses", "Available", "Adopted"}; + String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"}; WhiteTextArrayAdapter adapter = new WhiteTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); binding.spinnerStatus.setAdapter(adapter); 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 4c8e08d0..1c0a9c19 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,10 +1,15 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.text.*; +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.AdapterView; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; @@ -15,11 +20,18 @@ import androidx.recyclerview.widget.LinearLayoutManager; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductSupplierAdapter; import com.example.petstoremobile.databinding.FragmentProductSupplierBinding; +import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductSupplierDTO; +import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.fragments.ListFragment; +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 java.util.*; +import java.util.ArrayList; +import java.util.List; import dagger.hilt.android.AndroidEntryPoint; @@ -29,17 +41,23 @@ public class ProductSupplierFragment extends Fragment private FragmentProductSupplierBinding binding; private List psList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private List productList = new ArrayList<>(); + private List supplierList = new ArrayList<>(); + private ProductSupplierAdapter adapter; private ProductSupplierViewModel viewModel; + private ProductViewModel productViewModel; + private SupplierViewModel supplierViewModel; /** - * Initializes the fragment and its associated ProductSupplierViewModel. + * 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); } /** @@ -52,8 +70,10 @@ public class ProductSupplierFragment extends Fragment setupRecyclerView(); setupSearch(); + setupProductFilter(); + setupSupplierFilter(); setupSwipeRefresh(); - loadData(); + setupFilterToggle(); binding.fabAddPS.setOnClickListener(v -> openDetail(-1)); @@ -76,11 +96,41 @@ public class ProductSupplierFragment extends Fragment 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() { + binding.btnToggleFilter.setOnClickListener(v -> { + if (binding.layoutFilter.getVisibility() == View.GONE) { + binding.layoutFilter.setVisibility(View.VISIBLE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); + } else { + binding.layoutFilter.setVisibility(View.GONE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); + + // Reset filters when closing + binding.etSearchPS.setText(""); + binding.spinnerProduct.setSelection(0); + binding.spinnerSupplier.setSelection(0); + } + }); + } + /** * Initializes the RecyclerView with a layout manager and adapter for product-supplier data. */ private void setupRecyclerView() { - adapter = new ProductSupplierAdapter(filteredList, this); + adapter = new ProductSupplierAdapter(psList, this); binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPS.setAdapter(adapter); } @@ -90,10 +140,57 @@ public class ProductSupplierFragment extends Fragment */ private void setupSearch() { binding.etSearchPS.addTextChangedListener(new TextWatcher() { - public void beforeTextChanged(CharSequence s, int a, int b, int c) {} - public void afterTextChanged(Editable s) {} - public void onTextChanged(CharSequence s, int a, int b, int c) { - filter(s.toString()); + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + loadData(); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + /** + * Configures the product filter spinner. + */ + private void setupProductFilter() { + binding.spinnerProduct.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadData(); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + /** + * Configures the supplier filter spinner. + */ + private void setupSupplierFilter() { + binding.spinnerSupplier.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadData(); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + /** + * 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); } }); } @@ -106,30 +203,23 @@ public class ProductSupplierFragment extends Fragment } /** - * Filters the product-supplier list based on the search query. - */ - private void filter(String query) { - filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(psList); - } else { - String lower = query.toLowerCase(); - for (ProductSupplierDTO ps : psList) { - if ((ps.getProductName() != null && ps.getProductName().toLowerCase().contains(lower)) - || (ps.getSupplierName() != null && ps.getSupplierName().toLowerCase().contains(lower))) { - filteredList.add(ps); - } - } - } - adapter.notifyDataSetChanged(); - } - - /** - * Fetches all product-supplier data from the server through the ViewModel. + * Fetches product-supplier data from the server through the ViewModel with search query and filters. */ private void loadData() { - //Load all product suppliers from the backend using viewModel - viewModel.getAllProductSuppliers(0, 100).observe(getViewLifecycleOwner(), resource -> { + 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(); + } + + Long supplierId = null; + if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && !supplierList.isEmpty()) { + supplierId = supplierList.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 @@ -144,7 +234,7 @@ public class ProductSupplierFragment extends Fragment if (resource.data != null) { psList.clear(); psList.addAll(resource.data.getContent()); - filter(binding.etSearchPS != null ? binding.etSearchPS.getText().toString() : ""); + adapter.notifyDataSetChanged(); } break; case ERROR: @@ -163,7 +253,7 @@ public class ProductSupplierFragment extends Fragment private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { - ProductSupplierDTO ps = filteredList.get(position); + ProductSupplierDTO ps = psList.get(position); args.putLong("productId", ps.getProductId()); args.putLong("supplierId", ps.getSupplierId()); } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java index 72182918..e5c135a5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java @@ -23,8 +23,8 @@ public class ProductSupplierRepository extends BaseRepository { /** * Retrieves a paginated list of all product-supplier relationships from the API. */ - public LiveData>> getAllProductSuppliers(int page, int size) { - return executeCall(api.getAllProductSuppliers(page, size)); + public LiveData>> getAllProductSuppliers(int page, int size, String query, Long productId, Long supplierId, String sort) { + return executeCall(api.getAllProductSuppliers(page, size, query, productId, supplierId, sort)); } /** @@ -54,4 +54,4 @@ public class ProductSupplierRepository extends BaseRepository { public LiveData> deleteProductSupplier(Long productId, Long supplierId) { return executeCall(api.deleteProductSupplier(productId, supplierId)); } -} +} \ No newline at end of file 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 index dbc55534..95613929 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java @@ -24,8 +24,8 @@ public class ProductSupplierViewModel extends ViewModel { /** * Fetches a paginated list of all product-supplier relationships. */ - public LiveData>> getAllProductSuppliers(int page, int size) { - return repository.getAllProductSuppliers(page, size); + public LiveData>> getAllProductSuppliers(int page, int size, String query, Long productId, Long supplierId, String sort) { + return repository.getAllProductSuppliers(page, size, query, productId, supplierId, sort); } /** diff --git a/android/app/src/main/res/layout/fragment_product_supplier.xml b/android/app/src/main/res/layout/fragment_product_supplier.xml index 6924c36d..86bb7530 100644 --- a/android/app/src/main/res/layout/fragment_product_supplier.xml +++ b/android/app/src/main/res/layout/fragment_product_supplier.xml @@ -28,27 +28,100 @@ android:contentDescription="Open menu"/> + 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"> + + + + + + + + + + + + + + + + + + + + > getAllProductSuppliers( @RequestParam(required = false) String q, + @RequestParam(required = false) Long productId, + @RequestParam(required = false) Long supplierId, Pageable pageable) { - return ResponseEntity.ok(productSupplierService.getAllProductSuppliers(q, pageable)); + return ResponseEntity.ok(productSupplierService.getAllProductSuppliers(q, productId, supplierId, pageable)); } @GetMapping("/{productId}/{supplierId}") diff --git a/backend/src/main/java/com/petshop/backend/repository/ProductSupplierRepository.java b/backend/src/main/java/com/petshop/backend/repository/ProductSupplierRepository.java index 46e87945..15d17703 100644 --- a/backend/src/main/java/com/petshop/backend/repository/ProductSupplierRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/ProductSupplierRepository.java @@ -12,7 +12,13 @@ import org.springframework.stereotype.Repository; public interface ProductSupplierRepository extends JpaRepository { @Query("SELECT ps FROM ProductSupplier ps WHERE " + - "LOWER(ps.product.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(ps.supplier.supCompany) LIKE LOWER(CONCAT('%', :q, '%'))") - Page searchProductSuppliers(@Param("q") String query, Pageable pageable); + "(:q IS NULL OR (LOWER(ps.product.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(ps.supplier.supCompany) LIKE LOWER(CONCAT('%', :q, '%')))) AND " + + "(:productId IS NULL OR ps.product.prodId = :productId) AND " + + "(:supplierId IS NULL OR ps.supplier.supId = :supplierId)") + Page searchProductSuppliers( + @Param("q") String query, + @Param("productId") Long productId, + @Param("supplierId") Long supplierId, + Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java b/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java index 7e3677a9..c9be85f1 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java @@ -11,10 +11,15 @@ import com.petshop.backend.repository.ProductRepository; import com.petshop.backend.repository.ProductSupplierRepository; import com.petshop.backend.repository.SupplierRepository; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; + @Service public class ProductSupplierService { @@ -28,13 +33,10 @@ public class ProductSupplierService { this.supplierRepository = supplierRepository; } - public Page getAllProductSuppliers(String query, Pageable pageable) { - Page productSuppliers; - if (query != null && !query.trim().isEmpty()) { - productSuppliers = productSupplierRepository.searchProductSuppliers(query, pageable); - } else { - productSuppliers = productSupplierRepository.findAll(pageable); - } + public Page getAllProductSuppliers(String query, Long productId, Long supplierId, Pageable pageable) { + String normalizedQuery = normalizeFilter(query); + Pageable mappedPageable = mapSortProperties(pageable); + Page productSuppliers = productSupplierRepository.searchProductSuppliers(normalizedQuery, productId, supplierId, mappedPageable); return productSuppliers.map(this::mapToResponse); } @@ -95,6 +97,33 @@ public class ProductSupplierService { }); } + private String normalizeFilter(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + private Pageable mapSortProperties(Pageable pageable) { + if (pageable.getSort().isUnsorted()) { + return pageable; + } + + List orders = new ArrayList<>(); + for (Sort.Order order : pageable.getSort()) { + String property = order.getProperty(); + if ("productName".equalsIgnoreCase(property)) { + orders.add(new Sort.Order(order.getDirection(), "product.prodName")); + } else if ("supplierName".equalsIgnoreCase(property)) { + orders.add(new Sort.Order(order.getDirection(), "supplier.supCompany")); + } else { + orders.add(order); + } + } + return PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(orders)); + } + private ProductSupplierResponse mapToResponse(ProductSupplier productSupplier) { return new ProductSupplierResponse( productSupplier.getProduct().getProdId(),