From 87a4404c20bb81afde7625322b904079b7f7f617 Mon Sep 17 00:00:00 2001 From: Nikitha Date: Sun, 29 Mar 2026 16:26:21 -0600 Subject: [PATCH] Inventory Inventory- details of product loads with id and described with filter, and categories selection --- .../adapters/InventoryAdapter.java | 125 ++++-- .../petstoremobile/api/InventoryApi.java | 46 ++ .../dtos/BulkDeleteRequest.java | 22 + .../petstoremobile/dtos/InventoryDTO.java | 77 ++++ .../petstoremobile/dtos/InventoryRequest.java | 31 ++ .../listfragments/InventoryFragment.java | 406 ++++++++++++++---- .../InventoryDetailFragment.java | 400 ++++++++++++----- .../main/res/layout/fragment_inventory.xml | 58 ++- .../res/layout/fragment_inventory_detail.xml | 84 ++-- .../src/main/res/layout/item_inventory.xml | 122 +++--- 10 files changed, 1028 insertions(+), 343 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java index 7ae36dc5..63290ae7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java @@ -1,79 +1,148 @@ package com.example.petstoremobile.adapters; - import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.CheckBox; import android.widget.TextView; + import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; + import com.example.petstoremobile.R; -import com.example.petstoremobile.models.Inventory; +import com.example.petstoremobile.dtos.InventoryDTO; + +import java.util.ArrayList; import java.util.List; public class InventoryAdapter extends RecyclerView.Adapter { - private List inventoryList; - private OnInventoryClickListener inventoryClickListener; + private final List inventoryList; + private final OnInventoryClickListener clickListener; + private final List selectedIds = new ArrayList<>(); + private boolean selectionMode = false; - // Interface for inventory click on recycler view public interface OnInventoryClickListener { void onInventoryClick(int position); + + void onSelectionChanged(int selectedCount); } - // Constructor - public InventoryAdapter(List inventoryList, OnInventoryClickListener inventoryClickListener) { + public InventoryAdapter(List inventoryList, OnInventoryClickListener clickListener) { this.inventoryList = inventoryList; - this.inventoryClickListener = inventoryClickListener; + this.clickListener = clickListener; } - // Get the controls of each row in recycler view public static class InventoryViewHolder extends RecyclerView.ViewHolder { - TextView tvItemName, tvCategory, tvQuantity, tvUnitPrice, tvSupplier; + // Matches desktop table columns: Inventory ID, Product ID, Product Name, + // Quantity + TextView tvInventoryId, tvProdId, tvProductName, tvQuantity; + CheckBox checkBox; public InventoryViewHolder(@NonNull View v) { super(v); - tvItemName = v.findViewById(R.id.tvItemName); - tvCategory = v.findViewById(R.id.tvCategory); + tvInventoryId = v.findViewById(R.id.tvInventoryId); + tvProdId = v.findViewById(R.id.tvProdId); + tvProductName = v.findViewById(R.id.tvProductName); tvQuantity = v.findViewById(R.id.tvQuantity); - tvUnitPrice = v.findViewById(R.id.tvUnitPrice); - tvSupplier = v.findViewById(R.id.tvInvSupplier); + checkBox = v.findViewById(R.id.cbSelectInventory); } } - // Create a new row view @NonNull @Override public InventoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_inventory, parent, false); + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_inventory, parent, false); return new InventoryViewHolder(v); } - // Populate the row with inventory data @Override public void onBindViewHolder(@NonNull InventoryViewHolder holder, int position) { - Inventory inventory = inventoryList.get(position); + InventoryDTO inv = inventoryList.get(position); - holder.tvItemName.setText(inventory.getItemName()); - holder.tvCategory.setText(inventory.getCategory()); - holder.tvQuantity.setText("Qty: " + inventory.getQuantity()); - holder.tvUnitPrice.setText("$" + String.format("%.2f", inventory.getUnitPrice())); - holder.tvSupplier.setText("Supplier: " + inventory.getSupplier()); + // Column: Inventory ID + holder.tvInventoryId.setText(String.valueOf(inv.getInventoryId() != null ? inv.getInventoryId() : "—")); - // Highlight low stock items in red - if (inventory.getQuantity() <= 5) { + // Column: Product ID + holder.tvProdId.setText(String.valueOf(inv.getProdId() != null ? inv.getProdId() : "—")); + + // Column: Product Name + holder.tvProductName.setText(inv.getProductName() != null ? inv.getProductName() : "—"); + + // Column: Quantity + int qty = inv.getQuantity() != null ? inv.getQuantity() : 0; + holder.tvQuantity.setText(String.valueOf(qty)); + + // Low stock = red, normal = green (like desktop reorder concept) + if (qty <= 5) { holder.tvQuantity.setTextColor(Color.parseColor("#F44336")); } else { holder.tvQuantity.setTextColor(Color.parseColor("#4CAF50")); } - // When a row is clicked, open the detail view - holder.itemView.setOnClickListener(v -> inventoryClickListener.onInventoryClick(position)); + // Bulk delete selection mode + if (selectionMode) { + holder.checkBox.setVisibility(View.VISIBLE); + holder.checkBox.setChecked(inv.getInventoryId() != null + && selectedIds.contains(inv.getInventoryId())); + } else { + holder.checkBox.setVisibility(View.GONE); + holder.checkBox.setChecked(false); + } + + holder.itemView.setOnClickListener(v -> { + if (selectionMode) { + toggleSelection(inv.getInventoryId(), holder.checkBox); + } else { + clickListener.onInventoryClick(holder.getAdapterPosition()); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!selectionMode) { + selectionMode = true; + toggleSelection(inv.getInventoryId(), holder.checkBox); + notifyDataSetChanged(); + } + return true; + }); + } + + private void toggleSelection(Long id, CheckBox checkBox) { + if (id == null) + return; + if (selectedIds.contains(id)) { + selectedIds.remove(id); + checkBox.setChecked(false); + } else { + selectedIds.add(id); + checkBox.setChecked(true); + } + clickListener.onSelectionChanged(selectedIds.size()); + if (selectedIds.isEmpty()) { + selectionMode = false; + notifyDataSetChanged(); + } + } + + public List getSelectedIds() { + return new ArrayList<>(selectedIds); + } + + public void clearSelection() { + selectedIds.clear(); + selectionMode = false; + notifyDataSetChanged(); + } + + public boolean isInSelectionMode() { + return selectionMode; } @Override public int getItemCount() { return inventoryList.size(); } -} +} \ No newline at end of file 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 new file mode 100644 index 00000000..f54616ee --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java @@ -0,0 +1,46 @@ +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.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface InventoryApi { + + // GET /api/v1/inventory?q=...&page=...&size=... + @GET("api/v1/inventory") + Call> getAllInventory( + @Query("q") String query, + @Query("page") int page, + @Query("size") int size, + @Query("sort") String sort); + + // GET /api/v1/inventory/{id} + @GET("api/v1/inventory/{id}") + Call getInventoryById(@Path("id") Long id); + + // POST /api/v1/inventory + @POST("api/v1/inventory") + Call createInventory(@Body InventoryRequest request); + + // PUT /api/v1/inventory/{id} + @PUT("api/v1/inventory/{id}") + Call updateInventory(@Path("id") Long id, @Body InventoryRequest request); + + // DELETE /api/v1/inventory/{id} + @DELETE("api/v1/inventory/{id}") + Call deleteInventory(@Path("id") Long id); + + // DELETE /api/v1/inventory (bulk delete) + @DELETE("api/v1/inventory") + Call bulkDeleteInventory(@Body BulkDeleteRequest request); +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java new file mode 100644 index 00000000..49f92f06 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java @@ -0,0 +1,22 @@ +package com.example.petstoremobile.dtos; + +import java.util.List; + +public class BulkDeleteRequest { + private List ids; + + public BulkDeleteRequest() { + } + + public BulkDeleteRequest(List ids) { + this.ids = ids; + } + + public List getIds() { + return ids; + } + + public void setIds(List ids) { + this.ids = ids; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java new file mode 100644 index 00000000..fe2ec542 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java @@ -0,0 +1,77 @@ +package com.example.petstoremobile.dtos; + +public class InventoryDTO { + // Response fields (from backend InventoryResponse) + private Long inventoryId; + private Long prodId; + private String productName; + private String categoryName; + private Integer quantity; + private String createdAt; + private String updatedAt; + + public InventoryDTO() { + } + + // Constructor for create/update requests (matches InventoryRequest) + public InventoryDTO(Long prodId, Integer quantity) { + this.prodId = prodId; + this.quantity = quantity; + } + + public Long getInventoryId() { + return inventoryId; + } + + public void setInventoryId(Long inventoryId) { + this.inventoryId = inventoryId; + } + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public String getCategoryName() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName = categoryName; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public String getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; + } + + public String getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(String updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java new file mode 100644 index 00000000..f84dfb5f --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java @@ -0,0 +1,31 @@ +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; + } +} + 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 38546e83..6eb27fb4 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,162 +1,378 @@ package com.example.petstoremobile.fragments.listfragments; -// Added search/filter bar to filter inventory by item name or category. -// Added pull-to-refresh using SwipeRefreshLayout. - import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +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.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ImageButton; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.InventoryAdapter; +import com.example.petstoremobile.api.CategoryApi; +import com.example.petstoremobile.api.InventoryApi; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.detailfragments.InventoryDetailFragment; -import com.example.petstoremobile.models.Inventory; import com.google.android.material.floatingactionbutton.FloatingActionButton; + import java.util.ArrayList; import java.util.List; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener { - private List inventoryList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private static final String TAG = "InventoryFragment"; + private static final int PAGE_SIZE = 20; + + private final List inventoryList = new ArrayList<>(); + private final List categoryList = new ArrayList<>(); private InventoryAdapter adapter; + private InventoryApi inventoryApi; + private CategoryApi categoryApi; + private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; + private Spinner spinnerCategory; private ImageButton hamburger; + private Button btnBulkDelete; + private TextView tvSelectionCount; + + // 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; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_inventory, container, false); - hamburger = view.findViewById(R.id.btnHamburger); + inventoryApi = RetrofitClient.getInventoryApi(requireContext()); + categoryApi = RetrofitClient.getCategoryApi(requireContext()); + + hamburger = view.findViewById(R.id.btnHamburger); + btnBulkDelete = view.findViewById(R.id.btnBulkDelete); + tvSelectionCount = view.findViewById(R.id.tvSelectionCount); + spinnerCategory = view.findViewById(R.id.spinnerCategory); - loadInventoryData(); // TODO: Replace with actual API call when backend is ready setupRecyclerView(view); setupSearch(view); setupSwipeRefresh(view); + loadCategories(); // loads categories then triggers loadInventory + loadInventory(true); - FloatingActionButton fabAddInventory = view.findViewById(R.id.fabAddInventory); - fabAddInventory.setOnClickListener(v -> openInventoryDetails(-1)); + view.findViewById(R.id.fabAddInventory) + .setOnClickListener(v -> openDetail(null)); - //Make the hamburger button open the drawer from listFragment hamburger.setOnClickListener(v -> { - ListFragment listFragment = (ListFragment) getParentFragment(); - //if list fragment is found then use its helper function to open the drawer - if (listFragment != null) { - listFragment.openDrawer(); - } + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) + lf.openDrawer(); }); + btnBulkDelete.setOnClickListener(v -> confirmBulkDelete()); + return view; } - // Filters inventory list by item name or category - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchInventory); - etSearch.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) { - filterInventory(s.toString()); + // Categories + private void loadCategories() { + categoryApi.getAllCategories(0, 100).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, + Response> response) { + if (response.isSuccessful() && response.body() != null) { + categoryList.clear(); + categoryList.addAll(response.body().getContent()); + setupCategorySpinner(); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + Log.e(TAG, "Failed to load categories", t); + // Still setup spinner with just "All" + setupCategorySpinner(); } - @Override public void afterTextChanged(Editable s) {} }); } - private void filterInventory(String query) { - filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(inventoryList); - } else { - String lower = query.toLowerCase(); - for (Inventory i : inventoryList) { - if (i.getItemName().toLowerCase().contains(lower) - || i.getCategory().toLowerCase().contains(lower) - || i.getSupplier().toLowerCase().contains(lower)) { - filteredList.add(i); + 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()); + } + + ArrayAdapter spinnerAdapter = new ArrayAdapter<>( + requireContext(), + android.R.layout.simple_spinner_item, + categoryNames); + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerCategory.setAdapter(spinnerAdapter); + + 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) { + } + }); + } + + // Search + + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchInventory); + etSearch.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) { + if (searchRunnable != null) + searchHandler.removeCallbacks(searchRunnable); + searchRunnable = () -> { + currentQuery = s.toString().trim(); + loadInventory(true); + }; + searchHandler.postDelayed(searchRunnable, 400); + } + }); + } + + // RecyclerView + infinite scroll + private void setupRecyclerView(View view) { + RecyclerView rv = view.findViewById(R.id.recyclerViewInventory); + adapter = new InventoryAdapter(inventoryList, this); + rv.setLayoutManager(new LinearLayoutManager(getContext())); + rv.setAdapter(adapter); + + rv.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) + return; + LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager(); + if (lm == null) + return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + loadInventory(false); } } - } - adapter.notifyDataSetChanged(); + }); } private void setupSwipeRefresh(View view) { swipeRefreshLayout = view.findViewById(R.id.swipeRefreshInventory); - swipeRefreshLayout.setOnRefreshListener(() -> { - loadInventoryData(); // TODO: Replace with actual API call - filterInventory(etSearch.getText().toString()); - swipeRefreshLayout.setRefreshing(false); - }); + swipeRefreshLayout.setOnRefreshListener(() -> loadInventory(true)); } - private void openInventoryDetails(int position) { - InventoryDetailFragment detailFragment = new InventoryDetailFragment(); + // Load inventory + private void loadInventory(boolean reset) { + if (isLoading) + return; + isLoading = true; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + // Build query: combine search text + selected category + String q = buildQuery(); + + inventoryApi.getAllInventory(q, currentPage, PAGE_SIZE, "inventoryId,asc") + .enqueue(new Callback>() { + @Override + public void onResponse(Call> call, + Response> response) { + isLoading = false; + if (swipeRefreshLayout != null) + swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null) { + PageResponse page = response.body(); + if (reset) + inventoryList.clear(); + inventoryList.addAll(page.getContent()); + adapter.notifyDataSetChanged(); + isLastPage = page.isLast(); + if (!isLastPage) + currentPage++; + } else { + Log.e(TAG, "Error " + response.code()); + Toast.makeText(getContext(), "Failed to load inventory", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + isLoading = false; + if (swipeRefreshLayout != null) + swipeRefreshLayout.setRefreshing(false); + Log.e(TAG, "Network error", t); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + + // Combines search text and category into one query string for ?q= + 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; + } + + // Bulk delete + private void confirmBulkDelete() { + List ids = adapter.getSelectedIds(); + if (ids.isEmpty()) + return; + + new androidx.appcompat.app.AlertDialog.Builder(requireContext()) + .setTitle("Delete " + ids.size() + " item(s)?") + .setMessage("This cannot be undone.") + .setPositiveButton("Delete", (d, w) -> bulkDelete(ids)) + .setNegativeButton("Cancel", null) + .show(); + } + + private void bulkDelete(List ids) { + inventoryApi.bulkDeleteInventory(new BulkDeleteRequest(ids)) + .enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + adapter.clearSelection(); + hideBulkDeleteBar(); + loadInventory(true); + Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void hideBulkDeleteBar() { + if (btnBulkDelete != null) + btnBulkDelete.setVisibility(View.GONE); + if (tvSelectionCount != null) + tvSelectionCount.setVisibility(View.GONE); + } + + // Navigation + private void openDetail(InventoryDTO inv) { + InventoryDetailFragment detail = new InventoryDetailFragment(); Bundle args = new Bundle(); - args.putInt("position", position); - if (position != -1) { - Inventory inventory = filteredList.get(position); - int realPosition = inventoryList.indexOf(inventory); - args.putInt("position", realPosition); - args.putInt("inventoryId", inventory.getInventoryId()); - args.putString("itemName", inventory.getItemName()); - args.putString("category", inventory.getCategory()); - args.putInt("quantity", inventory.getQuantity()); - args.putDouble("unitPrice", inventory.getUnitPrice()); - args.putString("supplier", inventory.getSupplier()); + if (inv != null) { + args.putLong("inventoryId", inv.getInventoryId()); + args.putLong("prodId", inv.getProdId() != null ? inv.getProdId() : -1); + args.putString("productName", inv.getProductName()); + args.putString("categoryName", inv.getCategoryName()); + args.putInt("quantity", inv.getQuantity() != null ? inv.getQuantity() : 0); } - detailFragment.setArguments(args); - detailFragment.setInventoryFragment(this); + detail.setArguments(args); + detail.setInventoryFragment(this); - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.loadFragment(detailFragment); + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) + lf.loadFragment(detail); } - public void onInventorySaved(int position, Inventory inventory) { - if (position == -1) { - inventoryList.add(inventory); - } else { - inventoryList.set(position, inventory); - } - filterInventory(etSearch.getText().toString()); + public void onInventoryChanged() { + loadInventory(true); } - public void onInventoryDeleted(int position) { - inventoryList.remove(position); - filterInventory(etSearch.getText().toString()); - } + // Adapter callbacks @Override public void onInventoryClick(int position) { - openInventoryDetails(position); + if (position >= 0 && position < inventoryList.size()) { + openDetail(inventoryList.get(position)); + } } - private void loadInventoryData() { - inventoryList.clear(); - inventoryList.add(new Inventory(1, "Dog Food - Large", "Food", 50, 25.99, "PetSupplies Co.")); - inventoryList.add(new Inventory(2, "Cat Litter", "Hygiene", 30, 12.99, "CleanPaws Ltd.")); - inventoryList.add(new Inventory(3, "Dog Leash", "Accessories", 4, 15.99, "PetGear Inc.")); - inventoryList.add(new Inventory(4, "Bird Cage - Medium", "Housing", 8, 79.99, "BirdWorld")); - inventoryList.add(new Inventory(5, "Flea Treatment", "Medicine", 2, 34.99, "VetCare Supply")); - filteredList.clear(); - filteredList.addAll(inventoryList); + @Override + public void onSelectionChanged(int selectedCount) { + if (selectedCount > 0) { + btnBulkDelete.setVisibility(View.VISIBLE); + tvSelectionCount.setVisibility(View.VISIBLE); + tvSelectionCount.setText(selectedCount + " selected"); + } else { + hideBulkDeleteBar(); + } } - - private void setupRecyclerView(View view) { - RecyclerView recyclerView = view.findViewById(R.id.recyclerViewInventory); - adapter = new InventoryAdapter(filteredList, this); - recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(adapter); - } -} +} \ No newline at end of file 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 175feb91..9846bb36 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 @@ -1,34 +1,65 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; -// Uses InputValidator for detailed field validation and ActivityLogger to log all changes. - import android.os.Bundle; -import androidx.fragment.app.Fragment; +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.AutoCompleteTextView; import android.widget.Button; -import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + import com.example.petstoremobile.R; +import com.example.petstoremobile.api.InventoryApi; +import com.example.petstoremobile.api.ProductApi; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.InventoryRequest; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.InventoryFragment; -import com.example.petstoremobile.models.Inventory; -import com.example.petstoremobile.utils.ActivityLogger; -import com.example.petstoremobile.utils.InputValidator; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; public class InventoryDetailFragment extends Fragment { - private TextView tvMode, tvInventoryId; - private EditText etItemName, etCategory, etQuantity, etUnitPrice, etSupplier; - private Button btnSaveInventory, btnDeleteInventory, btnBack; - private int inventoryId; - private int position; - private boolean isEditing = false; + private TextView tvMode, tvInventoryId, tvProductInfo; + private AutoCompleteTextView etProductSearch; + private android.widget.EditText etQuantity; + private Button btnSave, btnDelete, btnBack; + + private InventoryApi inventoryApi; + private ProductApi productApi; private InventoryFragment inventoryFragment; - // Set the inventory fragment as parent so we refer back when save or delete is done + private boolean isEditing = false; + private long inventoryId = -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 productSuggestions = new ArrayList<>(); + private ArrayAdapter dropdownAdapter; + public void setInventoryFragment(InventoryFragment fragment) { this.inventoryFragment = fragment; } @@ -38,102 +69,271 @@ public class InventoryDetailFragment extends Fragment { Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_inventory_detail, container, false); + inventoryApi = RetrofitClient.getInventoryApi(requireContext()); + productApi = RetrofitClient.getProductApi(requireContext()); + initViews(view); + setupProductSearch(); handleArguments(); - btnBack.setOnClickListener(v -> { - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); - }); - btnSaveInventory.setOnClickListener(v -> saveInventory()); - btnDeleteInventory.setOnClickListener(v -> deleteInventory()); + btnBack.setOnClickListener(v -> navigateBack()); + btnSave.setOnClickListener(v -> saveInventory()); + btnDelete.setOnClickListener(v -> confirmDelete()); return view; } - // Validates all fields using InputValidator, then saves the inventory item - private void saveInventory() { - if (!InputValidator.isNotEmpty(etItemName, "Item Name")) return; - if (!InputValidator.isNotEmpty(etCategory, "Category")) return; - if (!InputValidator.isPositiveInteger(etQuantity, "Quantity")) return; - if (!InputValidator.isPositiveDecimal(etUnitPrice, "Unit Price")) return; - if (!InputValidator.isNotEmpty(etSupplier, "Supplier")) return; - - String itemName = etItemName.getText().toString().trim(); - String category = etCategory.getText().toString().trim(); - int quantity = Integer.parseInt(etQuantity.getText().toString().trim()); - double unitPrice = Double.parseDouble(etUnitPrice.getText().toString().trim()); - String supplier = etSupplier.getText().toString().trim(); - - try { - if (isEditing) { - // TODO: Replace with actual API PUT call when backend is ready - Inventory updated = new Inventory(inventoryId, itemName, category, quantity, unitPrice, supplier); - if (inventoryFragment != null) inventoryFragment.onInventorySaved(position, updated); - ActivityLogger.logChange(requireContext(), "Inventory", "UPDATED", inventoryId); - Toast.makeText(getContext(), "Inventory item updated.", Toast.LENGTH_SHORT).show(); - } else { - // TODO: Replace with actual API POST call when backend is ready - Inventory newItem = new Inventory(0, itemName, category, quantity, unitPrice, supplier); - if (inventoryFragment != null) inventoryFragment.onInventorySaved(-1, newItem); - ActivityLogger.log(requireContext(), "Added new Inventory item: " + itemName); - Toast.makeText(getContext(), "Inventory item added.", Toast.LENGTH_SHORT).show(); - } - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); - } catch (Exception e) { - ActivityLogger.logException(requireContext(), "InventoryDetailFragment.saveInventory", e); - Toast.makeText(getContext(), "Error saving inventory item.", Toast.LENGTH_SHORT).show(); - } - } - - // Deletes the inventory item and logs the action - private void deleteInventory() { - try { - // TODO: Replace with actual API DELETE call when backend is ready - if (inventoryFragment != null) inventoryFragment.onInventoryDeleted(position); - ActivityLogger.logChange(requireContext(), "Inventory", "DELETED", inventoryId); - Toast.makeText(getContext(), "Inventory item deleted.", Toast.LENGTH_SHORT).show(); - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); - } catch (Exception e) { - ActivityLogger.logException(requireContext(), "InventoryDetailFragment.deleteInventory", e); - Toast.makeText(getContext(), "Error deleting inventory item.", Toast.LENGTH_SHORT).show(); - } - } - - private void handleArguments() { - if (getArguments() != null && getArguments().containsKey("inventoryId")) { - isEditing = true; - inventoryId = getArguments().getInt("inventoryId"); - position = getArguments().getInt("position"); - tvMode.setText("Edit Inventory Item"); - tvInventoryId.setText("ID: " + inventoryId); - etItemName.setText(getArguments().getString("itemName")); - etCategory.setText(getArguments().getString("category")); - etQuantity.setText(String.valueOf(getArguments().getInt("quantity"))); - etUnitPrice.setText(String.valueOf(getArguments().getDouble("unitPrice"))); - etSupplier.setText(getArguments().getString("supplier")); - btnDeleteInventory.setVisibility(View.VISIBLE); - } else { - isEditing = false; - tvMode.setText("Add Inventory Item"); - tvInventoryId.setVisibility(View.GONE); - btnDeleteInventory.setVisibility(View.GONE); - btnSaveInventory.setText("Add"); - } - } - private void initViews(View view) { tvMode = view.findViewById(R.id.tvInventoryMode); tvInventoryId = view.findViewById(R.id.tvInventoryId); - etItemName = view.findViewById(R.id.etItemName); - etCategory = view.findViewById(R.id.etInventoryCategory); + tvProductInfo = view.findViewById(R.id.tvProductInfo); + etProductSearch = view.findViewById(R.id.etProductSearch); etQuantity = view.findViewById(R.id.etQuantity); - etUnitPrice = view.findViewById(R.id.etUnitPrice); - etSupplier = view.findViewById(R.id.etInventorySupplier); - btnSaveInventory = view.findViewById(R.id.btnSaveInventory); - btnDeleteInventory = view.findViewById(R.id.btnDeleteInventory); + btnSave = view.findViewById(R.id.btnSaveInventory); + btnDelete = view.findViewById(R.id.btnDeleteInventory); btnBack = view.findViewById(R.id.btnInventoryBack); + + // Setup dropdown adapter + dropdownAdapter = new ArrayAdapter<>(requireContext(), + android.R.layout.simple_dropdown_item_1line, new ArrayList<>()); + etProductSearch.setAdapter(dropdownAdapter); + etProductSearch.setThreshold(1); // start showing after 1 character } -} + + // Product search dropdown + private void setupProductSearch() { + 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; + 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 + etProductSearch.setOnItemClickListener((parent, view, position, id) -> { + if (position < productSuggestions.size()) { + selectedProduct = productSuggestions.get(position); + // Show product details below the search box + tvProductInfo.setText( + "ID: " + selectedProduct.getProdId() + + " • " + selectedProduct.getCategoryName()); + tvProductInfo.setVisibility(View.VISIBLE); + } + }); + } + + private void searchProducts(String query) { + productApi.getAllProducts(query, 0, 20).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, + Response> response) { + if (response.isSuccessful() && response.body() != null) { + productSuggestions.clear(); + productSuggestions.addAll(response.body().getContent()); + + // Build display strings: "Product Name (ID: X)" + List names = new ArrayList<>(); + for (ProductDTO p : productSuggestions) { + names.add(p.getProdName() + " (ID: " + p.getProdId() + ")"); + } + + dropdownAdapter.clear(); + dropdownAdapter.addAll(names); + dropdownAdapter.notifyDataSetChanged(); + etProductSearch.showDropDown(); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + Toast.makeText(getContext(), "Failed to load products", Toast.LENGTH_SHORT).show(); + } + }); + } + + // Arguments (edit mode) + + private void handleArguments() { + Bundle args = getArguments(); + if (args != null && args.containsKey("inventoryId")) { + isEditing = true; + inventoryId = args.getLong("inventoryId"); + + tvMode.setText("Edit Inventory"); + tvInventoryId.setText("Inventory ID: " + inventoryId); + tvInventoryId.setVisibility(View.VISIBLE); + + // Pre-fill search box with existing product name + String productName = args.getString("productName", ""); + long prodId = args.getLong("prodId", -1); + etProductSearch.setText(productName); + + // Show existing product info + if (prodId != -1) { + tvProductInfo.setText( + "ID: " + prodId + + " • " + args.getString("categoryName", "")); + tvProductInfo.setVisibility(View.VISIBLE); + + // Build a minimal ProductDTO so selectedProduct is not null on save + selectedProduct = new ProductDTO(productName, null, null, null); + selectedProduct.setProdId(prodId); + } + + etQuantity.setText(String.valueOf(args.getInt("quantity", 0))); + btnDelete.setVisibility(View.VISIBLE); + btnSave.setText("Save"); + } else { + isEditing = false; + tvMode.setText("Add Inventory"); + tvInventoryId.setVisibility(View.GONE); + tvProductInfo.setVisibility(View.GONE); + btnDelete.setVisibility(View.GONE); + btnSave.setText("Add"); + } + } + + // Save + private void saveInventory() { + if (selectedProduct == null) { + etProductSearch.setError("Please select a product from the list"); + etProductSearch.requestFocus(); + return; + } + + String quantityStr = etQuantity.getText().toString().trim(); + if (quantityStr.isEmpty()) { + etQuantity.setError("Quantity is required"); + etQuantity.requestFocus(); + return; + } + + int quantity; + try { + quantity = Integer.parseInt(quantityStr); + } catch (NumberFormatException e) { + etQuantity.setError("Invalid quantity"); + return; + } + + if (quantity < 0) { + etQuantity.setError("Quantity must be 0 or more"); + etQuantity.requestFocus(); + return; + } + + InventoryRequest request = new InventoryRequest(selectedProduct.getProdId(), quantity); + setButtonsEnabled(false); + + if (isEditing) { + inventoryApi.updateInventory(inventoryId, request).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + setButtonsEnabled(true); + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show(); + notifyParentAndGoBack(); + } else { + Toast.makeText(getContext(), "Update failed: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + setButtonsEnabled(true); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } else { + inventoryApi.createInventory(request).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + setButtonsEnabled(true); + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show(); + notifyParentAndGoBack(); + } else { + Toast.makeText(getContext(), "Create failed: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + setButtonsEnabled(true); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + } + + // Delete + private void confirmDelete() { + new AlertDialog.Builder(requireContext()) + .setTitle("Delete inventory item?") + .setMessage("This cannot be undone.") + .setPositiveButton("Delete", (d, w) -> deleteInventory()) + .setNegativeButton("Cancel", null) + .show(); + } + + private void deleteInventory() { + setButtonsEnabled(false); + inventoryApi.deleteInventory(inventoryId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + setButtonsEnabled(true); + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); + notifyParentAndGoBack(); + } else { + Toast.makeText(getContext(), "Delete failed: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + setButtonsEnabled(true); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + + // Helpers + + private void notifyParentAndGoBack() { + if (inventoryFragment != null) + inventoryFragment.onInventoryChanged(); + navigateBack(); + } + + private void navigateBack() { + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) + lf.getChildFragmentManager().popBackStack(); + } + + private void setButtonsEnabled(boolean enabled) { + btnSave.setEnabled(enabled); + btnDelete.setEnabled(enabled); + btnBack.setEnabled(enabled); + } +} \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_inventory.xml b/android/app/src/main/res/layout/fragment_inventory.xml index 7773c8da..d149214d 100644 --- a/android/app/src/main/res/layout/fragment_inventory.xml +++ b/android/app/src/main/res/layout/fragment_inventory.xml @@ -11,6 +11,7 @@ android:layout_height="match_parent" android:orientation="vertical"> + + + + + + + + + + +