Did the same to inventory

This commit is contained in:
Alex
2026-04-07 14:55:43 -06:00
parent 4e887c1a73
commit aa30efd3b6
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); InventoryDTO inv = inventoryList.get(position);
ItemInventoryBinding binding = holder.binding; 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 // Column: Product Name
binding.tvProductName.setText(inv.getProductName() != null ? inv.getProductName() : ""); binding.tvProductName.setText(inv.getProductName() != null ? inv.getProductName() : "");
// Column: Store Name
binding.tvInventoryStore.setText("Store: " + (inv.getStoreName() != null ? inv.getStoreName() : ""));
// Column: Quantity // Column: Quantity
int qty = inv.getQuantity() != null ? inv.getQuantity() : 0; 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) // Low stock = red, normal = green (like desktop reorder concept)
if (qty <= 5) { 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.BulkDeleteRequest;
import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.InventoryRequest;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PageResponse;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.http.Body; import retrofit2.http.Body;
import retrofit2.http.DELETE; import retrofit2.http.DELETE;
import retrofit2.http.GET; import retrofit2.http.GET;
import retrofit2.http.HTTP;
import retrofit2.http.POST; import retrofit2.http.POST;
import retrofit2.http.PUT; import retrofit2.http.PUT;
import retrofit2.http.Path; import retrofit2.http.Path;
@@ -32,17 +32,17 @@ public interface InventoryApi {
// POST /api/v1/inventory // POST /api/v1/inventory
@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}
@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}
@DELETE("api/v1/inventory/{id}") @DELETE("api/v1/inventory/{id}")
Call<Void> deleteInventory(@Path("id") Long id); Call<Void> deleteInventory(@Path("id") Long id);
// DELETE /api/v1/inventory (bulk delete) // 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); Call<Void> bulkDeleteInventory(@Body BulkDeleteRequest request);
} }

View File

@@ -6,6 +6,8 @@ public class InventoryDTO {
private Long prodId; private Long prodId;
private String productName; private String productName;
private String categoryName; private String categoryName;
private Long storeId;
private String storeName;
private Integer quantity; private Integer quantity;
private String createdAt; private String createdAt;
private String updatedAt; private String updatedAt;
@@ -14,8 +16,9 @@ public class InventoryDTO {
} }
// Constructor for create/update requests (matches InventoryRequest) // 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.prodId = prodId;
this.storeId = storeId;
this.quantity = quantity; this.quantity = quantity;
} }
@@ -51,6 +54,22 @@ public class InventoryDTO {
this.categoryName = categoryName; 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() { public Integer getQuantity() {
return quantity; 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; package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.os.Bundle; 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.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -18,15 +13,16 @@ import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding; import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding;
import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.InventoryRequest;
import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.InventoryViewModel; import com.example.petstoremobile.viewmodels.InventoryViewModel;
import com.example.petstoremobile.viewmodels.ProductViewModel; import com.example.petstoremobile.viewmodels.ProductViewModel;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -43,20 +39,15 @@ public class InventoryDetailFragment extends Fragment {
private InventoryViewModel inventoryViewModel; private InventoryViewModel inventoryViewModel;
private ProductViewModel productViewModel; private ProductViewModel productViewModel;
private StoreViewModel storeViewModel;
private boolean isEditing = false; private boolean isEditing = false;
private long inventoryId = -1; private long inventoryId = -1;
private long preselectedStoreId = -1;
private long preselectedProductId = -1;
// The product selected from the dropdown private List<StoreDTO> storeList = new ArrayList<>();
private ProductDTO selectedProduct = null; private List<ProductDTO> productList = new ArrayList<>();
// 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;
/** /**
* Initializes the view models. * Initializes the view models.
@@ -66,6 +57,7 @@ public class InventoryDetailFragment extends Fragment {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
inventoryViewModel = new ViewModelProvider(this).get(InventoryViewModel.class); inventoryViewModel = new ViewModelProvider(this).get(InventoryViewModel.class);
productViewModel = new ViewModelProvider(this).get(ProductViewModel.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) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
setupProductSearch(); loadSpinnersData();
handleArguments(); handleArguments();
binding.btnInventoryBack.setOnClickListener(v -> navigateBack()); binding.btnInventoryBack.setOnClickListener(v -> navigateBack());
binding.btnSaveInventory.setOnClickListener(v -> saveInventory()); binding.btnSaveInventory.setOnClickListener(v -> saveInventory());
binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete()); 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 @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
if (searchRunnable != null) {
searchHandler.removeCallbacks(searchRunnable);
}
binding = null; binding = null;
} }
/** /**
* Sets up the product search dropdown. * Fetches required data for spinners from the backend.
*/ */
private void setupProductSearch() { private void loadSpinnersData() {
binding.etProductSearch.addTextChangedListener(new TextWatcher() { loadStores();
@Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { loadProducts();
}
@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);
}
});
} }
/** /**
* Searches for products matching the query from the backend. * Loads the list of stores for the spinner.
*/ */
private void searchProducts(String query) { private void loadStores() {
if (getView() == null) return; storeViewModel.getAllStores(0, 100).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(); storeList = resource.data.getContent();
productSuggestions.addAll(resource.data.getContent()); refreshStoreSpinner();
// 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();
} }
}); });
} }
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. * Handles fragment arguments to determine if we are in edit or add mode.
*/ */
@@ -193,7 +155,6 @@ public class InventoryDetailFragment extends Fragment {
isEditing = false; isEditing = false;
binding.tvInventoryMode.setText("Add Inventory"); binding.tvInventoryMode.setText("Add Inventory");
binding.tvInventoryId.setVisibility(View.GONE); binding.tvInventoryId.setVisibility(View.GONE);
binding.tvProductInfo.setVisibility(View.GONE);
binding.btnDeleteInventory.setVisibility(View.GONE); binding.btnDeleteInventory.setVisibility(View.GONE);
binding.btnSaveInventory.setText("Add"); binding.btnSaveInventory.setText("Add");
} }
@@ -207,20 +168,12 @@ public class InventoryDetailFragment extends Fragment {
if (resource == null) return; if (resource == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
InventoryDTO inv = resource.data; InventoryDTO inv = resource.data;
binding.etProductSearch.setText(inv.getProductName());
binding.etQuantity.setText(String.valueOf(inv.getQuantity())); 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) { refreshStoreSpinner();
binding.tvProductInfo.setText( refreshProductSpinner();
"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());
}
} else if (resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show(); 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. * Validates input and saves the current inventory item details to the backend.
*/ */
private void saveInventory() { private void saveInventory() {
if (selectedProduct == null) { if (binding.spinnerInventoryStore.getSelectedItemPosition() == 0) {
binding.etProductSearch.setError("Please select a product from the list"); Toast.makeText(getContext(), "Please select a store", Toast.LENGTH_SHORT).show();
binding.etProductSearch.requestFocus(); return;
}
if (binding.spinnerInventoryProduct.getSelectedItemPosition() == 0) {
Toast.makeText(getContext(), "Please select a product", Toast.LENGTH_SHORT).show();
return; return;
} }
@@ -243,8 +199,10 @@ public class InventoryDetailFragment extends Fragment {
} }
int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim()); 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); setButtonsEnabled(false);
if (isEditing) { if (isEditing) {

View File

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

View File

@@ -67,7 +67,23 @@
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:visibility="gone"/> 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 <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -76,25 +92,12 @@
android:textSize="12sp" android:textSize="12sp"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="4dp"/>
<!-- AutoComplete search box --> <!-- Product Spinner -->
<AutoCompleteTextView <Spinner
android:id="@+id/etProductSearch" android:id="@+id/spinnerInventoryProduct"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="Search product name…" android:layout_marginBottom="16dp"/>
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"/>
<!-- Quantity label --> <!-- Quantity label -->
<TextView <TextView

View File

@@ -47,7 +47,7 @@
android:id="@+id/tvQuantity" android:id="@+id/tvQuantity"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="0" android:text="Stock: 0"
android:textColor="@color/text_dark" android:textColor="@color/text_dark"
android:textSize="16sp" android:textSize="16sp"
android:textStyle="bold" /> android:textStyle="bold" />
@@ -55,31 +55,14 @@
</LinearLayout> </LinearLayout>
<TextView <TextView
android:id="@+id/tvInventoryId" android:id="@+id/tvInventoryStore"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="2dp"
android:text="Inv ID: —" android:text="Store: —"
android:textColor="#888888" android:textColor="#888888"
android:textSize="14sp" /> android:textSize="14sp"
android:textStyle="italic" />
<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>
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"