Close chat #169
@@ -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<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
|
||||
public int getItemCount() {
|
||||
return inventoryList.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
} 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<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 {
|
||||
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<PageResponse<InventoryDTO>>() {
|
||||
@Override
|
||||
public void onResponse(Call<PageResponse<InventoryDTO>> call,
|
||||
Response<PageResponse<InventoryDTO>> response) {
|
||||
isLoading = false;
|
||||
if (swipeRefreshLayout != null)
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
|
||||
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 {
|
||||
Log.e(TAG, "Error " + response.code());
|
||||
Toast.makeText(getContext(), "Failed to load inventory", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"/>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvItemName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Item Name"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="18sp"
|
||||
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"
|
||||
<!-- 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_marginTop="4dp"
|
||||
android:text="Category"
|
||||
android:textColor="#888888"
|
||||
android:textSize="13sp" />
|
||||
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:layout_marginTop="8dp">
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Row 1: Product Name (most prominent — like desktop table header) -->
|
||||
<TextView
|
||||
android:id="@+id/tvProductName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Product Name"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<!-- Row 2: Inventory ID | Product ID -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvInventoryId"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Inv ID: —"
|
||||
android:textColor="#888888"
|
||||
android:textSize="12sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProdId"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Prod ID: —"
|
||||
android:textColor="#888888"
|
||||
android:textSize="12sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Row 3: Quantity -->
|
||||
<TextView
|
||||
android:id="@+id/tvQuantity"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Qty: 0"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvInvSupplier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Supplier: "
|
||||
android:textColor="#888888"
|
||||
android:textSize="13sp" />
|
||||
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="10dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#F0F0F0"
|
||||
android:layout_marginTop="12dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
Reference in New Issue
Block a user