diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java new file mode 100644 index 00000000..4c6377e1 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java @@ -0,0 +1,55 @@ +package com.example.petstoremobile.adapters; + +import android.view.*; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.dtos.ProductSupplierDTO; +import java.util.List; + +public class ProductSupplierAdapter extends RecyclerView.Adapter { + + private List list; + private OnProductSupplierClickListener listener; + + public interface OnProductSupplierClickListener { + void onProductSupplierClick(int position); + } + + public ProductSupplierAdapter(List list, OnProductSupplierClickListener listener) { + this.list = list; + this.listener = listener; + } + + public static class PSViewHolder extends RecyclerView.ViewHolder { + TextView tvProductName, tvSupplierName, tvCost; + + public PSViewHolder(@NonNull View v) { + super(v); + tvProductName = v.findViewById(R.id.tvPSProductName); + tvSupplierName = v.findViewById(R.id.tvPSSupplierName); + tvCost = v.findViewById(R.id.tvPSCost); + } + } + + @NonNull + @Override + public PSViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_product_supplier, parent, false); + return new PSViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull PSViewHolder holder, int position) { + ProductSupplierDTO ps = list.get(position); + holder.tvProductName.setText(ps.getProductName() != null ? ps.getProductName() : ""); + holder.tvSupplierName.setText("Supplier: " + (ps.getSupplierName() != null ? ps.getSupplierName() : "")); + holder.tvCost.setText(ps.getCost() != null ? "Cost: $" + ps.getCost() : ""); + holder.itemView.setOnClickListener(v -> listener.onProductSupplierClick(position)); + } + + @Override + public int getItemCount() { return list.size(); } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java new file mode 100644 index 00000000..32810b12 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java @@ -0,0 +1,28 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductSupplierDTO; +import retrofit2.Call; +import retrofit2.http.*; + +public interface ProductSupplierApi { + + @GET("api/v1/product-suppliers") + Call> getAllProductSuppliers( + @Query("page") int page, + @Query("size") int size); + + @POST("api/v1/product-suppliers") + Call createProductSupplier(@Body ProductSupplierDTO dto); + + @PUT("api/v1/product-suppliers/{productId}/{supplierId}") + Call updateProductSupplier( + @Path("productId") Long productId, + @Path("supplierId") Long supplierId, + @Body ProductSupplierDTO dto); + + @DELETE("api/v1/product-suppliers/{productId}/{supplierId}") + Call deleteProductSupplier( + @Path("productId") Long productId, + @Path("supplierId") Long supplierId); +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ProductSupplierDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ProductSupplierDTO.java new file mode 100644 index 00000000..887d29e1 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ProductSupplierDTO.java @@ -0,0 +1,48 @@ +package com.example.petstoremobile.dtos; + +import java.math.BigDecimal; + +public class ProductSupplierDTO { + private Long productId; + private String productName; + private Long supplierId; + private String supplierName; + private BigDecimal cost; + private String createdAt; + private String updatedAt; + + // Constructor for create/update + public ProductSupplierDTO(Long productId, Long supplierId, BigDecimal cost) { + this.productId = productId; + this.supplierId = supplierId; + this.cost = cost; + } + + public Long getProductId() { + return productId; + } + + public String getProductName() { + return productName; + } + + public Long getSupplierId() { + return supplierId; + } + + public String getSupplierName() { + return supplierName; + } + + public BigDecimal getCost() { + return cost; + } + + public String getCreatedAt() { + return createdAt; + } + + public String getUpdatedAt() { + return updatedAt; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java new file mode 100644 index 00000000..4ff88f16 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -0,0 +1,134 @@ +package com.example.petstoremobile.fragments.listfragments; + +import android.os.Bundle; +import android.text.*; +import android.util.Log; +import android.view.*; +import android.widget.*; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.ProductSupplierAdapter; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductSupplierDTO; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductSupplierDetailFragment; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import java.util.*; +import retrofit2.*; + +public class ProductSupplierFragment extends Fragment + implements ProductSupplierAdapter.OnProductSupplierClickListener { + + private List psList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); + private ProductSupplierAdapter adapter; + private SwipeRefreshLayout swipeRefresh; + private EditText etSearch; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_product_supplier, container, false); + + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); + loadData(); + + FloatingActionButton fab = view.findViewById(R.id.fabAddPS); + fab.setOnClickListener(v -> openDetail(-1)); + + ImageButton hamburger = view.findViewById(R.id.btnHamburgerPS); + hamburger.setOnClickListener(v -> { + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.openDrawer(); + }); + + return view; + } + + private void setupRecyclerView(View view) { + RecyclerView rv = view.findViewById(R.id.recyclerViewPS); + adapter = new ProductSupplierAdapter(filteredList, this); + rv.setLayoutManager(new LinearLayoutManager(getContext())); + rv.setAdapter(adapter); + } + + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchPS); + etSearch.addTextChangedListener(new TextWatcher() { + public void beforeTextChanged(CharSequence s, int a, int b, int c) {} + public void afterTextChanged(Editable s) {} + public void onTextChanged(CharSequence s, int a, int b, int c) { + filter(s.toString()); + } + }); + } + + private void setupSwipeRefresh(View view) { + swipeRefresh = view.findViewById(R.id.swipeRefreshPS); + swipeRefresh.setOnRefreshListener(this::loadData); + } + + private void filter(String query) { + filteredList.clear(); + if (query.isEmpty()) { + filteredList.addAll(psList); + } else { + String lower = query.toLowerCase(); + for (ProductSupplierDTO ps : psList) { + if ((ps.getProductName() != null && ps.getProductName().toLowerCase().contains(lower)) + || (ps.getSupplierName() != null && ps.getSupplierName().toLowerCase().contains(lower))) { + filteredList.add(ps); + } + } + } + adapter.notifyDataSetChanged(); + } + + private void loadData() { + if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + RetrofitClient.getProductSupplierApi(requireContext()).getAllProductSuppliers(0, 100) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + if (r.isSuccessful() && r.body() != null) { + psList.clear(); + psList.addAll(r.body().getContent()); + filter(etSearch != null ? etSearch.getText().toString() : ""); + } else { + Toast.makeText(getContext(), "Failed to load", + Toast.LENGTH_SHORT).show(); + } + } + public void onFailure(Call> c, Throwable t) { + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + Log.e("PSFragment", t.getMessage()); + } + }); + } + + private void openDetail(int position) { + ProductSupplierDetailFragment detail = new ProductSupplierDetailFragment(); + Bundle args = new Bundle(); + if (position != -1) { + ProductSupplierDTO ps = filteredList.get(position); + args.putLong("productId", ps.getProductId()); + args.putLong("supplierId", ps.getSupplierId()); + args.putString("productName", ps.getProductName()); + args.putString("supplierName", ps.getSupplierName()); + args.putString("cost", ps.getCost() != null ? ps.getCost().toString() : ""); + } + detail.setArguments(args); + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.loadFragment(detail); + } + + @Override + public void onProductSupplierClick(int position) { openDetail(position); } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java new file mode 100644 index 00000000..729ae49d --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -0,0 +1,220 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +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 com.example.petstoremobile.R; +import com.example.petstoremobile.api.*; +import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.fragments.ListFragment; +import java.math.BigDecimal; +import java.util.*; +import retrofit2.*; + +public class ProductSupplierDetailFragment extends Fragment { + + private TextView tvMode; + private Spinner spinnerProduct, spinnerSupplier; + private EditText etCost; + private Button btnSave, btnDelete, btnBack; + + private boolean isEditing = false; + private long editProductId = -1; + private long editSupplierId = -1; + private long preselectedProductId = -1; + private long preselectedSupplierId = -1; + + private List productList = new ArrayList<>(); + private List supplierList = new ArrayList<>(); + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_product_supplier_detail, container, false); + initViews(view); + loadData(); + handleArguments(); + + btnBack.setOnClickListener(v -> navigateBack()); + btnSave.setOnClickListener(v -> save()); + btnDelete.setOnClickListener(v -> confirmDelete()); + return view; + } + + private void initViews(View v) { + tvMode = v.findViewById(R.id.tvPSMode); + spinnerProduct = v.findViewById(R.id.spinnerPSProduct); + spinnerSupplier = v.findViewById(R.id.spinnerPSSupplier); + etCost = v.findViewById(R.id.etPSCost); + btnSave = v.findViewById(R.id.btnSavePS); + btnDelete = v.findViewById(R.id.btnDeletePS); + btnBack = v.findViewById(R.id.btnPSBack); + } + + private void loadData() { + loadProducts(); + loadSuppliers(); + } + + private void loadProducts() { + RetrofitClient.getProductApi(requireContext()).getAllProducts(null, 0, 200) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (r.isSuccessful() && r.body() != null) { + productList = r.body().getContent(); + populateProductSpinner(); + } + } + public void onFailure(Call> c, Throwable t) { + Log.e("PSDetail", "Product load failed: " + t.getMessage()); + } + }); + } + + private void populateProductSpinner() { + List names = new ArrayList<>(); + names.add("-- Select Product --"); + for (ProductDTO p : productList) names.add(p.getProdName()); + spinnerProduct.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + if (preselectedProductId != -1) { + for (int i = 0; i < productList.size(); i++) { + if (productList.get(i).getProdId().equals(preselectedProductId)) { + spinnerProduct.setSelection(i + 1); break; + } + } + } + } + + private void loadSuppliers() { + RetrofitClient.getSupplierApi(requireContext()).getAllSuppliers(0, 200) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (r.isSuccessful() && r.body() != null) { + supplierList = r.body().getContent(); + populateSupplierSpinner(); + } + } + public void onFailure(Call> c, Throwable t) { + Log.e("PSDetail", "Supplier load failed: " + t.getMessage()); + } + }); + } + + private void populateSupplierSpinner() { + List names = new ArrayList<>(); + names.add("-- Select Supplier --"); + for (SupplierDTO s : supplierList) names.add(s.getSupCompany()); + spinnerSupplier.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + if (preselectedSupplierId != -1) { + for (int i = 0; i < supplierList.size(); i++) { + if (supplierList.get(i).getSupId().equals(preselectedSupplierId)) { + spinnerSupplier.setSelection(i + 1); break; + } + } + } + } + + private void handleArguments() { + Bundle a = getArguments(); + if (a != null && a.containsKey("productId")) { + isEditing = true; + editProductId = a.getLong("productId"); + editSupplierId = a.getLong("supplierId"); + preselectedProductId = editProductId; + preselectedSupplierId = editSupplierId; + etCost.setText(a.getString("cost")); + tvMode.setText("Edit Product Supplier"); + btnDelete.setVisibility(View.VISIBLE); + } else { + tvMode.setText("Add Product Supplier"); + btnDelete.setVisibility(View.GONE); + } + } + + private void save() { + if (spinnerProduct.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return; + } + if (spinnerSupplier.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a supplier", Toast.LENGTH_SHORT).show(); return; + } + String costStr = etCost.getText().toString().trim(); + if (costStr.isEmpty()) { + etCost.setError("Enter cost"); return; + } + + ProductDTO product = productList.get(spinnerProduct.getSelectedItemPosition() - 1); + SupplierDTO supplier = supplierList.get(spinnerSupplier.getSelectedItemPosition() - 1); + BigDecimal cost; + try { + cost = new BigDecimal(costStr); + } catch (Exception e) { + etCost.setError("Invalid cost"); return; + } + + ProductSupplierDTO dto = new ProductSupplierDTO( + product.getProdId(), supplier.getSupId(), cost); + + ProductSupplierApi api = RetrofitClient.getProductSupplierApi(requireContext()); + if (isEditing) { + api.updateProductSupplier(editProductId, editSupplierId, dto) + .enqueue(simpleCallback("Updated")); + } else { + api.createProductSupplier(dto).enqueue(simpleCallback("Saved")); + } + } + + private Callback simpleCallback(String msg) { + return new Callback<>() { + public void onResponse(Call c, Response r) { + if (r.isSuccessful()) { + Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + try { + String err = r.errorBody().string(); + Log.e("PS_SAVE", "Error: " + err); + Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + Log.e("PS_SAVE", "Failed to read error"); + } + } + } + public void onFailure(Call c, Throwable t) { + Log.e("PS_SAVE", "Failure: " + t.getMessage()); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }; + } + + private void confirmDelete() { + new AlertDialog.Builder(requireContext()) + .setTitle("Delete?") + .setPositiveButton("Yes", (d, w) -> + RetrofitClient.getProductSupplierApi(requireContext()) + .deleteProductSupplier(editProductId, editSupplierId) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { + navigateBack(); + } + public void onFailure(Call 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(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/models/ProductSupplier.java b/android/app/src/main/java/com/example/petstoremobile/models/ProductSupplier.java new file mode 100644 index 00000000..b624b5e4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/models/ProductSupplier.java @@ -0,0 +1,49 @@ +package com.example.petstoremobile.models; + +public class ProductSupplier { + private int supId; + private int prodId; + private String supCompany; + private String prodName; + private double cost; + + public ProductSupplier(int supId, int prodId, String supCompany, String prodName, double cost) { + this.supId = supId; + this.prodId = prodId; + this.supCompany = supCompany; + this.prodName = prodName; + this.cost = cost; + } + + public int getSupId() { + return supId; + } + + public int getProdId() { + return prodId; + } + + public String getSupCompany() { + return supCompany; + } + + public String getProdName() { + return prodName; + } + + public double getCost() { + return cost; + } + + public void setSupCompany(String supCompany) { + this.supCompany = supCompany; + } + + public void setProdName(String prodName) { + this.prodName = prodName; + } + + public void setCost(double cost) { + this.cost = cost; + } +} diff --git a/android/app/src/main/res/layout/fragment_product_supplier.xml b/android/app/src/main/res/layout/fragment_product_supplier.xml new file mode 100644 index 00000000..6924c36d --- /dev/null +++ b/android/app/src/main/res/layout/fragment_product_supplier.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_product_supplier_detail.xml b/android/app/src/main/res/layout/fragment_product_supplier_detail.xml new file mode 100644 index 00000000..b25db019 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_product_supplier_detail.xml @@ -0,0 +1,138 @@ + + + + + + + +