Inventory

Inventory- details of product loads with id and described with filter, and categories selection
This commit is contained in:
Nikitha
2026-03-29 16:26:21 -06:00
parent 55f40572de
commit 87a4404c20
10 changed files with 1028 additions and 343 deletions

View File

@@ -1,75 +1,144 @@
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<InventoryAdapter.InventoryViewHolder> {
private List<Inventory> inventoryList;
private OnInventoryClickListener inventoryClickListener;
private final List<InventoryDTO> inventoryList;
private final OnInventoryClickListener clickListener;
private final List<Long> 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<Inventory> inventoryList, OnInventoryClickListener inventoryClickListener) {
public InventoryAdapter(List<InventoryDTO> 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<Long> getSelectedIds() {
return new ArrayList<>(selectedIds);
}
public void clearSelection() {
selectedIds.clear();
selectionMode = false;
notifyDataSetChanged();
}
public boolean isInSelectionMode() {
return selectionMode;
}
@Override

View File

@@ -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<PageResponse<InventoryDTO>> 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<InventoryDTO> getInventoryById(@Path("id") Long id);
// POST /api/v1/inventory
@POST("api/v1/inventory")
Call<InventoryDTO> createInventory(@Body InventoryRequest request);
// PUT /api/v1/inventory/{id}
@PUT("api/v1/inventory/{id}")
Call<InventoryDTO> updateInventory(@Path("id") Long id, @Body InventoryRequest 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")
Call<Void> bulkDeleteInventory(@Body BulkDeleteRequest request);
}

View File

@@ -0,0 +1,22 @@
package com.example.petstoremobile.dtos;
import java.util.List;
public class BulkDeleteRequest {
private List<Long> ids;
public BulkDeleteRequest() {
}
public BulkDeleteRequest(List<Long> ids) {
this.ids = ids;
}
public List<Long> getIds() {
return ids;
}
public void setIds(List<Long> ids) {
this.ids = ids;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<Inventory> inventoryList = new ArrayList<>();
private List<Inventory> filteredList = new ArrayList<>();
private static final String TAG = "InventoryFragment";
private static final int PAGE_SIZE = 20;
private final List<InventoryDTO> inventoryList = new ArrayList<>();
private final List<CategoryDTO> 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<PageResponse<CategoryDTO>>() {
@Override
public void onResponse(Call<PageResponse<CategoryDTO>> call,
Response<PageResponse<CategoryDTO>> response) {
if (response.isSuccessful() && response.body() != null) {
categoryList.clear();
categoryList.addAll(response.body().getContent());
setupCategorySpinner();
}
}
@Override
public void onFailure(Call<PageResponse<CategoryDTO>> 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);
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());
}
ArrayAdapter<String> 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 {
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);
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.setOnRefreshListener(() -> loadInventory(true));
}
// 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<PageResponse<InventoryDTO>>() {
@Override
public void onResponse(Call<PageResponse<InventoryDTO>> call,
Response<PageResponse<InventoryDTO>> response) {
isLoading = false;
if (swipeRefreshLayout != null)
swipeRefreshLayout.setRefreshing(false);
});
}
private void openInventoryDetails(int position) {
InventoryDetailFragment detailFragment = 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());
}
detailFragment.setArguments(args);
detailFragment.setInventoryFragment(this);
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) listFragment.loadFragment(detailFragment);
}
public void onInventorySaved(int position, Inventory inventory) {
if (position == -1) {
inventoryList.add(inventory);
if (response.isSuccessful() && response.body() != null) {
PageResponse<InventoryDTO> page = response.body();
if (reset)
inventoryList.clear();
inventoryList.addAll(page.getContent());
adapter.notifyDataSetChanged();
isLastPage = page.isLast();
if (!isLastPage)
currentPage++;
} else {
inventoryList.set(position, inventory);
Log.e(TAG, "Error " + response.code());
Toast.makeText(getContext(), "Failed to load inventory", Toast.LENGTH_SHORT).show();
}
filterInventory(etSearch.getText().toString());
}
public void onInventoryDeleted(int position) {
inventoryList.remove(position);
filterInventory(etSearch.getText().toString());
}
@Override
public void onFailure(Call<PageResponse<InventoryDTO>> 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<Long> 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<Long> ids) {
inventoryApi.bulkDeleteInventory(new BulkDeleteRequest(ids))
.enqueue(new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> 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<Void> 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();
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);
}
detail.setArguments(args);
detail.setInventoryFragment(this);
ListFragment lf = (ListFragment) getParentFragment();
if (lf != null)
lf.loadFragment(detail);
}
public void onInventoryChanged() {
loadInventory(true);
}
// 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);
}
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);
@Override
public void onSelectionChanged(int selectedCount) {
if (selectedCount > 0) {
btnBulkDelete.setVisibility(View.VISIBLE);
tvSelectionCount.setVisibility(View.VISIBLE);
tvSelectionCount.setText(selectedCount + " selected");
} else {
hideBulkDeleteBar();
}
}
}

View File

@@ -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<ProductDTO> productSuggestions = new ArrayList<>();
private ArrayAdapter<String> 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<PageResponse<ProductDTO>>() {
@Override
public void onResponse(Call<PageResponse<ProductDTO>> call,
Response<PageResponse<ProductDTO>> response) {
if (response.isSuccessful() && response.body() != null) {
productSuggestions.clear();
productSuggestions.addAll(response.body().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();
etProductSearch.showDropDown();
}
}
@Override
public void onFailure(Call<PageResponse<ProductDTO>> 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<InventoryDTO>() {
@Override
public void onResponse(Call<InventoryDTO> call, Response<InventoryDTO> 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<InventoryDTO> call, Throwable t) {
setButtonsEnabled(true);
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
}
});
} else {
inventoryApi.createInventory(request).enqueue(new Callback<InventoryDTO>() {
@Override
public void onResponse(Call<InventoryDTO> call, Response<InventoryDTO> 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<InventoryDTO> 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<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> 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<Void> 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);
}
}

View File

@@ -11,6 +11,7 @@
android:layout_height="match_parent"
android:orientation="vertical">
<!-- Header -->
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
@@ -38,12 +39,15 @@
</LinearLayout>
<!-- Search bar -->
<EditText
android:id="@+id/etSearchInventory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="Search by item name or category..."
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:hint="Search by product or category…"
android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search"
android:drawablePadding="8dp"
@@ -51,6 +55,51 @@
android:padding="12dp"
android:textColor="@color/text_dark"/>
<!-- Category filter dropdown -->
<Spinner
android:id="@+id/spinnerCategory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:background="@android:color/white"
android:padding="4dp"/>
<!-- Bulk-delete action bar (hidden until long-press) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@color/primary_medium"
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<TextView
android:id="@+id/tvSelectionCount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="0 selected"
android:textColor="@color/white"
android:visibility="gone"/>
<Button
android:id="@+id/btnBulkDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Delete Selected"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"
android:visibility="gone"/>
</LinearLayout>
<!-- Inventory list -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshInventory"
android:layout_width="match_parent"
@@ -60,7 +109,8 @@
android:id="@+id/recyclerViewInventory"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"/>
android:padding="8dp"
android:clipToPadding="false"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@@ -73,7 +123,7 @@
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:backgroundTint="@color/accent_coral"
android:contentDescription="Add Inventory Item"
android:contentDescription="Add Inventory"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@color/white"/>

View File

@@ -5,6 +5,7 @@
android:orientation="vertical"
android:background="@color/background_grey">
<!-- Header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
@@ -19,7 +20,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Inventory Item"
android:text="Add Inventory"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
@@ -28,10 +29,10 @@
android:id="@+id/btnDeleteInventory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white" />
android:textColor="@color/white"
android:visibility="gone"/>
</LinearLayout>
@@ -51,52 +52,51 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
android:padding="16dp">
<!-- Inventory ID — edit mode only -->
<TextView
android:id="@+id/tvInventoryId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ID: #0"
android:text="Inventory ID: "
android:textColor="@color/text_light"
android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
android:layout_marginBottom="12dp"
android:visibility="gone"/>
<!-- Product search label -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Item Name"
android:text="Product"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etItemName"
<!-- AutoComplete search box -->
<AutoCompleteTextView
android:id="@+id/etProductSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter item name"
android:hint="Search product name"
android:inputType="text"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Category"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:completionThreshold="1"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etInventoryCategory"
<!-- Selected product info (ID + category) shown after picking -->
<TextView
android:id="@+id/tvProductInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="e.g. Food, Toys, Medicine"
android:inputType="text"
android:layout_marginBottom="16dp"/>
android:textColor="#888888"
android:textSize="12sp"
android:layout_marginBottom="16dp"
android:visibility="gone"/>
<!-- Quantity label -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -105,44 +105,13 @@
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<!-- Quantity input -->
<EditText
android:id="@+id/etQuantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter quantity"
android:inputType="number"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Unit Price"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etUnitPrice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter unit price"
android:inputType="numberDecimal"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Supplier"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etInventorySupplier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter supplier name"
android:inputType="text"/>
android:inputType="number"/>
</LinearLayout>
@@ -150,6 +119,7 @@
</ScrollView>
<!-- Bottom buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@@ -2,80 +2,84 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:orientation="horizontal"
android:background="@color/white"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="@color/white">
android:paddingTop="12dp"
android:paddingBottom="12dp">
<!-- Checkbox (visible only in bulk-delete selection mode) -->
<CheckBox
android:id="@+id/cbSelectInventory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:visibility="gone"
android:clickable="false"
android:focusable="false"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
android:orientation="vertical">
<!-- Row 1: Product Name (most prominent — like desktop table header) -->
<TextView
android:id="@+id/tvItemName"
android:layout_width="0dp"
android:id="@+id/tvProductName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="Item Name"
android:text="Product Name"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textSize="17sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/tvUnitPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$0.00"
android:textColor="@color/accent_coral"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/tvCategory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Category"
android:textColor="#888888"
android:textSize="13sp" />
<!-- Row 2: Inventory ID | Product ID -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="8dp">
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tvQuantity"
android:id="@+id/tvInventoryId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Qty: 0"
android:textSize="13sp" />
android:text="Inv ID: "
android:textColor="#888888"
android:textSize="12sp"/>
<TextView
android:id="@+id/tvInvSupplier"
android:layout_width="wrap_content"
android:id="@+id/tvProdId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Supplier: "
android:layout_weight="1"
android:text="Prod ID: —"
android:textColor="#888888"
android:textSize="13sp" />
android:textSize="12sp"/>
</LinearLayout>
<!-- Row 3: Quantity -->
<TextView
android:id="@+id/tvQuantity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="0"
android:textSize="14sp"
android:textStyle="bold"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"
android:layout_marginTop="12dp"/>
android:layout_marginTop="10dp"/>
</LinearLayout>
</LinearLayout>