Did the same to inventory

This commit is contained in:
Alex
2026-04-07 14:55:43 -06:00
parent 679c451c04
commit 0813bb4b44
9 changed files with 113 additions and 188 deletions

View File

@@ -53,20 +53,15 @@ public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.Inve
InventoryDTO inv = inventoryList.get(position);
ItemInventoryBinding binding = holder.binding;
// Column: Inventory ID
String invIdStr = inv.getInventoryId() != null ? String.valueOf(inv.getInventoryId()) : "";
binding.tvInventoryId.setText("Inv ID: " + invIdStr);
// Column: Product ID
String prodIdStr = inv.getProdId() != null ? String.valueOf(inv.getProdId()) : "";
binding.tvProdId.setText("Prod ID: " + prodIdStr);
// Column: Product Name
binding.tvProductName.setText(inv.getProductName() != null ? inv.getProductName() : "");
// Column: Store Name
binding.tvInventoryStore.setText("Store: " + (inv.getStoreName() != null ? inv.getStoreName() : ""));
// Column: Quantity
int qty = inv.getQuantity() != null ? inv.getQuantity() : 0;
binding.tvQuantity.setText(String.valueOf(qty));
binding.tvQuantity.setText("Stock: " + qty);
// Low stock = red, normal = green (like desktop reorder concept)
if (qty <= 5) {

View File

@@ -2,13 +2,13 @@ package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.InventoryRequest;
import com.example.petstoremobile.dtos.PageResponse;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.HTTP;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
@@ -32,17 +32,17 @@ public interface InventoryApi {
// POST /api/v1/inventory
@POST("api/v1/inventory")
Call<InventoryDTO> createInventory(@Body InventoryRequest request);
Call<InventoryDTO> createInventory(@Body InventoryDTO request);
// PUT /api/v1/inventory/{id}
@PUT("api/v1/inventory/{id}")
Call<InventoryDTO> updateInventory(@Path("id") Long id, @Body InventoryRequest request);
Call<InventoryDTO> updateInventory(@Path("id") Long id, @Body InventoryDTO request);
// DELETE /api/v1/inventory/{id}
@DELETE("api/v1/inventory/{id}")
Call<Void> deleteInventory(@Path("id") Long id);
// DELETE /api/v1/inventory (bulk delete)
@DELETE("api/v1/inventory")
@HTTP(method = "DELETE", path = "api/v1/inventory", hasBody = true)
Call<Void> bulkDeleteInventory(@Body BulkDeleteRequest request);
}

View File

@@ -6,6 +6,8 @@ public class InventoryDTO {
private Long prodId;
private String productName;
private String categoryName;
private Long storeId;
private String storeName;
private Integer quantity;
private String createdAt;
private String updatedAt;
@@ -14,8 +16,9 @@ public class InventoryDTO {
}
// Constructor for create/update requests (matches InventoryRequest)
public InventoryDTO(Long prodId, Integer quantity) {
public InventoryDTO(Long prodId, Long storeId, Integer quantity) {
this.prodId = prodId;
this.storeId = storeId;
this.quantity = quantity;
}
@@ -51,6 +54,22 @@ public class InventoryDTO {
this.categoryName = categoryName;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
public String getStoreName() {
return storeName;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
public Integer getQuantity() {
return quantity;
}

View File

@@ -1,31 +0,0 @@
package com.example.petstoremobile.dtos;
public class InventoryRequest {
private Long prodId;
private Integer quantity;
public InventoryRequest() {
}
public InventoryRequest(Long prodId, Integer quantity) {
this.prodId = prodId;
this.quantity = quantity;
}
public Long getProdId() {
return prodId;
}
public void setProdId(Long prodId) {
this.prodId = prodId;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
}

View File

@@ -1,14 +1,9 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -18,15 +13,16 @@ import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.InventoryRequest;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.InventoryViewModel;
import com.example.petstoremobile.viewmodels.ProductViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -43,20 +39,15 @@ public class InventoryDetailFragment extends Fragment {
private InventoryViewModel inventoryViewModel;
private ProductViewModel productViewModel;
private StoreViewModel storeViewModel;
private boolean isEditing = false;
private long inventoryId = -1;
private long preselectedStoreId = -1;
private long preselectedProductId = -1;
// The product selected from the dropdown
private ProductDTO selectedProduct = null;
// For debouncing product search
private final Handler searchHandler = new Handler(Looper.getMainLooper());
private Runnable searchRunnable;
// Dropdown list
private final List<ProductDTO> productSuggestions = new ArrayList<>();
private ArrayAdapter<String> dropdownAdapter;
private List<StoreDTO> storeList = new ArrayList<>();
private List<ProductDTO> productList = new ArrayList<>();
/**
* Initializes the view models.
@@ -66,6 +57,7 @@ public class InventoryDetailFragment extends Fragment {
super.onCreate(savedInstanceState);
inventoryViewModel = new ViewModelProvider(this).get(InventoryViewModel.class);
productViewModel = new ViewModelProvider(this).get(ProductViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
}
/**
@@ -85,94 +77,64 @@ public class InventoryDetailFragment extends Fragment {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupProductSearch();
loadSpinnersData();
handleArguments();
binding.btnInventoryBack.setOnClickListener(v -> navigateBack());
binding.btnSaveInventory.setOnClickListener(v -> saveInventory());
binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete());
// Setup dropdown adapter
dropdownAdapter = new BlackTextArrayAdapter<>(requireContext(),
android.R.layout.simple_dropdown_item_1line, new ArrayList<>());
binding.etProductSearch.setAdapter(dropdownAdapter);
binding.etProductSearch.setThreshold(1); // start showing after 1 character
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (searchRunnable != null) {
searchHandler.removeCallbacks(searchRunnable);
}
binding = null;
}
/**
* Sets up the product search dropdown.
* Fetches required data for spinners from the backend.
*/
private void setupProductSearch() {
binding.etProductSearch.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) {
}
@Override public void afterTextChanged(Editable s) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// Clear selected product when user is typing again
selectedProduct = null;
binding.tvProductInfo.setVisibility(View.GONE);
if (searchRunnable != null)
searchHandler.removeCallbacks(searchRunnable);
String query = s.toString().trim();
if (query.isEmpty())
return;
searchRunnable = () -> searchProducts(query);
searchHandler.postDelayed(searchRunnable, 400);
}
});
// When user picks an item from the dropdown
binding.etProductSearch.setOnItemClickListener((parent, view, position, id) -> {
if (position < productSuggestions.size()) {
selectedProduct = productSuggestions.get(position);
// Show product details below the search box
binding.tvProductInfo.setText(
"ID: " + selectedProduct.getProdId()
+ "" + selectedProduct.getCategoryName());
binding.tvProductInfo.setVisibility(View.VISIBLE);
}
});
private void loadSpinnersData() {
loadStores();
loadProducts();
}
/**
* Searches for products matching the query from the backend.
* Loads the list of stores for the spinner.
*/
private void searchProducts(String query) {
if (getView() == null) return;
productViewModel.getAllProducts(query, null, 0, 20, "prodName").observe(getViewLifecycleOwner(), resource -> {
private void loadStores() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productSuggestions.clear();
productSuggestions.addAll(resource.data.getContent());
// Build display strings: "Product Name (ID: X)"
List<String> names = new ArrayList<>();
for (ProductDTO p : productSuggestions) {
names.add(p.getProdName() + " (ID: " + p.getProdId() + ")");
}
dropdownAdapter.clear();
dropdownAdapter.addAll(names);
dropdownAdapter.notifyDataSetChanged();
binding.etProductSearch.showDropDown();
storeList = resource.data.getContent();
refreshStoreSpinner();
}
});
}
private void refreshStoreSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, storeList,
StoreDTO::getStoreName, "-- Select Store --",
preselectedStoreId, StoreDTO::getStoreId);
}
/**
* Loads the list of products for the spinner.
*/
private void loadProducts() {
productViewModel.getAllProducts(null, null, 0, 500, "prodName").observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productList = resource.data.getContent();
refreshProductSpinner();
}
});
}
private void refreshProductSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, productList,
ProductDTO::getProdName, "-- Select Product --",
preselectedProductId, ProductDTO::getProdId);
}
/**
* Handles fragment arguments to determine if we are in edit or add mode.
*/
@@ -193,7 +155,6 @@ public class InventoryDetailFragment extends Fragment {
isEditing = false;
binding.tvInventoryMode.setText("Add Inventory");
binding.tvInventoryId.setVisibility(View.GONE);
binding.tvProductInfo.setVisibility(View.GONE);
binding.btnDeleteInventory.setVisibility(View.GONE);
binding.btnSaveInventory.setText("Add");
}
@@ -207,20 +168,12 @@ public class InventoryDetailFragment extends Fragment {
if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
InventoryDTO inv = resource.data;
binding.etProductSearch.setText(inv.getProductName());
binding.etQuantity.setText(String.valueOf(inv.getQuantity()));
preselectedStoreId = inv.getStoreId() != null ? inv.getStoreId() : -1;
preselectedProductId = inv.getProdId() != null ? inv.getProdId() : -1;
if (inv.getProdId() != null) {
binding.tvProductInfo.setText(
"ID: " + inv.getProdId()
+ "" + inv.getCategoryName());
binding.tvProductInfo.setVisibility(View.VISIBLE);
selectedProduct = new ProductDTO();
selectedProduct.setProdId(inv.getProdId());
selectedProduct.setProdName(inv.getProductName());
selectedProduct.setCategoryName(inv.getCategoryName());
}
refreshStoreSpinner();
refreshProductSpinner();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show();
}
@@ -231,9 +184,12 @@ public class InventoryDetailFragment extends Fragment {
* Validates input and saves the current inventory item details to the backend.
*/
private void saveInventory() {
if (selectedProduct == null) {
binding.etProductSearch.setError("Please select a product from the list");
binding.etProductSearch.requestFocus();
if (binding.spinnerInventoryStore.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Please select a store", Toast.LENGTH_SHORT).show();
return;
}
if (binding.spinnerInventoryProduct.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Please select a product", Toast.LENGTH_SHORT).show();
return;
}
@@ -243,8 +199,10 @@ public class InventoryDetailFragment extends Fragment {
}
int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim());
StoreDTO store = storeList.get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1);
ProductDTO product = productList.get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1);
InventoryRequest request = new InventoryRequest(selectedProduct.getProdId(), quantity);
InventoryDTO request = new InventoryDTO(product.getProdId(), store.getStoreId(), quantity);
setButtonsEnabled(false);
if (isEditing) {

View File

@@ -5,7 +5,6 @@ import androidx.lifecycle.LiveData;
import com.example.petstoremobile.api.InventoryApi;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.InventoryRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.utils.Resource;
@@ -39,11 +38,11 @@ public class InventoryRepository extends BaseRepository {
/**
* Sends a request to the API to create a new inventory record.
*/
public LiveData<Resource<InventoryDTO>> createInventory(InventoryRequest request) {
public LiveData<Resource<InventoryDTO>> createInventory(InventoryDTO request) {
return executeCall(inventoryApi.createInventory(request));
}
public LiveData<Resource<InventoryDTO>> updateInventory(Long id, InventoryRequest request) {
public LiveData<Resource<InventoryDTO>> updateInventory(Long id, InventoryDTO request) {
return executeCall(inventoryApi.updateInventory(id, request));
}

View File

@@ -6,7 +6,6 @@ import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.InventoryRequest;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.CategoryRepository;
@@ -50,14 +49,14 @@ public class InventoryViewModel extends ViewModel {
/**
* Creates a new inventory record.
*/
public LiveData<Resource<InventoryDTO>> createInventory(InventoryRequest request) {
public LiveData<Resource<InventoryDTO>> createInventory(InventoryDTO request) {
return inventoryRepository.createInventory(request);
}
/**
* Updates an existing inventory record by ID.
*/
public LiveData<Resource<InventoryDTO>> updateInventory(Long id, InventoryRequest request) {
public LiveData<Resource<InventoryDTO>> updateInventory(Long id, InventoryDTO request) {
return inventoryRepository.updateInventory(id, request);
}

View File

@@ -67,7 +67,23 @@
android:layout_marginBottom="12dp"
android:visibility="gone"/>
<!-- Product search label -->
<!-- Store selection label -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Store"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Store Spinner -->
<Spinner
android:id="@+id/spinnerInventoryStore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Product selection label -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -76,25 +92,12 @@
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- AutoComplete search box -->
<AutoCompleteTextView
android:id="@+id/etProductSearch"
<!-- Product Spinner -->
<Spinner
android:id="@+id/spinnerInventoryProduct"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Search product name…"
android:inputType="text"
android:completionThreshold="1"
android:layout_marginBottom="4dp"/>
<!-- Selected product info (ID + category) shown after picking -->
<TextView
android:id="@+id/tvProductInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#888888"
android:textSize="12sp"
android:layout_marginBottom="16dp"
android:visibility="gone"/>
android:layout_marginBottom="16dp"/>
<!-- Quantity label -->
<TextView

View File

@@ -47,7 +47,7 @@
android:id="@+id/tvQuantity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:text="Stock: 0"
android:textColor="@color/text_dark"
android:textSize="16sp"
android:textStyle="bold" />
@@ -55,31 +55,14 @@
</LinearLayout>
<TextView
android:id="@+id/tvInventoryId"
android:id="@+id/tvInventoryStore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Inv ID: —"
android:layout_marginTop="2dp"
android:text="Store: —"
android:textColor="#888888"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/tvProdId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Prod ID: —"
android:textColor="#888888"
android:textSize="13sp" />
</LinearLayout>
android:textSize="14sp"
android:textStyle="italic" />
<View
android:layout_width="match_parent"