From f26c795d467716e935a771954d8d2544d7835177 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:34:28 -0600 Subject: [PATCH 01/13] Added Customer CRUD --- .../adapters/CustomerAdapter.java | 80 ++++++ .../petstoremobile/api/CustomerApi.java | 16 +- .../petstoremobile/dtos/CustomerDTO.java | 89 ++++--- .../fragments/ListFragment.java | 1 + .../fragments/ProfileFragment.java | 3 +- .../listfragments/CustomerFragment.java | 125 +++++++++ .../CustomerDetailFragment.java | 175 ++++++++++++ .../repositories/CustomerRepository.java | 23 +- .../viewmodels/CustomerDetailViewModel.java | 49 ++++ .../viewmodels/CustomerListViewModel.java | 79 ++++++ .../src/main/res/layout/fragment_customer.xml | 131 +++++++++ .../res/layout/fragment_customer_detail.xml | 252 ++++++++++++++++++ .../app/src/main/res/layout/fragment_list.xml | 18 ++ .../app/src/main/res/layout/item_customer.xml | 117 ++++++++ .../main/res/navigation/list_nav_graph.xml | 12 + 15 files changed, 1118 insertions(+), 52 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/adapters/CustomerAdapter.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java create mode 100644 android/app/src/main/res/layout/fragment_customer.xml create mode 100644 android/app/src/main/res/layout/fragment_customer_detail.xml create mode 100644 android/app/src/main/res/layout/item_customer.xml diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/CustomerAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/CustomerAdapter.java new file mode 100644 index 00000000..30b08b23 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/CustomerAdapter.java @@ -0,0 +1,80 @@ +package com.example.petstoremobile.adapters; + +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.api.UserApi; +import com.example.petstoremobile.databinding.ItemCustomerBinding; +import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.utils.GlideUtils; + +import java.util.List; + +public class CustomerAdapter extends RecyclerView.Adapter { + + private List list; + private OnCustomerClickListener listener; + private String baseUrl; + private String token; + + public interface OnCustomerClickListener { + void onCustomerClick(int position); + } + + public CustomerAdapter(List list, OnCustomerClickListener listener) { + this.list = list; + this.listener = listener; + } + + public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } + public void setToken(String token) { this.token = token; } + + public static class CustomerViewHolder extends RecyclerView.ViewHolder { + final ItemCustomerBinding binding; + + public CustomerViewHolder(@NonNull ItemCustomerBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } + + @NonNull + @Override + public CustomerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemCustomerBinding binding = ItemCustomerBinding.inflate( + LayoutInflater.from(parent.getContext()), parent, false); + return new CustomerViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull CustomerViewHolder holder, int position) { + CustomerDTO c = list.get(position); + ItemCustomerBinding b = holder.binding; + + b.tvCustomerFullName.setText(c.getFullName() != null ? c.getFullName() : ""); + b.tvCustomerUsername.setText("@" + (c.getUsername() != null ? c.getUsername() : "")); + b.tvCustomerEmail.setText(c.getEmail() != null ? c.getEmail() : ""); + + int points = c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0; + b.tvCustomerLoyalty.setText(points + " pts"); + + boolean active = Boolean.TRUE.equals(c.getActive()); + b.tvCustomerStatus.setText(active ? "Active" : "Inactive"); + b.tvCustomerStatus.setTextColor(active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336")); + + if (baseUrl != null && c.getCustomerId() != null) { + String imageUrl = baseUrl + String.format(UserApi.AVATAR_PATH, c.getCustomerId()); + GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), b.ivCustomerProfile, imageUrl, token, R.drawable.placeholder); + } else { + b.ivCustomerProfile.setImageResource(R.drawable.placeholder); + } + + holder.itemView.setOnClickListener(v -> listener.onCustomerClick(position)); + } + + @Override + public int getItemCount() { return list.size(); } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java index 855ba5fa..55c51682 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java @@ -7,11 +7,14 @@ import com.example.petstoremobile.dtos.PageResponse; import java.util.List; 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; -//api calls to get customers public interface CustomerApi { @GET("api/v1/customers") @@ -20,6 +23,15 @@ public interface CustomerApi { @GET("api/v1/customers/{customerId}") Call getCustomerById(@Path("customerId") Long customerId); + @PUT("api/v1/customers/{customerId}") + Call updateCustomer(@Path("customerId") Long customerId, @Body CustomerDTO customer); + + @DELETE("api/v1/customers/{customerId}") + Call deleteCustomer(@Path("customerId") Long customerId); + + @POST("api/v1/auth/register") + Call registerCustomer(@Body CustomerDTO customer); + @GET("api/v1/dropdowns/customers") Call> getCustomerDropdowns(); -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java index 21376a63..52d96a1e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java @@ -5,61 +5,72 @@ import com.google.gson.annotations.SerializedName; public class CustomerDTO { @SerializedName("id") private Long customerId; + private String username; private String firstName; private String lastName; + private String fullName; private String email; + private String phone; + private Boolean active; + private Integer loyaltyPoints; + private Long primaryStoreId; private String createdAt; private String updatedAt; + private String password; - public Long getCustomerId() { - return customerId; - } + public CustomerDTO() {} - public void setCustomerId(Long customerId) { - this.customerId = customerId; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { + public CustomerDTO(String username, String password, String firstName, String lastName, + String email, String phone) { + this.username = username; + this.password = password; this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { this.lastName = lastName; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { this.email = email; + this.phone = phone; } + public Long getCustomerId() { return customerId; } + public void setCustomerId(Long customerId) { this.customerId = customerId; } + + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + public String getFullName() { - return firstName + " " + lastName; + if (fullName != null) return fullName; + String f = firstName != null ? firstName : ""; + String l = lastName != null ? lastName : ""; + return (f + " " + l).trim(); } + public void setFullName(String fullName) { this.fullName = fullName; } - public String getCreatedAt() { - return createdAt; - } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } - public void setCreatedAt(String createdAt) { - this.createdAt = createdAt; - } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } - public String getUpdatedAt() { - return updatedAt; - } + public Boolean getActive() { return active; } + public void setActive(Boolean active) { this.active = active; } - public void setUpdatedAt(String updatedAt) { - this.updatedAt = updatedAt; - } + public Integer getLoyaltyPoints() { return loyaltyPoints; } + public void setLoyaltyPoints(Integer loyaltyPoints) { this.loyaltyPoints = loyaltyPoints; } + + public Long getPrimaryStoreId() { return primaryStoreId; } + public void setPrimaryStoreId(Long primaryStoreId) { this.primaryStoreId = primaryStoreId; } + + 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; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java index e7180292..0aab247b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java @@ -89,6 +89,7 @@ public class ListFragment extends Fragment { binding.drawerPurchaseOrderView.setOnClickListener(v -> navigateTo(R.id.nav_purchase_order)); binding.drawerSale.setOnClickListener(v -> navigateTo(R.id.nav_sale)); binding.drawerStaff.setOnClickListener(v -> navigateTo(R.id.nav_staff)); + binding.drawerCustomers.setOnClickListener(v -> navigateTo(R.id.nav_customer)); binding.drawerAnalytics.setOnClickListener(v -> navigateTo(R.id.nav_analytics)); binding.drawerCoupons.setOnClickListener(v -> navigateTo(R.id.nav_coupon)); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index b31243c2..7f8fc039 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -160,7 +160,8 @@ public class ProfileFragment extends Fragment { android.content.Intent serviceIntent = new android.content.Intent(requireContext(), ChatNotificationService.class); requireContext().stopService(serviceIntent); - tokenManager.clearLoginData(); // clear the token for next login + // clear the token for next login + tokenManager.clearLoginData(); //get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen android.content.Intent intent = new android.content.Intent(getActivity(), MainActivity.class); intent.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP | android.content.Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java new file mode 100644 index 00000000..69fb967c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java @@ -0,0 +1,125 @@ +package com.example.petstoremobile.fragments.listfragments; + +import android.os.Bundle; +import android.view.*; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.fragment.NavHostFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.CustomerAdapter; +import com.example.petstoremobile.api.auth.TokenManager; +import com.example.petstoremobile.databinding.FragmentCustomerBinding; +import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.CustomerListViewModel; +import dagger.hilt.android.AndroidEntryPoint; +import java.util.*; + +import javax.inject.Inject; +import javax.inject.Named; + +@AndroidEntryPoint +public class CustomerFragment extends Fragment implements CustomerAdapter.OnCustomerClickListener { + + private FragmentCustomerBinding binding; + private CustomerListViewModel viewModel; + private List customerList = new ArrayList<>(); + private CustomerAdapter adapter; + + @Inject @Named("baseUrl") String baseUrl; + @Inject TokenManager tokenManager; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + binding = FragmentCustomerBinding.inflate(inflater, container, false); + viewModel = new ViewModelProvider(this).get(CustomerListViewModel.class); + + setupRecyclerView(); + setupSearch(); + setupStatusFilter(); + setupSwipeRefresh(); + observeViewModel(); + + viewModel.loadCustomers(); + + binding.fabAddCustomer.setOnClickListener(v -> openDetail(-1)); + + UIUtils.setupHamburgerMenu(binding.btnHamburgerCustomer, this); + UIUtils.setupFilterToggle(binding.btnToggleFilterCustomer, binding.layoutFilterCustomer, + binding.etSearchCustomer, binding.spinnerStatusCustomer); + + return binding.getRoot(); + } + + private void observeViewModel() { + viewModel.getFilteredCustomers().observe(getViewLifecycleOwner(), list -> { + customerList.clear(); + customerList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshCustomer.setRefreshing(loading); + }); + } + + private void setupRecyclerView() { + adapter = new CustomerAdapter(customerList, this); + adapter.setBaseUrl(baseUrl); + adapter.setToken(tokenManager.getToken()); + binding.recyclerViewCustomer.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewCustomer.setAdapter(adapter); + } + + private void setupStatusFilter() { + String[] statuses = {"All Statuses", "Active", "Inactive"}; + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCustomer, statuses, this::applyFilters); + } + + private void setupSearch() { + UIUtils.attachSearch(binding.etSearchCustomer, this::applyFilters); + } + + private void applyFilters() { + String query = binding.etSearchCustomer.getText().toString().trim(); + String status = binding.spinnerStatusCustomer.getSelectedItem() != null ? + binding.spinnerStatusCustomer.getSelectedItem().toString() : "All Statuses"; + viewModel.filter(query, status); + } + + private void setupSwipeRefresh() { + binding.swipeRefreshCustomer.setOnRefreshListener(viewModel::loadCustomers); + } + + private void openDetail(int position) { + Bundle args = new Bundle(); + if (position != -1) { + CustomerDTO c = customerList.get(position); + args.putLong("customerId", c.getCustomerId() != null ? c.getCustomerId() : -1); + args.putString("username", c.getUsername() != null ? c.getUsername() : ""); + args.putString("firstName", c.getFirstName() != null ? c.getFirstName() : ""); + args.putString("lastName", c.getLastName() != null ? c.getLastName() : ""); + args.putString("email", c.getEmail() != null ? c.getEmail() : ""); + args.putString("phone", c.getPhone() != null ? c.getPhone() : ""); + args.putBoolean("active", Boolean.TRUE.equals(c.getActive())); + args.putInt("loyaltyPoints", c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0); + args.putBoolean("isEditing", true); + } + NavHostFragment.findNavController(this).navigate(R.id.nav_customer_detail, args); + } + + @Override + public void onCustomerClick(int position) { + openDetail(position); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java new file mode 100644 index 00000000..f7a8c5a3 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java @@ -0,0 +1,175 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +import android.os.Bundle; +import android.view.*; +import android.widget.*; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.fragment.NavHostFragment; +import com.example.petstoremobile.databinding.FragmentCustomerDetailBinding; +import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.CustomerDetailViewModel; +import com.example.petstoremobile.utils.Resource; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class CustomerDetailFragment extends Fragment { + + private FragmentCustomerDetailBinding binding; + private CustomerDetailViewModel viewModel; + + private final String[] STATUSES = {"Active", "Inactive"}; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + binding = FragmentCustomerDetailBinding.inflate(inflater, container, false); + viewModel = new ViewModelProvider(this).get(CustomerDetailViewModel.class); + + setupSpinners(); + handleArguments(); + + binding.btnCustomerBack.setOnClickListener(v -> navigateBack()); + binding.btnSaveCustomer.setOnClickListener(v -> save()); + binding.btnDeleteCustomer.setOnClickListener(v -> confirmDelete()); + + UIUtils.formatPhoneInput(binding.etCustomerPhone); + + return binding.getRoot(); + } + + private void setupSpinners() { + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerCustomerStatus, STATUSES); + } + + private void handleArguments() { + Bundle a = getArguments(); + if (a != null && a.getBoolean("isEditing", false)) { + long customerId = a.getLong("customerId", -1); + viewModel.setCustomerId(customerId, true); + + binding.tvCustomerMode.setText("Edit Customer"); + binding.tvCustomerId.setText("ID: " + customerId); + binding.tvCustomerId.setVisibility(View.VISIBLE); + binding.btnDeleteCustomer.setVisibility(View.VISIBLE); + + // Hide password field in edit mode + binding.tvCustomerPasswordLabel.setVisibility(View.GONE); + binding.etCustomerPassword.setVisibility(View.GONE); + + // Show loyalty points + binding.tvLoyaltyPointsLabel.setVisibility(View.VISIBLE); + binding.tvCustomerLoyaltyPoints.setVisibility(View.VISIBLE); + + loadCustomerData(customerId); + } else { + viewModel.setCustomerId(-1, false); + binding.tvCustomerMode.setText("Add Customer"); + binding.btnDeleteCustomer.setVisibility(View.GONE); + binding.tvCustomerId.setVisibility(View.GONE); + binding.tvLoyaltyPointsLabel.setVisibility(View.GONE); + binding.tvCustomerLoyaltyPoints.setVisibility(View.GONE); + } + } + + private void loadCustomerData(long id) { + viewModel.loadCustomer(id).observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + CustomerDTO c = resource.data; + binding.etCustomerUsername.setText(c.getUsername() != null ? c.getUsername() : ""); + binding.etCustomerFirstName.setText(c.getFirstName() != null ? c.getFirstName() : ""); + binding.etCustomerLastName.setText(c.getLastName() != null ? c.getLastName() : ""); + binding.etCustomerEmail.setText(c.getEmail() != null ? c.getEmail() : ""); + binding.etCustomerPhone.setText(c.getPhone() != null ? c.getPhone() : ""); + binding.spinnerCustomerStatus.setSelection(Boolean.TRUE.equals(c.getActive()) ? 0 : 1); + int pts = c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0; + binding.tvCustomerLoyaltyPoints.setText(String.valueOf(pts)); + } + } + }); + } + + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + + private void save() { + if (!InputValidator.isNotEmpty(binding.etCustomerUsername, "Username")) return; + + if (!viewModel.isEditing()) { + if (!InputValidator.isNotEmpty(binding.etCustomerPassword, "Password")) return; + String pass = binding.etCustomerPassword.getText().toString(); + if (pass.length() < 6) { + binding.etCustomerPassword.setError("At least 6 characters"); + binding.etCustomerPassword.requestFocus(); + return; + } + } + + if (!InputValidator.isNotEmpty(binding.etCustomerFirstName, "First Name")) return; + if (!InputValidator.isNotEmpty(binding.etCustomerLastName, "Last Name")) return; + if (!InputValidator.isValidEmail(binding.etCustomerEmail)) return; + if (!InputValidator.isValidPhone(binding.etCustomerPhone)) return; + + String username = binding.etCustomerUsername.getText().toString().trim(); + String password = viewModel.isEditing() ? null : binding.etCustomerPassword.getText().toString().trim(); + String firstName = binding.etCustomerFirstName.getText().toString().trim(); + String lastName = binding.etCustomerLastName.getText().toString().trim(); + String email = binding.etCustomerEmail.getText().toString().trim(); + String phone = binding.etCustomerPhone.getText().toString().trim(); + boolean active = binding.spinnerCustomerStatus.getSelectedItemPosition() == 0; + + CustomerDTO dto = new CustomerDTO(username, password, firstName, lastName, email, phone); + dto.setFullName(firstName + " " + lastName); + dto.setActive(active); + + viewModel.saveCustomer(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), + viewModel.isEditing() ? "Updated successfully" : "Customer created", + Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); + } + } + }); + } + + private void confirmDelete() { + DialogUtils.showDeleteConfirmDialog(requireContext(), "Customer", () -> + viewModel.deleteCustomer().observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, + Toast.LENGTH_SHORT).show(); + } + } + })); + } + + private void navigateBack() { + NavHostFragment.findNavController(this).popBackStack(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java index 9fa9d2e6..65c9a79d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java @@ -23,24 +23,27 @@ public class CustomerRepository extends BaseRepository { this.customerApi = customerApi; } - /** - * Retrieves a paginated list of all customers from the API. - */ public LiveData>> getAllCustomers(int page, int size) { return executeCall(customerApi.getAllCustomers(page, size)); } - /** - * Retrieves a specific customer by their ID. - */ public LiveData> getCustomerById(Long id) { return executeCall(customerApi.getCustomerById(id)); } - /** - * Retrieves a list of customer dropdowns from the API. - */ + public LiveData> createCustomer(CustomerDTO dto) { + return executeCall(customerApi.registerCustomer(dto)); + } + + public LiveData> updateCustomer(Long id, CustomerDTO dto) { + return executeCall(customerApi.updateCustomer(id, dto)); + } + + public LiveData> deleteCustomer(Long id) { + return executeCall(customerApi.deleteCustomer(id)); + } + public LiveData>> getCustomerDropdowns() { return executeCall(customerApi.getCustomerDropdowns()); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java new file mode 100644 index 00000000..e7864b66 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java @@ -0,0 +1,49 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class CustomerDetailViewModel extends ViewModel { + private final CustomerRepository repository; + + private long customerId = -1; + private boolean isEditing = false; + + @Inject + public CustomerDetailViewModel(CustomerRepository repository) { + this.repository = repository; + } + + public void setCustomerId(long id, boolean isEditing) { + this.customerId = id; + this.isEditing = isEditing; + } + + public long getCustomerId() { return customerId; } + public boolean isEditing() { return isEditing; } + + public LiveData> loadCustomer(long id) { + return repository.getCustomerById(id); + } + + public LiveData> saveCustomer(CustomerDTO dto) { + if (isEditing && customerId > 0) { + return repository.updateCustomer(customerId, dto); + } else { + return repository.createCustomer(dto); + } + } + + public LiveData> deleteCustomer() { + return repository.deleteCustomer(customerId); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java new file mode 100644 index 00000000..241fc811 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java @@ -0,0 +1,79 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class CustomerListViewModel extends ViewModel { + private final CustomerRepository repository; + + private final MutableLiveData> customers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> filteredCustomers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private String lastQuery = ""; + private String lastStatus = "All Statuses"; + + @Inject + public CustomerListViewModel(CustomerRepository repository) { + this.repository = repository; + } + + public LiveData> getFilteredCustomers() { return filteredCustomers; } + public LiveData getIsLoading() { return isLoading; } + + public void loadCustomers() { + isLoading.setValue(true); + repository.getAllCustomers(0, 200).observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + customers.setValue(resource.data.getContent()); + filter(lastQuery, lastStatus); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void filter(String query, String status) { + this.lastQuery = query; + this.lastStatus = status; + + List all = customers.getValue(); + if (all == null) return; + + List filtered = new ArrayList<>(); + String lowerQuery = query.toLowerCase(); + + for (CustomerDTO c : all) { + boolean matchesQuery = query.isEmpty() || + (c.getFullName() != null && c.getFullName().toLowerCase().contains(lowerQuery)) || + (c.getUsername() != null && c.getUsername().toLowerCase().contains(lowerQuery)) || + (c.getEmail() != null && c.getEmail().toLowerCase().contains(lowerQuery)) || + (c.getPhone() != null && c.getPhone().toLowerCase().contains(lowerQuery)); + + boolean matchesStatus = status.equals("All Statuses") || + (status.equals("Active") && Boolean.TRUE.equals(c.getActive())) || + (status.equals("Inactive") && Boolean.FALSE.equals(c.getActive())); + + if (matchesQuery && matchesStatus) { + filtered.add(c); + } + } + filteredCustomers.setValue(filtered); + } +} diff --git a/android/app/src/main/res/layout/fragment_customer.xml b/android/app/src/main/res/layout/fragment_customer.xml new file mode 100644 index 00000000..6244babf --- /dev/null +++ b/android/app/src/main/res/layout/fragment_customer.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/fragment_customer_detail.xml b/android/app/src/main/res/layout/fragment_customer_detail.xml new file mode 100644 index 00000000..a2c63030 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_customer_detail.xml @@ -0,0 +1,252 @@ + + + + + + + + + +