Added Customer CRUD

This commit is contained in:
Alex
2026-04-10 19:34:28 -06:00
parent c11790c8b2
commit 6ee60164ce
15 changed files with 1118 additions and 52 deletions

View File

@@ -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<CustomerAdapter.CustomerViewHolder> {
private List<CustomerDTO> list;
private OnCustomerClickListener listener;
private String baseUrl;
private String token;
public interface OnCustomerClickListener {
void onCustomerClick(int position);
}
public CustomerAdapter(List<CustomerDTO> 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(); }
}

View File

@@ -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<CustomerDTO> getCustomerById(@Path("customerId") Long customerId);
@PUT("api/v1/customers/{customerId}")
Call<CustomerDTO> updateCustomer(@Path("customerId") Long customerId, @Body CustomerDTO customer);
@DELETE("api/v1/customers/{customerId}")
Call<Void> deleteCustomer(@Path("customerId") Long customerId);
@POST("api/v1/auth/register")
Call<CustomerDTO> registerCustomer(@Body CustomerDTO customer);
@GET("api/v1/dropdowns/customers")
Call<List<DropdownDTO>> getCustomerDropdowns();
}

View File

@@ -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; }
}

View File

@@ -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));

View File

@@ -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);

View File

@@ -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<CustomerDTO> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -23,23 +23,26 @@ public class CustomerRepository extends BaseRepository {
this.customerApi = customerApi;
}
/**
* Retrieves a paginated list of all customers from the API.
*/
public LiveData<Resource<PageResponse<CustomerDTO>>> getAllCustomers(int page, int size) {
return executeCall(customerApi.getAllCustomers(page, size));
}
/**
* Retrieves a specific customer by their ID.
*/
public LiveData<Resource<CustomerDTO>> getCustomerById(Long id) {
return executeCall(customerApi.getCustomerById(id));
}
/**
* Retrieves a list of customer dropdowns from the API.
*/
public LiveData<Resource<CustomerDTO>> createCustomer(CustomerDTO dto) {
return executeCall(customerApi.registerCustomer(dto));
}
public LiveData<Resource<CustomerDTO>> updateCustomer(Long id, CustomerDTO dto) {
return executeCall(customerApi.updateCustomer(id, dto));
}
public LiveData<Resource<Void>> deleteCustomer(Long id) {
return executeCall(customerApi.deleteCustomer(id));
}
public LiveData<Resource<List<DropdownDTO>>> getCustomerDropdowns() {
return executeCall(customerApi.getCustomerDropdowns());
}

View File

@@ -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<Resource<CustomerDTO>> loadCustomer(long id) {
return repository.getCustomerById(id);
}
public LiveData<Resource<CustomerDTO>> saveCustomer(CustomerDTO dto) {
if (isEditing && customerId > 0) {
return repository.updateCustomer(customerId, dto);
} else {
return repository.createCustomer(dto);
}
}
public LiveData<Resource<Void>> deleteCustomer() {
return repository.deleteCustomer(customerId);
}
}

View File

@@ -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<List<CustomerDTO>> customers = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<CustomerDTO>> filteredCustomers = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private String lastQuery = "";
private String lastStatus = "All Statuses";
@Inject
public CustomerListViewModel(CustomerRepository repository) {
this.repository = repository;
}
public LiveData<List<CustomerDTO>> getFilteredCustomers() { return filteredCustomers; }
public LiveData<Boolean> 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<CustomerDTO> all = customers.getValue();
if (all == null) return;
List<CustomerDTO> 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);
}
}

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_grey">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/headerCustomer"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageButton
android:id="@+id/btnHamburgerCustomer"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/baseline_menu_36"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Open menu"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Customers"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginStart="8dp"/>
<ImageButton
android:id="@+id/btnToggleFilterCustomer"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_search"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="@color/white"
android:contentDescription="Toggle filter"/>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutFilterCustomer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:visibility="gone"
android:background="@color/primary_dark"
android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@drawable/bg_search_bar"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@android:drawable/ic_menu_search"
android:alpha="0.6"/>
<EditText
android:id="@+id/etSearchCustomer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="Search customers..."
android:inputType="text"
android:background="@android:color/transparent"
android:textColor="@color/text_dark"
android:textColorHint="#99000000"
android:textSize="14sp"
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
</LinearLayout>
<Spinner
android:id="@+id/spinnerStatusCustomer"
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:layout_marginTop="8dp"/>
</LinearLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshCustomer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewCustomer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAddCustomer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:backgroundTint="@color/accent_coral"
android:contentDescription="Add Customer"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@color/white"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,252 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvCustomerMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Customer"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeleteCustomer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/tvCustomerId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
<!-- Username -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Username"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter username"
android:inputType="text"
android:layout_marginBottom="16dp"/>
<!-- Password -->
<TextView
android:id="@+id/tvCustomerPasswordLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Password"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter password (min 6 characters)"
android:inputType="textPassword"
android:layout_marginBottom="16dp"/>
<!-- First Name -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="First Name"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerFirstName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter first name"
android:inputType="textPersonName"
android:layout_marginBottom="16dp"/>
<!-- Last Name -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Last Name"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerLastName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter last name"
android:inputType="textPersonName"
android:layout_marginBottom="16dp"/>
<!-- Email -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Email"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter email"
android:inputType="textEmailAddress"
android:layout_marginBottom="16dp"/>
<!-- Phone -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Phone"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerPhone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter phone number"
android:inputType="phone"
android:layout_marginBottom="16dp"/>
<!-- Status -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Status"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerCustomerStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Loyalty Points (read-only) -->
<TextView
android:id="@+id/tvLoyaltyPointsLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Loyalty Points"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvCustomerLoyaltyPoints"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/text_dark"
android:textSize="16sp"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnCustomerBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSaveCustomer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

View File

@@ -146,6 +146,23 @@
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerCustomers"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Customers"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerServices"
android:layout_width="match_parent"
@@ -345,6 +362,7 @@
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/ivCustomerProfile"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="16dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder"
app:shapeAppearanceOverlay="@style/CircleImageView"
app:strokeWidth="2dp"
app:strokeColor="#BDBDBD"
android:padding="2dp"
android:contentDescription="Customer Profile Image" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvCustomerFullName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="Full Name"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvCustomerLoyalty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingTop="3dp"
android:paddingEnd="8dp"
android:paddingBottom="3dp"
android:text="0 pts"
android:textColor="@color/white"
android:textSize="11sp"
android:background="#FF9800" />
</LinearLayout>
<TextView
android:id="@+id/tvCustomerUsername"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@username"
android:textColor="#888888"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tvCustomerEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="email@example.com"
android:textColor="#888888"
android:textSize="13sp"
android:ellipsize="end"
android:maxLines="1"/>
<TextView
android:id="@+id/tvCustomerStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Active"
android:textColor="@color/accent_coral"
android:textSize="13sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"
android:layout_marginTop="12dp"/>
</LinearLayout>

View File

@@ -151,6 +151,18 @@
android:label="Staff Details"
tools:layout="@layout/fragment_staff_detail" />
<fragment
android:id="@+id/nav_customer"
android:name="com.example.petstoremobile.fragments.listfragments.CustomerFragment"
android:label="Customers"
tools:layout="@layout/fragment_customer" />
<fragment
android:id="@+id/nav_customer_detail"
android:name="com.example.petstoremobile.fragments.listfragments.detailfragments.CustomerDetailFragment"
android:label="Customer Details"
tools:layout="@layout/fragment_customer_detail" />
<fragment
android:id="@+id/nav_analytics"
android:name="com.example.petstoremobile.fragments.listfragments.AnalyticsFragment"