diff --git a/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java new file mode 100644 index 00000000..a4e5e770 --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java @@ -0,0 +1,72 @@ +package com.example.petstoremobile.adapters; + + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.models.Product; +import java.util.List; + +public class ProductAdapter extends RecyclerView.Adapter { + + private List productList; + private OnProductClickListener productClickListener; + + // Interface for product click on recycler view + public interface OnProductClickListener { + void onProductClick(int position); + } + + // Constructor + public ProductAdapter(List productList, OnProductClickListener productClickListener) { + this.productList = productList; + this.productClickListener = productClickListener; + } + + // Get the controls of each row in recycler view + public static class ProductViewHolder extends RecyclerView.ViewHolder { + TextView tvProductName, tvProductDesc, tvCategory, tvProductPrice, tvStockQuantity; + + public ProductViewHolder(@NonNull View v) { + super(v); + tvProductName = v.findViewById(R.id.tvProductName); + tvProductDesc = v.findViewById(R.id.tvProductDesc); + tvCategory = v.findViewById(R.id.tvProductCategory); + tvProductPrice = v.findViewById(R.id.tvProductPrice); + tvStockQuantity = v.findViewById(R.id.tvStockQuantity); + } + } + + // Create a new row view + @NonNull + @Override + public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_product, parent, false); + return new ProductViewHolder(v); + } + + // Populate the row with product data + @Override + public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) { + Product product = productList.get(position); + + holder.tvProductName.setText(product.getProductName()); + holder.tvProductDesc.setText(product.getProductDesc()); + holder.tvCategory.setText(product.getCategory()); + holder.tvProductPrice.setText("$" + String.format("%.2f", product.getProductPrice())); + holder.tvStockQuantity.setText("Stock: " + product.getStockQuantity()); + + // When a row is clicked, open the detail view + holder.itemView.setOnClickListener(v -> productClickListener.onProductClick(position)); + } + + @Override + public int getItemCount() { + return productList.size(); + } +} + diff --git a/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java new file mode 100644 index 00000000..14117a12 --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -0,0 +1,148 @@ +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 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 com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.ProductAdapter; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductDetailFragment; +import com.example.petstoremobile.models.Product; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import java.util.ArrayList; +import java.util.List; + +public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener { + + private List productList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); + private ProductAdapter adapter; + private SwipeRefreshLayout swipeRefreshLayout; + private EditText etSearch; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_product, container, false); + + loadProductData(); // TODO: Replace with actual API call when backend is ready + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); + + FloatingActionButton fabAddProduct = view.findViewById(R.id.fabAddProduct); + fabAddProduct.setOnClickListener(v -> openProductDetails(-1)); + + return view; + } + + // Filters products by name, description, or category + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchProduct); + 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) { + filterProducts(s.toString()); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + private void filterProducts(String query) { + filteredList.clear(); + if (query.isEmpty()) { + filteredList.addAll(productList); + } else { + String lower = query.toLowerCase(); + for (Product p : productList) { + if (p.getProductName().toLowerCase().contains(lower) + || p.getCategory().toLowerCase().contains(lower) + || p.getProductDesc().toLowerCase().contains(lower)) { + filteredList.add(p); + } + } + } + adapter.notifyDataSetChanged(); + } + + private void setupSwipeRefresh(View view) { + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshProduct); + swipeRefreshLayout.setOnRefreshListener(() -> { + loadProductData(); // TODO: Replace with actual API call + filterProducts(etSearch.getText().toString()); + swipeRefreshLayout.setRefreshing(false); + }); + } + + private void openProductDetails(int position) { + ProductDetailFragment detailFragment = new ProductDetailFragment(); + Bundle args = new Bundle(); + args.putInt("position", position); + + if (position != -1) { + Product product = filteredList.get(position); + int realPosition = productList.indexOf(product); + args.putInt("position", realPosition); + args.putInt("productId", product.getProductId()); + args.putString("productName", product.getProductName()); + args.putString("productDesc", product.getProductDesc()); + args.putString("category", product.getCategory()); + args.putDouble("productPrice", product.getProductPrice()); + args.putInt("stockQuantity", product.getStockQuantity()); + } + + detailFragment.setArguments(args); + detailFragment.setProductFragment(this); + + 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 + public void onProductClick(int 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); + } +} diff --git a/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java new file mode 100644 index 00000000..4179c4f5 --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -0,0 +1,139 @@ +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.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.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.ProductFragment; +import com.example.petstoremobile.models.Product; +import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.InputValidator; + +public class ProductDetailFragment extends Fragment { + + private TextView tvMode, tvProductId; + private EditText etProductName, etProductDesc, etCategory, etProductPrice, etStockQuantity; + private Button btnSaveProduct, btnDeleteProduct, btnBack; + private int productId; + 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 + public void setProductFragment(ProductFragment fragment) { + this.productFragment = fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_product_detail, container, false); + + initViews(view); + handleArguments(); + + btnBack.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + }); + btnSaveProduct.setOnClickListener(v -> saveProduct()); + btnDeleteProduct.setOnClickListener(v -> deleteProduct()); + + return view; + } + + // Validates all fields using InputValidator, then saves the product + private void saveProduct() { + if (!InputValidator.isNotEmpty(etProductName, "Product Name")) return; + if (!InputValidator.isNotEmpty(etProductDesc, "Description")) return; + if (!InputValidator.isNotEmpty(etCategory, "Category")) return; + if (!InputValidator.isPositiveDecimal(etProductPrice, "Price")) return; + if (!InputValidator.isPositiveInteger(etStockQuantity, "Stock Quantity")) return; + + String productName = etProductName.getText().toString().trim(); + String productDesc = etProductDesc.getText().toString().trim(); + 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 deleteProduct() { + try { + // TODO: Replace with actual API DELETE call when backend is ready + if (productFragment != null) productFragment.onProductDeleted(position); + ActivityLogger.logChange(requireContext(), "Product", "DELETED", productId); + Toast.makeText(getContext(), "Product deleted.", Toast.LENGTH_SHORT).show(); + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + } catch (Exception e) { + ActivityLogger.logException(requireContext(), "ProductDetailFragment.deleteProduct", e); + Toast.makeText(getContext(), "Error deleting product.", Toast.LENGTH_SHORT).show(); + } + } + + private void handleArguments() { + if (getArguments() != null && getArguments().containsKey("productId")) { + isEditing = true; + productId = getArguments().getInt("productId"); + position = getArguments().getInt("position"); + tvMode.setText("Edit Product"); + tvProductId.setText("ID: " + productId); + etProductName.setText(getArguments().getString("productName")); + etProductDesc.setText(getArguments().getString("productDesc")); + etCategory.setText(getArguments().getString("category")); + etProductPrice.setText(String.valueOf(getArguments().getDouble("productPrice"))); + etStockQuantity.setText(String.valueOf(getArguments().getInt("stockQuantity"))); + btnDeleteProduct.setVisibility(View.VISIBLE); + } else { + isEditing = false; + tvMode.setText("Add Product"); + tvProductId.setVisibility(View.GONE); + btnDeleteProduct.setVisibility(View.GONE); + btnSaveProduct.setText("Add"); + } + } + + private void initViews(View view) { + tvMode = view.findViewById(R.id.tvProductMode); + tvProductId = view.findViewById(R.id.tvProductId); + etProductName = view.findViewById(R.id.etProductName); + etProductDesc = view.findViewById(R.id.etProductDesc); + etCategory = view.findViewById(R.id.etProductCategory); + etProductPrice = view.findViewById(R.id.etProductPrice); + etStockQuantity = view.findViewById(R.id.etStockQuantity); + btnSaveProduct = view.findViewById(R.id.btnSaveProduct); + btnDeleteProduct = view.findViewById(R.id.btnDeleteProduct); + btnBack = view.findViewById(R.id.btnProductBack); + } +} diff --git a/app/src/main/java/com/example/petstoremobile/models/Product.java b/app/src/main/java/com/example/petstoremobile/models/Product.java new file mode 100644 index 00000000..90a56eab --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/models/Product.java @@ -0,0 +1,66 @@ +package com.example.petstoremobile.models; + + +public class Product { + private int productId; + private String productName; + private String productDesc; + private String category; + private double productPrice; + private int stockQuantity; + + // Constructor + public Product(int productId, String productName, String productDesc, String category, double productPrice, int stockQuantity) { + this.productId = productId; + this.productName = productName; + this.productDesc = productDesc; + this.category = category; + this.productPrice = productPrice; + this.stockQuantity = stockQuantity; + } + + // Getters and setters + public int getProductId() { + return productId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public String getProductDesc() { + return productDesc; + } + + public void setProductDesc(String productDesc) { + this.productDesc = productDesc; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public double getProductPrice() { + return productPrice; + } + + public void setProductPrice(double productPrice) { + this.productPrice = productPrice; + } + + public int getStockQuantity() { + return stockQuantity; + } + + public void setStockQuantity(int stockQuantity) { + this.stockQuantity = stockQuantity; + } +} diff --git a/app/src/main/res/layout/fragment_product.xml b/app/src/main/res/layout/fragment_product.xml new file mode 100644 index 00000000..47f4727b --- /dev/null +++ b/app/src/main/res/layout/fragment_product.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_product_detail.xml b/app/src/main/res/layout/fragment_product_detail.xml new file mode 100644 index 00000000..69aa4514 --- /dev/null +++ b/app/src/main/res/layout/fragment_product_detail.xml @@ -0,0 +1,154 @@ + + + + + + + + + +