Product files
loads details of product, categories and cost of it
This commit is contained in:
@@ -1,72 +1,57 @@
|
|||||||
package com.example.petstoremobile.adapters;
|
package com.example.petstoremobile.adapters;
|
||||||
|
|
||||||
|
import android.view.*;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.models.Product;
|
import com.example.petstoremobile.dtos.ProductDTO;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> {
|
public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> {
|
||||||
|
|
||||||
private List<Product> productList;
|
private List<ProductDTO> productList;
|
||||||
private OnProductClickListener productClickListener;
|
private OnProductClickListener listener;
|
||||||
|
|
||||||
// Interface for product click on recycler view
|
|
||||||
public interface OnProductClickListener {
|
public interface OnProductClickListener {
|
||||||
void onProductClick(int position);
|
void onProductClick(int position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructor
|
public ProductAdapter(List<ProductDTO> productList, OnProductClickListener listener) {
|
||||||
public ProductAdapter(List<Product> productList, OnProductClickListener productClickListener) {
|
|
||||||
this.productList = productList;
|
this.productList = productList;
|
||||||
this.productClickListener = productClickListener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the controls of each row in recycler view
|
|
||||||
public static class ProductViewHolder extends RecyclerView.ViewHolder {
|
public static class ProductViewHolder extends RecyclerView.ViewHolder {
|
||||||
TextView tvProductName, tvProductDesc, tvCategory, tvProductPrice, tvStockQuantity;
|
TextView tvName, tvCategory, tvDesc, tvPrice;
|
||||||
|
|
||||||
public ProductViewHolder(@NonNull View v) {
|
public ProductViewHolder(@NonNull View v) {
|
||||||
super(v);
|
super(v);
|
||||||
tvProductName = v.findViewById(R.id.tvProductName);
|
tvName = v.findViewById(R.id.tvProductName);
|
||||||
tvProductDesc = v.findViewById(R.id.tvProductDesc);
|
|
||||||
tvCategory = v.findViewById(R.id.tvProductCategory);
|
tvCategory = v.findViewById(R.id.tvProductCategory);
|
||||||
tvProductPrice = v.findViewById(R.id.tvProductPrice);
|
tvDesc = v.findViewById(R.id.tvProductDesc);
|
||||||
tvStockQuantity = v.findViewById(R.id.tvStockQuantity);
|
tvPrice = v.findViewById(R.id.tvProductPrice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new row view
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_product, parent, false);
|
View v = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_product, parent, false);
|
||||||
return new ProductViewHolder(v);
|
return new ProductViewHolder(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate the row with product data
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) {
|
||||||
Product product = productList.get(position);
|
ProductDTO p = productList.get(position);
|
||||||
|
holder.tvName.setText(p.getProdName() != null ? p.getProdName() : "");
|
||||||
holder.tvProductName.setText(product.getProductName());
|
holder.tvCategory.setText("Category: " + (p.getCategoryName() != null ? p.getCategoryName() : ""));
|
||||||
holder.tvProductDesc.setText(product.getProductDesc());
|
holder.tvDesc.setText(p.getProdDesc() != null ? p.getProdDesc() : "");
|
||||||
holder.tvCategory.setText(product.getCategory());
|
holder.tvPrice.setText(p.getProdPrice() != null ? "$" + p.getProdPrice() : "");
|
||||||
holder.tvProductPrice.setText("$" + String.format("%.2f", product.getProductPrice()));
|
holder.itemView.setOnClickListener(v -> listener.onProductClick(position));
|
||||||
holder.tvStockQuantity.setText("Stock: " + product.getStockQuantity());
|
|
||||||
|
|
||||||
// When a row is clicked, open the detail view
|
|
||||||
holder.itemView.setOnClickListener(v -> productClickListener.onProductClick(position));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() { return productList.size(); }
|
||||||
return productList.size();
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.example.petstoremobile.api;
|
||||||
|
|
||||||
|
import com.example.petstoremobile.dtos.PageResponse;
|
||||||
|
import com.example.petstoremobile.dtos.ProductDTO;
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.http.*;
|
||||||
|
|
||||||
|
public interface ProductApi {
|
||||||
|
|
||||||
|
@GET("api/v1/products")
|
||||||
|
Call<PageResponse<ProductDTO>> getAllProducts(
|
||||||
|
@Query("q") String query,
|
||||||
|
@Query("page") int page,
|
||||||
|
@Query("size") int size);
|
||||||
|
|
||||||
|
@GET("api/v1/products/{id}")
|
||||||
|
Call<ProductDTO> getProductById(@Path("id") Long id);
|
||||||
|
|
||||||
|
@POST("api/v1/products")
|
||||||
|
Call<ProductDTO> createProduct(@Body ProductDTO product);
|
||||||
|
|
||||||
|
@PUT("api/v1/products/{id}")
|
||||||
|
Call<ProductDTO> updateProduct(@Path("id") Long id, @Body ProductDTO product);
|
||||||
|
|
||||||
|
@DELETE("api/v1/products/{id}")
|
||||||
|
Call<Void> deleteProduct(@Path("id") Long id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.example.petstoremobile.dtos;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public class ProductDTO {
|
||||||
|
private Long prodId;
|
||||||
|
private String prodName;
|
||||||
|
private Long categoryId;
|
||||||
|
private String categoryName;
|
||||||
|
private String prodDesc;
|
||||||
|
private BigDecimal prodPrice;
|
||||||
|
private String createdAt;
|
||||||
|
private String updatedAt;
|
||||||
|
|
||||||
|
public ProductDTO() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor for create/update
|
||||||
|
public ProductDTO(String prodName, Long categoryId, String prodDesc, BigDecimal prodPrice) {
|
||||||
|
this.prodName = prodName;
|
||||||
|
this.categoryId = categoryId;
|
||||||
|
this.prodDesc = prodDesc;
|
||||||
|
this.prodPrice = prodPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getProdId() {
|
||||||
|
return prodId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProdId(Long prodId) {
|
||||||
|
this.prodId = prodId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProdName() {
|
||||||
|
return prodName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProdName(String prodName) {
|
||||||
|
this.prodName = prodName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getCategoryId() {
|
||||||
|
return categoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCategoryId(Long categoryId) {
|
||||||
|
this.categoryId = categoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCategoryName() {
|
||||||
|
return categoryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCategoryName(String categoryName) {
|
||||||
|
this.categoryName = categoryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProdDesc() {
|
||||||
|
return prodDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProdDesc(String prodDesc) {
|
||||||
|
this.prodDesc = prodDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getProdPrice() {
|
||||||
|
return prodPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProdPrice(BigDecimal prodPrice) {
|
||||||
|
this.prodPrice = prodPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,88 +1,87 @@
|
|||||||
package com.example.petstoremobile.fragments.listfragments;
|
package com.example.petstoremobile.fragments.listfragments;
|
||||||
|
|
||||||
// Added search/filter bar to filter products by name or category.
|
|
||||||
// Added pull-to-refresh using SwipeRefreshLayout.
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.*;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.*;
|
||||||
|
import android.widget.*;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
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.R;
|
||||||
import com.example.petstoremobile.adapters.ProductAdapter;
|
import com.example.petstoremobile.adapters.ProductAdapter;
|
||||||
|
import com.example.petstoremobile.api.RetrofitClient;
|
||||||
|
import com.example.petstoremobile.dtos.PageResponse;
|
||||||
|
import com.example.petstoremobile.dtos.ProductDTO;
|
||||||
import com.example.petstoremobile.fragments.ListFragment;
|
import com.example.petstoremobile.fragments.ListFragment;
|
||||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductDetailFragment;
|
import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductDetailFragment;
|
||||||
import com.example.petstoremobile.models.Product;
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.List;
|
import retrofit2.*;
|
||||||
|
|
||||||
public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener {
|
public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener {
|
||||||
|
|
||||||
private List<Product> productList = new ArrayList<>();
|
private List<ProductDTO> productList = new ArrayList<>();
|
||||||
private List<Product> filteredList = new ArrayList<>();
|
private List<ProductDTO> filteredList = new ArrayList<>();
|
||||||
private ProductAdapter adapter;
|
private ProductAdapter adapter;
|
||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefresh;
|
||||||
private EditText etSearch;
|
private EditText etSearch;
|
||||||
private ImageButton hamburger;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
View view = inflater.inflate(R.layout.fragment_product, container, false);
|
View view = inflater.inflate(R.layout.fragment_product, container, false);
|
||||||
|
|
||||||
hamburger = view.findViewById(R.id.btnHamburger);
|
|
||||||
|
|
||||||
loadProductData(); // TODO: Replace with actual API call when backend is ready
|
|
||||||
setupRecyclerView(view);
|
setupRecyclerView(view);
|
||||||
setupSearch(view);
|
setupSearch(view);
|
||||||
setupSwipeRefresh(view);
|
setupSwipeRefresh(view);
|
||||||
|
loadProducts();
|
||||||
|
|
||||||
FloatingActionButton fabAddProduct = view.findViewById(R.id.fabAddProduct);
|
FloatingActionButton fab = view.findViewById(R.id.fabAddProduct);
|
||||||
fabAddProduct.setOnClickListener(v -> openProductDetails(-1));
|
fab.setOnClickListener(v -> openDetail(-1));
|
||||||
|
|
||||||
//Make the hamburger button open the drawer from listFragment
|
ImageButton hamburger = view.findViewById(R.id.btnHamburgerProduct);
|
||||||
hamburger.setOnClickListener(v -> {
|
hamburger.setOnClickListener(v -> {
|
||||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
ListFragment lf = (ListFragment) getParentFragment();
|
||||||
//if list fragment is found then use its helper function to open the drawer
|
if (lf != null) lf.openDrawer();
|
||||||
if (listFragment != null) {
|
|
||||||
listFragment.openDrawer();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters products by name, description, or category
|
private void setupRecyclerView(View view) {
|
||||||
|
RecyclerView rv = view.findViewById(R.id.recyclerViewProducts);
|
||||||
|
adapter = new ProductAdapter(filteredList, this);
|
||||||
|
rv.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
|
rv.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
private void setupSearch(View view) {
|
private void setupSearch(View view) {
|
||||||
etSearch = view.findViewById(R.id.etSearchProduct);
|
etSearch = view.findViewById(R.id.etSearchProduct);
|
||||||
etSearch.addTextChangedListener(new TextWatcher() {
|
etSearch.addTextChangedListener(new TextWatcher() {
|
||||||
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
public void beforeTextChanged(CharSequence s, int a, int b, int c) {}
|
||||||
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
|
public void afterTextChanged(Editable s) {}
|
||||||
filterProducts(s.toString());
|
public void onTextChanged(CharSequence s, int a, int b, int c) {
|
||||||
|
filter(s.toString());
|
||||||
}
|
}
|
||||||
@Override public void afterTextChanged(Editable s) {}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void filterProducts(String query) {
|
private void setupSwipeRefresh(View view) {
|
||||||
|
swipeRefresh = view.findViewById(R.id.swipeRefreshProduct);
|
||||||
|
swipeRefresh.setOnRefreshListener(this::loadProducts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void filter(String query) {
|
||||||
filteredList.clear();
|
filteredList.clear();
|
||||||
if (query.isEmpty()) {
|
if (query.isEmpty()) {
|
||||||
filteredList.addAll(productList);
|
filteredList.addAll(productList);
|
||||||
} else {
|
} else {
|
||||||
String lower = query.toLowerCase();
|
String lower = query.toLowerCase();
|
||||||
for (Product p : productList) {
|
for (ProductDTO p : productList) {
|
||||||
if (p.getProductName().toLowerCase().contains(lower)
|
if ((p.getProdName() != null && p.getProdName().toLowerCase().contains(lower))
|
||||||
|| p.getCategory().toLowerCase().contains(lower)
|
|| (p.getCategoryName() != null && p.getCategoryName().toLowerCase().contains(lower))) {
|
||||||
|| p.getProductDesc().toLowerCase().contains(lower)) {
|
|
||||||
filteredList.add(p);
|
filteredList.add(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,73 +89,45 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
|||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSwipeRefresh(View view) {
|
private void loadProducts() {
|
||||||
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshProduct);
|
if (swipeRefresh != null) swipeRefresh.setRefreshing(true);
|
||||||
swipeRefreshLayout.setOnRefreshListener(() -> {
|
RetrofitClient.getProductApi(requireContext()).getAllProducts(null, 0, 100)
|
||||||
loadProductData(); // TODO: Replace with actual API call
|
.enqueue(new Callback<PageResponse<ProductDTO>>() {
|
||||||
filterProducts(etSearch.getText().toString());
|
public void onResponse(Call<PageResponse<ProductDTO>> c,
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
Response<PageResponse<ProductDTO>> r) {
|
||||||
});
|
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
|
||||||
|
if (r.isSuccessful() && r.body() != null) {
|
||||||
|
productList.clear();
|
||||||
|
productList.addAll(r.body().getContent());
|
||||||
|
filter(etSearch != null ? etSearch.getText().toString() : "");
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(), "Failed to load products",
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void onFailure(Call<PageResponse<ProductDTO>> c, Throwable t) {
|
||||||
|
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
|
||||||
|
Log.e("ProductFragment", t.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openProductDetails(int position) {
|
private void openDetail(int position) {
|
||||||
ProductDetailFragment detailFragment = new ProductDetailFragment();
|
ProductDetailFragment detail = new ProductDetailFragment();
|
||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
args.putInt("position", position);
|
|
||||||
|
|
||||||
if (position != -1) {
|
if (position != -1) {
|
||||||
Product product = filteredList.get(position);
|
ProductDTO p = filteredList.get(position);
|
||||||
int realPosition = productList.indexOf(product);
|
args.putLong("prodId", p.getProdId());
|
||||||
args.putInt("position", realPosition);
|
args.putString("prodName", p.getProdName());
|
||||||
args.putInt("productId", product.getProductId());
|
args.putString("prodDesc", p.getProdDesc() != null ? p.getProdDesc() : "");
|
||||||
args.putString("productName", product.getProductName());
|
args.putString("prodPrice", p.getProdPrice() != null ? p.getProdPrice().toString() : "");
|
||||||
args.putString("productDesc", product.getProductDesc());
|
args.putLong("categoryId", p.getCategoryId() != null ? p.getCategoryId() : -1);
|
||||||
args.putString("category", product.getCategory());
|
|
||||||
args.putDouble("productPrice", product.getProductPrice());
|
|
||||||
args.putInt("stockQuantity", product.getStockQuantity());
|
|
||||||
}
|
}
|
||||||
|
detail.setArguments(args);
|
||||||
detailFragment.setArguments(args);
|
ListFragment lf = (ListFragment) getParentFragment();
|
||||||
detailFragment.setProductFragment(this);
|
if (lf != null) lf.loadFragment(detail);
|
||||||
|
|
||||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
|
||||||
if (listFragment != null) listFragment.loadFragment(detailFragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onProductSaved(int position, Product product) {
|
|
||||||
if (position == -1) {
|
|
||||||
productList.add(product);
|
|
||||||
} else {
|
|
||||||
productList.set(position, product);
|
|
||||||
}
|
|
||||||
filterProducts(etSearch.getText().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onProductDeleted(int position) {
|
|
||||||
productList.remove(position);
|
|
||||||
filterProducts(etSearch.getText().toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onProductClick(int position) {
|
public void onProductClick(int position) { openDetail(position); }
|
||||||
openProductDetails(position);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void loadProductData() {
|
|
||||||
productList.clear();
|
|
||||||
productList.add(new Product(1, "Premium Dog Food", "High protein dry food for adult dogs", "Food", 45.99, 25));
|
|
||||||
productList.add(new Product(2, "Cat Toy Bundle", "Set of 5 interactive toys", "Toys", 19.99, 40));
|
|
||||||
productList.add(new Product(3, "Pet Shampoo", "Gentle formula for all breeds", "Grooming", 12.99, 60));
|
|
||||||
productList.add(new Product(4, "Dog Bed - Large", "Memory foam orthopedic bed", "Bedding", 89.99, 10));
|
|
||||||
productList.add(new Product(5, "Aquarium Starter Kit", "20-gallon tank with filter and light", "Aquatic", 129.99, 5));
|
|
||||||
filteredList.clear();
|
|
||||||
filteredList.addAll(productList);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupRecyclerView(View view) {
|
|
||||||
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewProducts);
|
|
||||||
adapter = new ProductAdapter(filteredList, this);
|
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +1,190 @@
|
|||||||
package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
||||||
|
|
||||||
// Uses InputValidator for detailed field validation and ActivityLogger to log all changes.
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.*;
|
||||||
|
import android.widget.*;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
|
import com.example.petstoremobile.api.*;
|
||||||
|
import com.example.petstoremobile.dtos.*;
|
||||||
import com.example.petstoremobile.fragments.ListFragment;
|
import com.example.petstoremobile.fragments.ListFragment;
|
||||||
import com.example.petstoremobile.fragments.listfragments.ProductFragment;
|
import java.math.BigDecimal;
|
||||||
import com.example.petstoremobile.models.Product;
|
import java.util.*;
|
||||||
import com.example.petstoremobile.utils.ActivityLogger;
|
import retrofit2.*;
|
||||||
import com.example.petstoremobile.utils.InputValidator;
|
|
||||||
|
|
||||||
public class ProductDetailFragment extends Fragment {
|
public class ProductDetailFragment extends Fragment {
|
||||||
|
|
||||||
private TextView tvMode, tvProductId;
|
private TextView tvMode, tvProductId;
|
||||||
private EditText etProductName, etProductDesc, etCategory, etProductPrice, etStockQuantity;
|
private EditText etProductName, etProductDesc, etProductPrice;
|
||||||
private Button btnSaveProduct, btnDeleteProduct, btnBack;
|
private Spinner spinnerCategory;
|
||||||
private int productId;
|
private Button btnSave, btnDelete, btnBack;
|
||||||
private int position;
|
|
||||||
private boolean isEditing = false;
|
|
||||||
private ProductFragment productFragment;
|
|
||||||
|
|
||||||
// Set the product fragment as parent so we refer back when save or delete is done
|
private long prodId = -1;
|
||||||
public void setProductFragment(ProductFragment fragment) {
|
private boolean isEditing = false;
|
||||||
this.productFragment = fragment;
|
private long preselectedCategoryId = -1;
|
||||||
}
|
|
||||||
|
private List<CategoryDTO> categoryList = new ArrayList<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
View view = inflater.inflate(R.layout.fragment_product_detail, container, false);
|
View view = inflater.inflate(R.layout.fragment_product_detail, container, false);
|
||||||
|
|
||||||
initViews(view);
|
initViews(view);
|
||||||
|
loadCategories();
|
||||||
handleArguments();
|
handleArguments();
|
||||||
|
|
||||||
btnBack.setOnClickListener(v -> {
|
btnBack.setOnClickListener(v -> navigateBack());
|
||||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
btnSave.setOnClickListener(v -> saveProduct());
|
||||||
if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
|
btnDelete.setOnClickListener(v -> confirmDelete());
|
||||||
});
|
|
||||||
btnSaveProduct.setOnClickListener(v -> saveProduct());
|
|
||||||
btnDeleteProduct.setOnClickListener(v -> deleteProduct());
|
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validates all fields using InputValidator, then saves the product
|
private void initViews(View v) {
|
||||||
private void saveProduct() {
|
tvMode = v.findViewById(R.id.tvProductMode);
|
||||||
if (!InputValidator.isNotEmpty(etProductName, "Product Name")) return;
|
tvProductId = v.findViewById(R.id.tvProductId);
|
||||||
if (!InputValidator.isNotEmpty(etProductDesc, "Description")) return;
|
etProductName = v.findViewById(R.id.etProductName);
|
||||||
if (!InputValidator.isNotEmpty(etCategory, "Category")) return;
|
etProductDesc = v.findViewById(R.id.etProductDesc);
|
||||||
if (!InputValidator.isPositiveDecimal(etProductPrice, "Price")) return;
|
etProductPrice = v.findViewById(R.id.etProductPrice);
|
||||||
if (!InputValidator.isPositiveInteger(etStockQuantity, "Stock Quantity")) return;
|
spinnerCategory = v.findViewById(R.id.spinnerProductCategory);
|
||||||
|
btnSave = v.findViewById(R.id.btnSaveProduct);
|
||||||
String productName = etProductName.getText().toString().trim();
|
btnDelete = v.findViewById(R.id.btnDeleteProduct);
|
||||||
String productDesc = etProductDesc.getText().toString().trim();
|
btnBack = v.findViewById(R.id.btnProductBack);
|
||||||
String category = etCategory.getText().toString().trim();
|
|
||||||
double productPrice = Double.parseDouble(etProductPrice.getText().toString().trim());
|
|
||||||
int stockQuantity = Integer.parseInt(etStockQuantity.getText().toString().trim());
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isEditing) {
|
|
||||||
// TODO: Replace with actual API PUT call when backend is ready
|
|
||||||
Product updated = new Product(productId, productName, productDesc, category, productPrice, stockQuantity);
|
|
||||||
if (productFragment != null) productFragment.onProductSaved(position, updated);
|
|
||||||
ActivityLogger.logChange(requireContext(), "Product", "UPDATED", productId);
|
|
||||||
Toast.makeText(getContext(), "Product updated.", Toast.LENGTH_SHORT).show();
|
|
||||||
} else {
|
|
||||||
// TODO: Replace with actual API POST call when backend is ready
|
|
||||||
Product newProduct = new Product(0, productName, productDesc, category, productPrice, stockQuantity);
|
|
||||||
if (productFragment != null) productFragment.onProductSaved(-1, newProduct);
|
|
||||||
ActivityLogger.log(requireContext(), "Added new Product: " + productName);
|
|
||||||
Toast.makeText(getContext(), "Product added.", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
|
||||||
if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
|
|
||||||
} catch (Exception e) {
|
|
||||||
ActivityLogger.logException(requireContext(), "ProductDetailFragment.saveProduct", e);
|
|
||||||
Toast.makeText(getContext(), "Error saving product.", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletes the product and logs the action
|
private void loadCategories() {
|
||||||
private void deleteProduct() {
|
RetrofitClient.getCategoryApi(requireContext()).getAllCategories(0, 100)
|
||||||
try {
|
.enqueue(new Callback<PageResponse<CategoryDTO>>() {
|
||||||
// TODO: Replace with actual API DELETE call when backend is ready
|
public void onResponse(Call<PageResponse<CategoryDTO>> c,
|
||||||
if (productFragment != null) productFragment.onProductDeleted(position);
|
Response<PageResponse<CategoryDTO>> r) {
|
||||||
ActivityLogger.logChange(requireContext(), "Product", "DELETED", productId);
|
if (r.isSuccessful() && r.body() != null) {
|
||||||
Toast.makeText(getContext(), "Product deleted.", Toast.LENGTH_SHORT).show();
|
categoryList = r.body().getContent();
|
||||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
populateCategorySpinner();
|
||||||
if (listFragment != null) listFragment.getChildFragmentManager().popBackStack();
|
}
|
||||||
} catch (Exception e) {
|
}
|
||||||
ActivityLogger.logException(requireContext(), "ProductDetailFragment.deleteProduct", e);
|
public void onFailure(Call<PageResponse<CategoryDTO>> c, Throwable t) {
|
||||||
Toast.makeText(getContext(), "Error deleting product.", Toast.LENGTH_SHORT).show();
|
Log.e("ProductDetail", "Category load failed: " + t.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateCategorySpinner() {
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
names.add("-- Select Category --");
|
||||||
|
for (CategoryDTO c : categoryList) names.add(c.getCategoryName());
|
||||||
|
spinnerCategory.setAdapter(new ArrayAdapter<>(requireContext(),
|
||||||
|
android.R.layout.simple_spinner_item, names));
|
||||||
|
if (preselectedCategoryId != -1) {
|
||||||
|
for (int i = 0; i < categoryList.size(); i++) {
|
||||||
|
if (categoryList.get(i).getCategoryId().equals(preselectedCategoryId)) {
|
||||||
|
spinnerCategory.setSelection(i + 1); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleArguments() {
|
private void handleArguments() {
|
||||||
if (getArguments() != null && getArguments().containsKey("productId")) {
|
Bundle a = getArguments();
|
||||||
|
if (a != null && a.containsKey("prodId")) {
|
||||||
isEditing = true;
|
isEditing = true;
|
||||||
productId = getArguments().getInt("productId");
|
prodId = a.getLong("prodId");
|
||||||
position = getArguments().getInt("position");
|
preselectedCategoryId = a.getLong("categoryId", -1);
|
||||||
|
|
||||||
tvMode.setText("Edit Product");
|
tvMode.setText("Edit Product");
|
||||||
tvProductId.setText("ID: " + productId);
|
tvProductId.setText("ID: " + prodId);
|
||||||
etProductName.setText(getArguments().getString("productName"));
|
tvProductId.setVisibility(View.VISIBLE);
|
||||||
etProductDesc.setText(getArguments().getString("productDesc"));
|
etProductName.setText(a.getString("prodName"));
|
||||||
etCategory.setText(getArguments().getString("category"));
|
etProductDesc.setText(a.getString("prodDesc"));
|
||||||
etProductPrice.setText(String.valueOf(getArguments().getDouble("productPrice")));
|
etProductPrice.setText(a.getString("prodPrice"));
|
||||||
etStockQuantity.setText(String.valueOf(getArguments().getInt("stockQuantity")));
|
btnDelete.setVisibility(View.VISIBLE);
|
||||||
btnDeleteProduct.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
} else {
|
||||||
isEditing = false;
|
|
||||||
tvMode.setText("Add Product");
|
tvMode.setText("Add Product");
|
||||||
|
btnDelete.setVisibility(View.GONE);
|
||||||
tvProductId.setVisibility(View.GONE);
|
tvProductId.setVisibility(View.GONE);
|
||||||
btnDeleteProduct.setVisibility(View.GONE);
|
|
||||||
btnSaveProduct.setText("Add");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initViews(View view) {
|
private void saveProduct() {
|
||||||
tvMode = view.findViewById(R.id.tvProductMode);
|
String name = etProductName.getText().toString().trim();
|
||||||
tvProductId = view.findViewById(R.id.tvProductId);
|
String desc = etProductDesc.getText().toString().trim();
|
||||||
etProductName = view.findViewById(R.id.etProductName);
|
String priceStr = etProductPrice.getText().toString().trim();
|
||||||
etProductDesc = view.findViewById(R.id.etProductDesc);
|
|
||||||
etCategory = view.findViewById(R.id.etProductCategory);
|
if (name.isEmpty()) {
|
||||||
etProductPrice = view.findViewById(R.id.etProductPrice);
|
etProductName.setError("Enter product name"); return;
|
||||||
etStockQuantity = view.findViewById(R.id.etStockQuantity);
|
}
|
||||||
btnSaveProduct = view.findViewById(R.id.btnSaveProduct);
|
if (spinnerCategory.getSelectedItemPosition() == 0) {
|
||||||
btnDeleteProduct = view.findViewById(R.id.btnDeleteProduct);
|
Toast.makeText(getContext(), "Select a category", Toast.LENGTH_SHORT).show(); return;
|
||||||
btnBack = view.findViewById(R.id.btnProductBack);
|
}
|
||||||
|
if (priceStr.isEmpty()) {
|
||||||
|
etProductPrice.setError("Enter price"); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoryDTO category = categoryList.get(spinnerCategory.getSelectedItemPosition() - 1);
|
||||||
|
BigDecimal price;
|
||||||
|
try {
|
||||||
|
price = new BigDecimal(priceStr);
|
||||||
|
} catch (Exception e) {
|
||||||
|
etProductPrice.setError("Invalid price"); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price);
|
||||||
|
|
||||||
|
Log.d("PRODUCT_SAVE", "name=" + name + " categoryId=" + category.getCategoryId()
|
||||||
|
+ " price=" + price);
|
||||||
|
|
||||||
|
ProductApi api = RetrofitClient.getProductApi(requireContext());
|
||||||
|
if (isEditing) {
|
||||||
|
api.updateProduct(prodId, dto).enqueue(simpleCallback("Updated"));
|
||||||
|
} else {
|
||||||
|
api.createProduct(dto).enqueue(simpleCallback("Saved"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private Callback<ProductDTO> simpleCallback(String msg) {
|
||||||
|
return new Callback<>() {
|
||||||
|
public void onResponse(Call<ProductDTO> c, Response<ProductDTO> r) {
|
||||||
|
if (r.isSuccessful()) {
|
||||||
|
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
|
||||||
|
navigateBack();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
String err = r.errorBody().string();
|
||||||
|
Log.e("PRODUCT_SAVE", "Error: " + err);
|
||||||
|
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("PRODUCT_SAVE", "Failed to read error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void onFailure(Call<ProductDTO> c, Throwable t) {
|
||||||
|
Log.e("PRODUCT_SAVE", "Failure: " + t.getMessage());
|
||||||
|
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void confirmDelete() {
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle("Delete Product?")
|
||||||
|
.setPositiveButton("Yes", (d, w) ->
|
||||||
|
RetrofitClient.getProductApi(requireContext())
|
||||||
|
.deleteProduct(prodId)
|
||||||
|
.enqueue(new Callback<Void>() {
|
||||||
|
public void onResponse(Call<Void> c, Response<Void> r) {
|
||||||
|
navigateBack();
|
||||||
|
}
|
||||||
|
public void onFailure(Call<Void> c, Throwable t) {
|
||||||
|
Toast.makeText(getContext(), "Delete failed",
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.setNegativeButton("No", null).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void navigateBack() {
|
||||||
|
ListFragment lf = (ListFragment) getParentFragment();
|
||||||
|
if (lf != null) lf.getChildFragmentManager().popBackStack();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/header"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="56dp"
|
android:layout_height="56dp"
|
||||||
android:background="@color/primary_dark"
|
android:background="@color/primary_dark"
|
||||||
@@ -21,7 +20,7 @@
|
|||||||
android:paddingEnd="16dp">
|
android:paddingEnd="16dp">
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btnHamburger"
|
android:id="@+id/btnHamburgerProduct"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:src="@drawable/baseline_menu_36"
|
android:src="@drawable/baseline_menu_36"
|
||||||
@@ -43,7 +42,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="8dp"
|
||||||
android:hint="Search by product name or category..."
|
android:hint="Search by name or category..."
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:drawableStart="@android:drawable/ic_menu_search"
|
android:drawableStart="@android:drawable/ic_menu_search"
|
||||||
android:drawablePadding="8dp"
|
android:drawablePadding="8dp"
|
||||||
|
|||||||
@@ -28,10 +28,9 @@
|
|||||||
android:id="@+id/btnDeleteProduct"
|
android:id="@+id/btnDeleteProduct"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:backgroundTint="@color/accent_coral"
|
android:backgroundTint="@color/accent_coral"
|
||||||
android:text="Delete"
|
android:text="Delete"
|
||||||
android:textColor="@color/white" />
|
android:textColor="@color/white"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -65,6 +64,7 @@
|
|||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
android:layout_marginBottom="8dp"/>
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
|
<!-- Product Name -->
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -81,6 +81,22 @@
|
|||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:layout_marginBottom="16dp"/>
|
android:layout_marginBottom="16dp"/>
|
||||||
|
|
||||||
|
<!-- Category -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Category"
|
||||||
|
android:textColor="@color/text_dark"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:layout_marginBottom="4dp"/>
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinnerProductCategory"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"/>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -93,27 +109,12 @@
|
|||||||
android:id="@+id/etProductDesc"
|
android:id="@+id/etProductDesc"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="16dp"
|
android:hint="Enter description"
|
||||||
android:hint="Enter product description"
|
|
||||||
android:inputType="textMultiLine"
|
android:inputType="textMultiLine"
|
||||||
android:minLines="1" />
|
android:minLines="2"
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Category"
|
|
||||||
android:textColor="@color/text_dark"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:layout_marginBottom="4dp"/>
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/etProductCategory"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="e.g. Food, Toys, Grooming"
|
|
||||||
android:inputType="text"
|
|
||||||
android:layout_marginBottom="16dp"/>
|
android:layout_marginBottom="16dp"/>
|
||||||
|
|
||||||
|
<!-- Price -->
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -126,24 +127,9 @@
|
|||||||
android:id="@+id/etProductPrice"
|
android:id="@+id/etProductPrice"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="Enter price"
|
android:hint="0.00"
|
||||||
android:inputType="numberDecimal"
|
android:inputType="numberDecimal"
|
||||||
android:layout_marginBottom="16dp"/>
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Stock Quantity"
|
|
||||||
android:textColor="@color/text_dark"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:layout_marginBottom="4dp"/>
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/etStockQuantity"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="Enter stock quantity"
|
|
||||||
android:inputType="number"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user