From 6c832e01f3d6b3ae96ed2dc4ce74d2797de2c9b4 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:09:48 -0600 Subject: [PATCH] updated inventory backend to have filter by store and added more search features to andriod --- .../petstoremobile/api/InventoryApi.java | 6 +- .../petstoremobile/api/ProductApi.java | 4 +- .../listfragments/InventoryFragment.java | 160 ++++++---------- .../listfragments/ProductFragment.java | 175 +++++++++++------- .../InventoryDetailFragment.java | 2 +- .../ProductSupplierDetailFragment.java | 2 +- .../repositories/InventoryRepository.java | 6 +- .../repositories/ProductRepository.java | 6 +- .../viewmodels/InventoryViewModel.java | 17 +- .../viewmodels/ProductViewModel.java | 6 +- .../main/res/layout/fragment_inventory.xml | 82 +++++--- .../src/main/res/layout/fragment_product.xml | 76 ++++++-- .../controller/InventoryController.java | 3 +- .../repository/InventoryRepository.java | 6 +- .../backend/service/InventoryService.java | 18 +- 15 files changed, 342 insertions(+), 227 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java index f54616ee..6c747e6e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java @@ -16,12 +16,14 @@ import retrofit2.http.Query; public interface InventoryApi { - // GET /api/v1/inventory?q=...&page=...&size=... + // GET /api/v1/inventory?q=...&page=...&size=...&category=...&storeId=...&sort=... @GET("api/v1/inventory") Call> getAllInventory( - @Query("q") String query, @Query("page") int page, @Query("size") int size, + @Query("q") String query, + @Query("category") String category, + @Query("storeId") Long storeId, @Query("sort") String sort); // GET /api/v1/inventory/{id} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java index dc02fd6c..1d46107b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java @@ -12,8 +12,10 @@ public interface ProductApi { @GET("api/v1/products") Call> getAllProducts( @Query("q") String query, + @Query("categoryId") Long categoryId, @Query("page") int page, - @Query("size") int size); + @Query("size") int size, + @Query("sort") String sort); @GET("api/v1/products/{id}") Call getProductById(@Path("id") Long id); 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 029dc446..1fe9d188 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,8 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; @@ -21,14 +19,14 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; -import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.InventoryAdapter; import com.example.petstoremobile.databinding.FragmentInventoryBinding; -import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.InventoryViewModel; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import java.util.ArrayList; import java.util.List; @@ -43,26 +41,15 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn private FragmentInventoryBinding binding; private final List inventoryList = new ArrayList<>(); - private final List categoryList = new ArrayList<>(); + private List storeList = new ArrayList<>(); private InventoryAdapter adapter; private InventoryViewModel viewModel; - // Debounce search - private final Handler searchHandler = new Handler(Looper.getMainLooper()); - private Runnable searchRunnable; - private String currentQuery = ""; - - // Selected category filter — null means "All" - private String selectedCategory = null; - // Pagination private int currentPage = 0; private boolean isLastPage = false; private boolean isLoading = false; - // Prevent spinner from firing on initial load - private boolean spinnerReady = false; - /** * Initializes the fragment and its ViewModel. */ @@ -73,7 +60,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } /** - * Sets up the fragment's UI components, including the inventory list, search, and category filter. + * Sets up the fragment's UI components, including the inventory list and search. */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -82,9 +69,11 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn setupRecyclerView(); setupSearch(); + setupStoreFilter(); setupSwipeRefresh(); - loadCategories(); // loads categories then triggers loadInventory + setupFilterToggle(); loadInventory(true); + loadStoreData(); binding.fabAddInventory.setOnClickListener(v -> openDetail(null)); @@ -110,83 +99,59 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } /** - * Fetches all product categories to populate the filter spinner. + * Sets up the filter toggle button to show/hide the filter layout. */ - private void loadCategories() { - viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - categoryList.clear(); - categoryList.addAll(resource.data.getContent()); - setupCategorySpinner(); - } else if (resource != null && resource.status == Resource.Status.ERROR) { - Log.e(TAG, "Failed to load categories: " + resource.message); - setupCategorySpinner(); + 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.etSearchInventory.setText(""); + binding.spinnerStore.setSelection(0); } }); } - /** - * Setup the category filter spinner. - */ - private void setupCategorySpinner() { - // First item is always "All Categories" - List categoryNames = new ArrayList<>(); - categoryNames.add("All Categories"); - for (CategoryDTO c : categoryList) { - categoryNames.add(c.getCategoryName()); - } - - if (getContext() != null) { - BlackTextArrayAdapter spinnerAdapter = new BlackTextArrayAdapter<>( - requireContext(), - android.R.layout.simple_spinner_item, - categoryNames); - spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - binding.spinnerCategory.setAdapter(spinnerAdapter); - - binding.spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (!spinnerReady) { - // Skip the first automatic trigger on setup - spinnerReady = true; - return; - } - if (position == 0) { - selectedCategory = null; // "All Categories" - } else { - selectedCategory = categoryList.get(position - 1).getCategoryName(); - } - loadInventory(true); - } - - @Override - public void onNothingSelected(AdapterView parent) { - } - }); - } - } - /** * Sets up the search bar for filtering. */ private void setupSearch() { binding.etSearchInventory.addTextChangedListener(new TextWatcher() { - @Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { - } - - @Override public void afterTextChanged(Editable s) { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + loadInventory(true); } + @Override public void afterTextChanged(Editable s) {} + }); + } + /** + * Configures the store filter spinner. + */ + private void setupStoreFilter() { + binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - if (searchRunnable != null) - searchHandler.removeCallbacks(searchRunnable); - searchRunnable = () -> { - currentQuery = s.toString().trim(); - loadInventory(true); - }; - searchHandler.postDelayed(searchRunnable, 400); + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadInventory(true); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + /** + * 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); } }); } @@ -228,19 +193,24 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn * Fetches a page of inventory items from the API. */ private void loadInventory(boolean reset) { - if (isLoading) - return; + if (isLoading) return; if (reset) { currentPage = 0; isLastPage = false; } - // Build query: combine search text + selected category - String q = buildQuery(); + // 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(); + } //Load all inventory items from the backend using viewModel - viewModel.getAllInventory(q, currentPage, PAGE_SIZE, "inventoryId,asc").observe(getViewLifecycleOwner(), resource -> { + viewModel.getAllInventory(query, null, 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 @@ -273,22 +243,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn }); } - /** - * Constructs a query string based on the current search text and selected category. - */ - private String buildQuery() { - String q = null; - if (!currentQuery.isEmpty() && selectedCategory != null) { - // Both active — prioritize search text, category acts as context - q = currentQuery; - } else if (!currentQuery.isEmpty()) { - q = currentQuery; - } else if (selectedCategory != null) { - q = selectedCategory; - } - return q; - } - /** * Displays a confirmation dialog before performing a bulk deletion of selected items. */ 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 6103918e..0eea959f 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 @@ -1,10 +1,7 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.text.*; -import android.util.Log; -import android.view.*; -import android.widget.*; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; @@ -12,15 +9,27 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Toast; + import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductAdapter; -import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentProductBinding; +import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.ProductViewModel; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; import javax.inject.Named; @@ -32,12 +41,11 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc private FragmentProductBinding binding; private List productList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private List categoryList = new ArrayList<>(); private ProductAdapter adapter; private ProductViewModel viewModel; @Inject @Named("baseUrl") String baseUrl; - @Inject TokenManager tokenManager; /** * Initializes the fragment and its associated ProductViewModel. @@ -49,7 +57,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc } /** - * Sets up the fragment's UI components, including the product list, search, and swipe-to-refresh. + * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -58,11 +66,11 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc setupRecyclerView(); setupSearch(); + setupCategoryFilter(); setupSwipeRefresh(); + setupFilterToggle(); - loadProducts(); - - binding.fabAddProduct.setOnClickListener(v -> openDetail(-1)); + binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1)); binding.btnHamburgerProduct.setOnClickListener(v -> { Fragment parent = getParentFragment(); @@ -84,61 +92,110 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc } /** - * Initializes the RecyclerView with a layout manager and adapter for displaying products. + * Reloads data every time the fragment becomes visible. */ - private void setupRecyclerView() { - adapter = new ProductAdapter(filteredList, this); - adapter.setBaseUrl(baseUrl); - adapter.setToken(tokenManager.getToken()); - binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); - binding.recyclerViewProducts.setAdapter(adapter); + @Override + public void onResume() { + super.onResume(); + loadProductData(); + loadCategoryData(); } /** - * Configures the search bar for filtering. + * Sets up the filter toggle button to show/hide the filter layout. */ - private void setupSearch() { - binding.etSearchProduct.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(); + 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.etSearchProduct.setText(""); + binding.spinnerCategory.setSelection(0); } }); } /** - * Sets up the SwipeRefreshLayout to allow manual re-fetching of product data. + * Configures the search bar for triggering data load from backend. + */ + private void setupSearch() { + binding.etSearchProduct.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) { + loadProductData(); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + /** + * Configures the category filter spinner. + */ + private void setupCategoryFilter() { + binding.spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadProductData(); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + /** + * 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::loadProducts); + binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData); } /** - * Filters the product list based on the search query across name, category, and description. + * Navigates to the product detail screen. */ - private void filter() { - String query = binding.etSearchProduct.getText().toString().toLowerCase(); - - filteredList.clear(); - for (ProductDTO p : productList) { - boolean matchesSearch = query.isEmpty() || - (p.getProdName() != null && p.getProdName().toLowerCase().contains(query)) || - (p.getCategoryName() != null && p.getCategoryName().toLowerCase().contains(query)) || - (p.getProdDesc() != null && p.getProdDesc().toLowerCase().contains(query)); - - if (matchesSearch) { - filteredList.add(p); - } + private void openProductDetails(int position) { + Bundle args = new Bundle(); + if (position != -1) { + ProductDTO product = productList.get(position); + args.putLong("productId", product.getProdId()); } - adapter.notifyDataSetChanged(); + NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args); + } + + @Override + public void onProductClick(int position) { + openProductDetails(position); } /** - * Fetches all product data from the server through the ViewModel and updates the UI. + * Fetches product data from the server with search query, category, and sorting. */ - private void loadProducts() { - viewModel.getAllProducts(null, 0, 100).observe(getViewLifecycleOwner(), resource -> { + 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) { @@ -150,12 +207,14 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc if (resource.data != null) { productList.clear(); productList.addAll(resource.data.getContent()); - filter(); + adapter.notifyDataSetChanged(); } break; case ERROR: binding.swipeRefreshProduct.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show(); + 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; } @@ -163,20 +222,12 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc } /** - * Navigates to the product detail screen for a specific product or to add a new one. + * Initializes the RecyclerView. */ - private void openDetail(int position) { - Bundle args = new Bundle(); - if (position != -1) { - ProductDTO p = filteredList.get(position); - args.putLong("prodId", p.getProdId()); - } - NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args); + private void setupRecyclerView() { + adapter = new ProductAdapter(productList, this); + adapter.setBaseUrl(baseUrl); + binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewProducts.setAdapter(adapter); } - - /** - * Handles item click in the product list. - */ - @Override - public void onProductClick(int position) { openDetail(position); } } 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 cd1e84f4..1a32df6e 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 @@ -154,7 +154,7 @@ public class InventoryDetailFragment extends Fragment { */ private void searchProducts(String query) { if (getView() == null) return; - productViewModel.getAllProducts(query, 0, 20).observe(getViewLifecycleOwner(), resource -> { + productViewModel.getAllProducts(query, null, 0, 20, "prodName").observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { productSuggestions.clear(); productSuggestions.addAll(resource.data.getContent()); 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 0674c458..aa570d13 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 @@ -89,7 +89,7 @@ public class ProductSupplierDetailFragment extends Fragment { * Loads the list of products from the API. */ private void loadProducts() { - productViewModel.getAllProducts(null, 0, 200).observe(getViewLifecycleOwner(), resource -> { + productViewModel.getAllProducts(null, null, 0, 200, "prodName").observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { productList = resource.data.getContent(); refreshProductSpinner(); diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java index 5513d0e3..05719c38 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java @@ -23,10 +23,10 @@ public class InventoryRepository extends BaseRepository { } /** - * Retrieves a paginated list of inventory items from the API with optional search and sort. + * Retrieves a paginated list of inventory items from the API with optional search, category, storeId and sort. */ - public LiveData>> getAllInventory(String query, int page, int size, String sort) { - return executeCall(inventoryApi.getAllInventory(query, page, size, sort)); + public LiveData>> getAllInventory(String query, String category, Long storeId, int page, int size, String sort) { + return executeCall(inventoryApi.getAllInventory(page, size, query, category, storeId, sort)); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java index 5ed95c8a..a6d32336 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java @@ -23,10 +23,10 @@ public class ProductRepository extends BaseRepository { } /** - * Retrieves a paginated list of products from the API, filtered by an optional query. + * Retrieves a paginated list of products from the API, filtered by an optional query, category and sorted. */ - public LiveData>> getAllProducts(String query, int page, int size) { - return executeCall(productApi.getAllProducts(query, page, size)); + public LiveData>> getAllProducts(String query, Long categoryId, int page, int size, String sort) { + return executeCall(productApi.getAllProducts(query, categoryId, page, size, sort)); } /** 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 index 3af31b5c..02a5f1bb 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java @@ -8,8 +8,10 @@ import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryRequest; 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; @@ -22,18 +24,20 @@ import dagger.hilt.android.lifecycle.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) { + 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, int page, int size, String sort) { - return inventoryRepository.getAllInventory(query, page, size, sort); + public LiveData>> getAllInventory(String query, String category, Long storeId, int page, int size, String sort) { + return inventoryRepository.getAllInventory(query, category, storeId, page, size, sort); } /** @@ -77,4 +81,11 @@ public class InventoryViewModel extends ViewModel { 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/ProductViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java index 6edcdd4b..b44c08eb 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java @@ -27,10 +27,10 @@ public class ProductViewModel extends ViewModel { } /** - * Retrieves a paginated list of products, optionally filtered by a query string. + * Retrieves a paginated list of products, optionally filtered by a query string, category and sorted. */ - public LiveData>> getAllProducts(String query, int page, int size) { - return productRepository.getAllProducts(query, page, size); + public LiveData>> getAllProducts(String query, Long categoryId, int page, int size, String sort) { + return productRepository.getAllProducts(query, categoryId, page, size, sort); } /** diff --git a/android/app/src/main/res/layout/fragment_inventory.xml b/android/app/src/main/res/layout/fragment_inventory.xml index 4b075953..8197078e 100644 --- a/android/app/src/main/res/layout/fragment_inventory.xml +++ b/android/app/src/main/res/layout/fragment_inventory.xml @@ -30,42 +30,78 @@ 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"> - + + + + + + + + android:id="@+id/spinnerStore" + android:layout_width="match_parent" + android:layout_height="44dp" + android:layout_marginTop="8dp" + android:background="@drawable/bg_spinner" + android:paddingStart="12dp" + android:paddingEnd="8dp"/> + diff --git a/android/app/src/main/res/layout/fragment_product.xml b/android/app/src/main/res/layout/fragment_product.xml index 21c9d9ec..8dba3f51 100644 --- a/android/app/src/main/res/layout/fragment_product.xml +++ b/android/app/src/main/res/layout/fragment_product.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"> + + + + + + + + + + + + > getAllInventory( @RequestParam(required = false) String q, + @RequestParam(required = false) Long storeId, Pageable pageable) { - return ResponseEntity.ok(inventoryService.getAllInventory(q, pageable)); + return ResponseEntity.ok(inventoryService.getAllInventory(q, storeId, pageable)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java b/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java index b448b497..7dd535eb 100644 --- a/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java @@ -20,8 +20,10 @@ public interface InventoryRepository extends JpaRepository { Optional findByProductIdAndStoreId(@Param("productId") Long productId, @Param("storeId") Long storeId); @Query("SELECT i FROM Inventory i LEFT JOIN i.store s WHERE " + + "(:q IS NULL OR (" + "LOWER(i.product.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(i.product.category.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(s.storeName) LIKE LOWER(CONCAT('%', :q, '%'))") - Page searchInventory(@Param("q") String query, Pageable pageable); + "LOWER(s.storeName) LIKE LOWER(CONCAT('%', :q, '%')))) AND " + + "(:storeId IS NULL OR i.store.storeId = :storeId)") + Page searchInventory(@Param("q") String query, @Param("storeId") Long storeId, Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/service/InventoryService.java b/backend/src/main/java/com/petshop/backend/service/InventoryService.java index 499e8dd5..884458f9 100644 --- a/backend/src/main/java/com/petshop/backend/service/InventoryService.java +++ b/backend/src/main/java/com/petshop/backend/service/InventoryService.java @@ -28,13 +28,9 @@ public class InventoryService { this.storeRepository = storeRepository; } - public Page getAllInventory(String query, Pageable pageable) { - Page inventory; - if (query != null && !query.trim().isEmpty()) { - inventory = inventoryRepository.searchInventory(query, pageable); - } else { - inventory = inventoryRepository.findAll(pageable); - } + public Page getAllInventory(String query, Long storeId, Pageable pageable) { + String normalizedQuery = normalizeFilter(query); + Page inventory = inventoryRepository.searchInventory(normalizedQuery, storeId, pageable); return inventory.map(this::mapToResponse); } @@ -97,6 +93,14 @@ public class InventoryService { inventoryRepository.deleteAllById(request.getIds()); } + private String normalizeFilter(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + private InventoryResponse mapToResponse(Inventory inventory) { StoreLocation store = inventory.getStore(); return new InventoryResponse(