updated inventory backend to have filter by store and added more search features to andriod

This commit is contained in:
Alex
2026-04-07 05:09:48 -06:00
parent 863a85472f
commit ef5651d468
15 changed files with 342 additions and 227 deletions

View File

@@ -16,12 +16,14 @@ import retrofit2.http.Query;
public interface InventoryApi { public interface InventoryApi {
// GET /api/v1/inventory?q=...&page=...&size=... // GET /api/v1/inventory?q=...&page=...&size=...&category=...&storeId=...&sort=...
@GET("api/v1/inventory") @GET("api/v1/inventory")
Call<PageResponse<InventoryDTO>> getAllInventory( Call<PageResponse<InventoryDTO>> getAllInventory(
@Query("q") String query,
@Query("page") int page, @Query("page") int page,
@Query("size") int size, @Query("size") int size,
@Query("q") String query,
@Query("category") String category,
@Query("storeId") Long storeId,
@Query("sort") String sort); @Query("sort") String sort);
// GET /api/v1/inventory/{id} // GET /api/v1/inventory/{id}

View File

@@ -12,8 +12,10 @@ public interface ProductApi {
@GET("api/v1/products") @GET("api/v1/products")
Call<PageResponse<ProductDTO>> getAllProducts( Call<PageResponse<ProductDTO>> getAllProducts(
@Query("q") String query, @Query("q") String query,
@Query("categoryId") Long categoryId,
@Query("page") int page, @Query("page") int page,
@Query("size") int size); @Query("size") int size,
@Query("sort") String sort);
@GET("api/v1/products/{id}") @GET("api/v1/products/{id}")
Call<ProductDTO> getProductById(@Path("id") Long id); Call<ProductDTO> getProductById(@Path("id") Long id);

View File

@@ -1,8 +1,6 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
@@ -21,14 +19,14 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.adapters.InventoryAdapter; import com.example.petstoremobile.adapters.InventoryAdapter;
import com.example.petstoremobile.databinding.FragmentInventoryBinding; import com.example.petstoremobile.databinding.FragmentInventoryBinding;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.viewmodels.InventoryViewModel; import com.example.petstoremobile.viewmodels.InventoryViewModel;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -43,26 +41,15 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
private FragmentInventoryBinding binding; private FragmentInventoryBinding binding;
private final List<InventoryDTO> inventoryList = new ArrayList<>(); private final List<InventoryDTO> inventoryList = new ArrayList<>();
private final List<CategoryDTO> categoryList = new ArrayList<>(); private List<StoreDTO> storeList = new ArrayList<>();
private InventoryAdapter adapter; private InventoryAdapter adapter;
private InventoryViewModel viewModel; 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 // Pagination
private int currentPage = 0; private int currentPage = 0;
private boolean isLastPage = false; private boolean isLastPage = false;
private boolean isLoading = false; private boolean isLoading = false;
// Prevent spinner from firing on initial load
private boolean spinnerReady = false;
/** /**
* Initializes the fragment and its ViewModel. * 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 @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
@@ -82,9 +69,11 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
setupRecyclerView(); setupRecyclerView();
setupSearch(); setupSearch();
setupStoreFilter();
setupSwipeRefresh(); setupSwipeRefresh();
loadCategories(); // loads categories then triggers loadInventory setupFilterToggle();
loadInventory(true); loadInventory(true);
loadStoreData();
binding.fabAddInventory.setOnClickListener(v -> openDetail(null)); 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() { private void setupFilterToggle() {
viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { binding.btnToggleFilter.setOnClickListener(v -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { if (binding.layoutFilter.getVisibility() == View.GONE) {
categoryList.clear(); binding.layoutFilter.setVisibility(View.VISIBLE);
categoryList.addAll(resource.data.getContent()); binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
setupCategorySpinner();
} else if (resource != null && resource.status == Resource.Status.ERROR) {
Log.e(TAG, "Failed to load categories: " + resource.message);
setupCategorySpinner();
}
});
}
/**
* Setup the category filter spinner.
*/
private void setupCategorySpinner() {
// First item is always "All Categories"
List<String> categoryNames = new ArrayList<>();
categoryNames.add("All Categories");
for (CategoryDTO c : categoryList) {
categoryNames.add(c.getCategoryName());
}
if (getContext() != null) {
BlackTextArrayAdapter<String> 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 { } else {
selectedCategory = categoryList.get(position - 1).getCategoryName(); binding.layoutFilter.setVisibility(View.GONE);
} binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search);
loadInventory(true);
}
@Override // Reset filters when closing
public void onNothingSelected(AdapterView<?> parent) { binding.etSearchInventory.setText("");
binding.spinnerStore.setSelection(0);
} }
}); });
} }
}
/** /**
* Sets up the search bar for filtering. * Sets up the search bar for filtering.
*/ */
private void setupSearch() { private void setupSearch() {
binding.etSearchInventory.addTextChangedListener(new TextWatcher() { binding.etSearchInventory.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
} @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
@Override public void afterTextChanged(Editable s) {
}
@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); loadInventory(true);
}; }
searchHandler.postDelayed(searchRunnable, 400); @Override public void afterTextChanged(Editable s) {}
});
}
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() {
binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
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. * Fetches a page of inventory items from the API.
*/ */
private void loadInventory(boolean reset) { private void loadInventory(boolean reset) {
if (isLoading) if (isLoading) return;
return;
if (reset) { if (reset) {
currentPage = 0; currentPage = 0;
isLastPage = false; isLastPage = false;
} }
// Build query: combine search text + selected category // Search text from input
String q = buildQuery(); 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 //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; if (resource == null) return;
// Check the status to see if the resource is loaded and display the data // 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. * Displays a confirmation dialog before performing a bulk deletion of selected items.
*/ */

View File

@@ -1,10 +1,7 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle; import android.os.Bundle;
import android.text.*;
import android.util.Log;
import android.view.*;
import android.widget.*;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@@ -12,15 +9,27 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; 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.R;
import com.example.petstoremobile.adapters.ProductAdapter; import com.example.petstoremobile.adapters.ProductAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentProductBinding; import com.example.petstoremobile.databinding.FragmentProductBinding;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.fragments.ListFragment; 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 com.example.petstoremobile.viewmodels.ProductViewModel;
import java.util.*; import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@@ -32,12 +41,11 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
private FragmentProductBinding binding; private FragmentProductBinding binding;
private List<ProductDTO> productList = new ArrayList<>(); private List<ProductDTO> productList = new ArrayList<>();
private List<ProductDTO> filteredList = new ArrayList<>(); private List<CategoryDTO> categoryList = new ArrayList<>();
private ProductAdapter adapter; private ProductAdapter adapter;
private ProductViewModel viewModel; private ProductViewModel viewModel;
@Inject @Named("baseUrl") String baseUrl; @Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager;
/** /**
* Initializes the fragment and its associated ProductViewModel. * 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 @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
@@ -58,11 +66,11 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
setupRecyclerView(); setupRecyclerView();
setupSearch(); setupSearch();
setupCategoryFilter();
setupSwipeRefresh(); setupSwipeRefresh();
setupFilterToggle();
loadProducts(); binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1));
binding.fabAddProduct.setOnClickListener(v -> openDetail(-1));
binding.btnHamburgerProduct.setOnClickListener(v -> { binding.btnHamburgerProduct.setOnClickListener(v -> {
Fragment parent = getParentFragment(); 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() { @Override
adapter = new ProductAdapter(filteredList, this); public void onResume() {
adapter.setBaseUrl(baseUrl); super.onResume();
adapter.setToken(tokenManager.getToken()); loadProductData();
binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); loadCategoryData();
binding.recyclerViewProducts.setAdapter(adapter);
} }
/** /**
* Configures the search bar for filtering. * Sets up the filter toggle button to show/hide the filter layout.
*/ */
private void setupSearch() { private void setupFilterToggle() {
binding.etSearchProduct.addTextChangedListener(new TextWatcher() { binding.btnToggleFilter.setOnClickListener(v -> {
public void beforeTextChanged(CharSequence s, int a, int b, int c) {} if (binding.layoutFilter.getVisibility() == View.GONE) {
public void afterTextChanged(Editable s) {} binding.layoutFilter.setVisibility(View.VISIBLE);
public void onTextChanged(CharSequence s, int a, int b, int c) { binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
filter(); } 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() { 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() { private void openProductDetails(int position) {
String query = binding.etSearchProduct.getText().toString().toLowerCase(); Bundle args = new Bundle();
if (position != -1) {
filteredList.clear(); ProductDTO product = productList.get(position);
for (ProductDTO p : productList) { args.putLong("productId", product.getProdId());
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);
} }
NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args);
} }
adapter.notifyDataSetChanged();
@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() { private void loadProductData() {
viewModel.getAllProducts(null, 0, 100).observe(getViewLifecycleOwner(), resource -> { 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; if (resource == null) return;
switch (resource.status) { switch (resource.status) {
@@ -150,12 +207,14 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
if (resource.data != null) { if (resource.data != null) {
productList.clear(); productList.clear();
productList.addAll(resource.data.getContent()); productList.addAll(resource.data.getContent());
filter(); adapter.notifyDataSetChanged();
} }
break; break;
case ERROR: case ERROR:
binding.swipeRefreshProduct.setRefreshing(false); binding.swipeRefreshProduct.setRefreshing(false);
if (getContext() != null) {
Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show();
}
Log.e("ProductFragment", "Error loading products: " + resource.message); Log.e("ProductFragment", "Error loading products: " + resource.message);
break; 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) { private void setupRecyclerView() {
Bundle args = new Bundle(); adapter = new ProductAdapter(productList, this);
if (position != -1) { adapter.setBaseUrl(baseUrl);
ProductDTO p = filteredList.get(position); binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext()));
args.putLong("prodId", p.getProdId()); binding.recyclerViewProducts.setAdapter(adapter);
} }
NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args);
}
/**
* Handles item click in the product list.
*/
@Override
public void onProductClick(int position) { openDetail(position); }
} }

View File

@@ -154,7 +154,7 @@ public class InventoryDetailFragment extends Fragment {
*/ */
private void searchProducts(String query) { private void searchProducts(String query) {
if (getView() == null) return; 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) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productSuggestions.clear(); productSuggestions.clear();
productSuggestions.addAll(resource.data.getContent()); productSuggestions.addAll(resource.data.getContent());

View File

@@ -89,7 +89,7 @@ public class ProductSupplierDetailFragment extends Fragment {
* Loads the list of products from the API. * Loads the list of products from the API.
*/ */
private void loadProducts() { 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) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productList = resource.data.getContent(); productList = resource.data.getContent();
refreshProductSpinner(); refreshProductSpinner();

View File

@@ -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<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, int page, int size, String sort) { public LiveData<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, String category, Long storeId, int page, int size, String sort) {
return executeCall(inventoryApi.getAllInventory(query, page, size, sort)); return executeCall(inventoryApi.getAllInventory(page, size, query, category, storeId, sort));
} }
/** /**

View File

@@ -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<Resource<PageResponse<ProductDTO>>> getAllProducts(String query, int page, int size) { public LiveData<Resource<PageResponse<ProductDTO>>> getAllProducts(String query, Long categoryId, int page, int size, String sort) {
return executeCall(productApi.getAllProducts(query, page, size)); return executeCall(productApi.getAllProducts(query, categoryId, page, size, sort));
} }
/** /**

View File

@@ -8,8 +8,10 @@ import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.InventoryRequest;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.CategoryRepository; import com.example.petstoremobile.repositories.CategoryRepository;
import com.example.petstoremobile.repositories.InventoryRepository; import com.example.petstoremobile.repositories.InventoryRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import java.util.List; import java.util.List;
@@ -22,18 +24,20 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
public class InventoryViewModel extends ViewModel { public class InventoryViewModel extends ViewModel {
private final InventoryRepository inventoryRepository; private final InventoryRepository inventoryRepository;
private final CategoryRepository categoryRepository; private final CategoryRepository categoryRepository;
private final StoreRepository storeRepository;
@Inject @Inject
public InventoryViewModel(InventoryRepository inventoryRepository, CategoryRepository categoryRepository) { public InventoryViewModel(InventoryRepository inventoryRepository, CategoryRepository categoryRepository, StoreRepository storeRepository) {
this.inventoryRepository = inventoryRepository; this.inventoryRepository = inventoryRepository;
this.categoryRepository = categoryRepository; this.categoryRepository = categoryRepository;
this.storeRepository = storeRepository;
} }
/** /**
* Retrieves a paginated list of inventory items, with optional filtering and sorting. * Retrieves a paginated list of inventory items, with optional filtering and sorting.
*/ */
public LiveData<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, int page, int size, String sort) { public LiveData<Resource<PageResponse<InventoryDTO>>> getAllInventory(String query, String category, Long storeId, int page, int size, String sort) {
return inventoryRepository.getAllInventory(query, page, size, sort); return inventoryRepository.getAllInventory(query, category, storeId, page, size, sort);
} }
/** /**
@@ -77,4 +81,11 @@ public class InventoryViewModel extends ViewModel {
public LiveData<Resource<PageResponse<CategoryDTO>>> getAllCategories(int page, int size) { public LiveData<Resource<PageResponse<CategoryDTO>>> getAllCategories(int page, int size) {
return categoryRepository.getAllCategories(page, size); return categoryRepository.getAllCategories(page, size);
} }
/**
* Retrieves a paginated list of stores.
*/
public LiveData<Resource<PageResponse<StoreDTO>>> getAllStores(int page, int size) {
return storeRepository.getAllStores(page, size);
}
} }

View File

@@ -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<Resource<PageResponse<ProductDTO>>> getAllProducts(String query, int page, int size) { public LiveData<Resource<PageResponse<ProductDTO>>> getAllProducts(String query, Long categoryId, int page, int size, String sort) {
return productRepository.getAllProducts(query, page, size); return productRepository.getAllProducts(query, categoryId, page, size, sort);
} }
/** /**

View File

@@ -30,42 +30,78 @@
android:contentDescription="Open menu"/> android:contentDescription="Open menu"/>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Inventory" android:text="Inventory"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold"/> android:textStyle="bold"
android:layout_marginStart="8dp"/>
<ImageButton
android:id="@+id/btnToggleFilter"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_search"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="@color/white"
android:contentDescription="Toggle filter"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/layoutFilter"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="vertical"
android:padding="8dp" android:paddingStart="12dp"
android:gravity="center_vertical"> android:paddingEnd="12dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:visibility="gone"
android:background="@color/primary_dark"
android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@drawable/bg_search_bar"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@android:drawable/ic_menu_search"
android:alpha="0.6"/>
<EditText <EditText
android:id="@+id/etSearchInventory" android:id="@+id/etSearchInventory"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="48dp" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
android:hint="Search by product or category…" android:hint="Search by product…"
android:inputType="text" android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search" android:background="@android:color/transparent"
android:drawablePadding="8dp" android:textColor="@color/text_dark"
android:background="@android:color/white" android:textColorHint="#99000000"
android:padding="12dp" android:textSize="14sp"
android:textColor="@color/text_dark"/> android:paddingStart="8dp"
android:paddingEnd="8dp"/>
</LinearLayout>
<Spinner <Spinner
android:id="@+id/spinnerCategory" android:id="@+id/spinnerStore"
android:layout_width="140dp" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="44dp"
android:layout_marginStart="8dp" android:layout_marginTop="8dp"
android:background="@android:color/white" android:background="@drawable/bg_spinner"
android:padding="10dp"/> android:paddingStart="12dp"
android:paddingEnd="8dp"/>
</LinearLayout> </LinearLayout>
<!-- Bulk-delete action bar (hidden until long-press) --> <!-- Bulk-delete action bar (hidden until long-press) -->

View File

@@ -12,6 +12,7 @@
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:id="@+id/header"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="56dp"
android:background="@color/primary_dark" android:background="@color/primary_dark"
@@ -28,27 +29,78 @@
android:contentDescription="Open menu"/> android:contentDescription="Open menu"/>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Products" android:text="Products"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold"/> android:textStyle="bold"
android:layout_marginStart="8dp"/>
<ImageButton
android:id="@+id/btnToggleFilter"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_search"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="@color/white"
android:contentDescription="Toggle filter"/>
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/layoutFilter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@drawable/bg_search_bar"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@android:drawable/ic_menu_search"
android:alpha="0.6"/>
<EditText <EditText
android:id="@+id/etSearchProduct" android:id="@+id/etSearchProduct"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_margin="8dp" android:hint="Search products..."
android:hint="Search by name or category..."
android:inputType="text" android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search" android:background="@android:color/transparent"
android:drawablePadding="8dp" android:textColor="@color/text_dark"
android:background="@android:color/white" android:textColorHint="#99000000"
android:padding="12dp" android:textSize="14sp"
android:textColor="@color/text_dark"/> android:paddingStart="8dp"
android:paddingEnd="8dp"/>
</LinearLayout>
<Spinner
android:id="@+id/spinnerCategory"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_marginTop="8dp"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"/>
</LinearLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshProduct" android:id="@+id/swipeRefreshProduct"

View File

@@ -26,8 +26,9 @@ public class InventoryController {
@GetMapping @GetMapping
public ResponseEntity<Page<InventoryResponse>> getAllInventory( public ResponseEntity<Page<InventoryResponse>> getAllInventory(
@RequestParam(required = false) String q, @RequestParam(required = false) String q,
@RequestParam(required = false) Long storeId,
Pageable pageable) { Pageable pageable) {
return ResponseEntity.ok(inventoryService.getAllInventory(q, pageable)); return ResponseEntity.ok(inventoryService.getAllInventory(q, storeId, pageable));
} }
@GetMapping("/{id}") @GetMapping("/{id}")

View File

@@ -20,8 +20,10 @@ public interface InventoryRepository extends JpaRepository<Inventory, Long> {
Optional<Inventory> findByProductIdAndStoreId(@Param("productId") Long productId, @Param("storeId") Long storeId); Optional<Inventory> findByProductIdAndStoreId(@Param("productId") Long productId, @Param("storeId") Long storeId);
@Query("SELECT i FROM Inventory i LEFT JOIN i.store s WHERE " + @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.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(i.product.category.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(i.product.category.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(s.storeName) LIKE LOWER(CONCAT('%', :q, '%'))") "LOWER(s.storeName) LIKE LOWER(CONCAT('%', :q, '%')))) AND " +
Page<Inventory> searchInventory(@Param("q") String query, Pageable pageable); "(:storeId IS NULL OR i.store.storeId = :storeId)")
Page<Inventory> searchInventory(@Param("q") String query, @Param("storeId") Long storeId, Pageable pageable);
} }

View File

@@ -28,13 +28,9 @@ public class InventoryService {
this.storeRepository = storeRepository; this.storeRepository = storeRepository;
} }
public Page<InventoryResponse> getAllInventory(String query, Pageable pageable) { public Page<InventoryResponse> getAllInventory(String query, Long storeId, Pageable pageable) {
Page<Inventory> inventory; String normalizedQuery = normalizeFilter(query);
if (query != null && !query.trim().isEmpty()) { Page<Inventory> inventory = inventoryRepository.searchInventory(normalizedQuery, storeId, pageable);
inventory = inventoryRepository.searchInventory(query, pageable);
} else {
inventory = inventoryRepository.findAll(pageable);
}
return inventory.map(this::mapToResponse); return inventory.map(this::mapToResponse);
} }
@@ -97,6 +93,14 @@ public class InventoryService {
inventoryRepository.deleteAllById(request.getIds()); 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) { private InventoryResponse mapToResponse(Inventory inventory) {
StoreLocation store = inventory.getStore(); StoreLocation store = inventory.getStore();
return new InventoryResponse( return new InventoryResponse(