From 5850adedc333704ea9c9b90203da89e595bf47d8 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:17:19 -0600 Subject: [PATCH] fixed staff accounts and added coupons andriod --- .../adapters/CouponAdapter.java | 137 ++++++ .../adapters/EmployeeAdapter.java | 22 +- .../example/petstoremobile/api/CouponApi.java | 44 ++ .../petstoremobile/api/RetrofitClient.java | 143 ------ .../example/petstoremobile/api/UserApi.java | 2 + .../petstoremobile/di/NetworkModule.java | 6 + .../petstoremobile/dtos/CouponDTO.java | 52 +++ .../fragments/ListFragment.java | 1 + .../listfragments/CouponFragment.java | 145 ++++++ .../listfragments/StaffFragment.java | 47 +- .../detailfragments/CouponDetailFragment.java | 175 +++++++ .../repositories/CouponRepository.java | 48 ++ .../petstoremobile/utils/ErrorUtils.java | 1 + .../petstoremobile/utils/InputValidator.java | 18 + .../example/petstoremobile/utils/UIUtils.java | 4 + .../viewmodels/CouponDetailViewModel.java | 54 +++ .../viewmodels/CouponListViewModel.java | 50 ++ .../viewmodels/StaffListViewModel.java | 60 ++- .../src/main/res/layout/fragment_coupon.xml | 160 +++++++ .../res/layout/fragment_coupon_detail.xml | 258 ++++++++++ .../app/src/main/res/layout/fragment_list.xml | 441 ++++++++++-------- .../src/main/res/layout/fragment_staff.xml | 28 ++ .../app/src/main/res/layout/item_coupon.xml | 92 ++++ .../app/src/main/res/layout/item_employee.xml | 4 +- .../main/res/navigation/list_nav_graph.xml | 17 + .../backend/controller/CouponController.java | 65 +++ .../backend/dto/common/CouponRequest.java | 53 +++ .../backend/dto/common/CouponResponse.java | 65 +++ .../backend/repository/CouponRepository.java | 9 + .../backend/service/CouponService.java | 106 +++++ 30 files changed, 1939 insertions(+), 368 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/adapters/CouponAdapter.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/api/CouponApi.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/CouponDTO.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java create mode 100644 android/app/src/main/res/layout/fragment_coupon.xml create mode 100644 android/app/src/main/res/layout/fragment_coupon_detail.xml create mode 100644 android/app/src/main/res/layout/item_coupon.xml create mode 100644 backend/src/main/java/com/petshop/backend/controller/CouponController.java create mode 100644 backend/src/main/java/com/petshop/backend/dto/common/CouponRequest.java create mode 100644 backend/src/main/java/com/petshop/backend/dto/common/CouponResponse.java create mode 100644 backend/src/main/java/com/petshop/backend/service/CouponService.java diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/CouponAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/CouponAdapter.java new file mode 100644 index 00000000..a1414f56 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/CouponAdapter.java @@ -0,0 +1,137 @@ +package com.example.petstoremobile.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.dtos.CouponDTO; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class CouponAdapter extends RecyclerView.Adapter { + private final List coupons; + private final OnCouponClickListener listener; + private boolean selectionMode = false; + private final Set selectedIds = new HashSet<>(); + + public interface OnCouponClickListener { + void onCouponClick(CouponDTO coupon); + void onSelectionChanged(int count); + } + + public CouponAdapter(List coupons, OnCouponClickListener listener) { + this.coupons = coupons; + this.listener = listener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_coupon, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + CouponDTO coupon = coupons.get(position); + holder.tvCouponCode.setText(coupon.getCouponCode()); + + String discountText = ""; + if ("PERCENT".equals(coupon.getDiscountType())) { + discountText = coupon.getDiscountValue().stripTrailingZeros().toPlainString() + "% OFF"; + } else { + discountText = "$" + coupon.getDiscountValue().stripTrailingZeros().toPlainString() + " OFF"; + } + holder.tvCouponDiscount.setText(discountText); + + holder.tvCouponMinOrder.setText("Min order: $" + coupon.getMinOrderAmount().stripTrailingZeros().toPlainString()); + + if (coupon.getEndsAt() != null) { + holder.tvCouponExpiry.setText("Expires: " + coupon.getEndsAt().substring(0, 10)); + holder.tvCouponExpiry.setVisibility(View.VISIBLE); + } else { + holder.tvCouponExpiry.setVisibility(View.GONE); + } + + if (Boolean.TRUE.equals(coupon.getActive())) { + holder.tvCouponStatus.setText("ACTIVE"); + holder.tvCouponStatus.setBackgroundTintList(ContextCompat.getColorStateList(holder.itemView.getContext(), R.color.primary_dark)); + } else { + holder.tvCouponStatus.setText("INACTIVE"); + holder.tvCouponStatus.setBackgroundTintList(ContextCompat.getColorStateList(holder.itemView.getContext(), R.color.accent_coral)); + } + + holder.cbSelectCoupon.setVisibility(selectionMode ? View.VISIBLE : View.GONE); + holder.cbSelectCoupon.setChecked(selectedIds.contains(coupon.getCouponId())); + + holder.itemView.setOnClickListener(v -> { + if (selectionMode) { + toggleSelection(coupon.getCouponId()); + } else { + listener.onCouponClick(coupon); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!selectionMode) { + setSelectionMode(true); + toggleSelection(coupon.getCouponId()); + return true; + } + return false; + }); + + holder.cbSelectCoupon.setOnClickListener(v -> toggleSelection(coupon.getCouponId())); + } + + private void toggleSelection(Long id) { + if (selectedIds.contains(id)) { + selectedIds.remove(id); + } else { + selectedIds.add(id); + } + notifyDataSetChanged(); + listener.onSelectionChanged(selectedIds.size()); + } + + public void setSelectionMode(boolean selectionMode) { + this.selectionMode = selectionMode; + if (!selectionMode) selectedIds.clear(); + notifyDataSetChanged(); + listener.onSelectionChanged(selectedIds.size()); + } + + public Set getSelectedIds() { + return selectedIds; + } + + @Override + public int getItemCount() { + return coupons.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + TextView tvCouponCode, tvCouponDiscount, tvCouponMinOrder, tvCouponExpiry, tvCouponStatus; + CheckBox cbSelectCoupon; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + tvCouponCode = itemView.findViewById(R.id.tvCouponCode); + tvCouponDiscount = itemView.findViewById(R.id.tvCouponDiscount); + tvCouponMinOrder = itemView.findViewById(R.id.tvCouponMinOrder); + tvCouponExpiry = itemView.findViewById(R.id.tvCouponExpiry); + tvCouponStatus = itemView.findViewById(R.id.tvCouponStatus); + cbSelectCoupon = itemView.findViewById(R.id.cbSelectCoupon); + } + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java index 3dca0d7c..8213dbd0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java @@ -7,14 +7,19 @@ import android.widget.TextView; 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.ItemEmployeeBinding; import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.utils.GlideUtils; + import java.util.List; public class EmployeeAdapter extends RecyclerView.Adapter { private List list; private OnEmployeeClickListener listener; + private String baseUrl; + private String token; public interface OnEmployeeClickListener { void onEmployeeClick(int position); @@ -25,6 +30,14 @@ public class EmployeeAdapter extends RecyclerView.Adapter listener.onEmployeeClick(position)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/CouponApi.java b/android/app/src/main/java/com/example/petstoremobile/api/CouponApi.java new file mode 100644 index 00000000..ca12cf48 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/CouponApi.java @@ -0,0 +1,44 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.CouponDTO; +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; + +public interface CouponApi { + + @GET("api/v1/coupons") + Call> getAllCoupons( + @Query("page") int page, + @Query("size") int size, + @Query("active") Boolean active, + @Query("discountType") String discountType, + @Query("sort") String sort); + + @GET("api/v1/coupons/{id}") + Call getCouponById(@Path("id") Long id); + + @GET("api/v1/coupons/code/{code}") + Call getCouponByCode(@Path("code") String code); + + @POST("api/v1/coupons") + Call createCoupon(@Body CouponDTO coupon); + + @PUT("api/v1/coupons/{id}") + Call updateCoupon(@Path("id") Long id, @Body CouponDTO coupon); + + @DELETE("api/v1/coupons/{id}") + Call deleteCoupon(@Path("id") Long id); + + @DELETE("api/v1/coupons") + Call bulkDeleteCoupons(@Query("ids") List ids); +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java deleted file mode 100644 index 0bdf14d1..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java +++ /dev/null @@ -1,143 +0,0 @@ -package com.example.petstoremobile.api; - -import android.content.Context; -import android.os.Build; -import android.util.Log; - -import com.example.petstoremobile.BuildConfig; -import com.example.petstoremobile.api.auth.AuthApi; -import com.example.petstoremobile.api.auth.AuthInterceptor; -import com.example.petstoremobile.api.auth.TokenManager; - -import okhttp3.OkHttpClient; -import okhttp3.logging.HttpLoggingInterceptor; -import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; - -import java.util.concurrent.TimeUnit; - -//Retrofit client Used for API calls TODO: DELETE THIS FILE AFTER MERGE NOW THAT WE ARE USING HILT AND NETWORKMODULE -public class RetrofitClient { - private static final String TAG = "RetrofitClient"; - public static final String BASE_URL = getBaseUrl(); - - // Helper function to determine BASE_URL based on whether we are testing on an emulator or a real device - private static String getBaseUrl() { - String url = isEmulator() ? BuildConfig.EMULATOR_BACKEND_URL : BuildConfig.DEVICE_BACKEND_URL; - Log.i(TAG, "Using backend URL: " + url + " (emulator=" + isEmulator() + ")"); - return url; - } - - private static boolean isEmulator() { - return Build.FINGERPRINT.startsWith("generic") - || Build.FINGERPRINT.startsWith("unknown") - || Build.MODEL.contains("google_sdk") - || Build.MODEL.contains("Emulator") - || Build.MODEL.contains("Android SDK built for x86") - || Build.MANUFACTURER.contains("Genymotion") - || Build.HARDWARE.contains("goldfish") - || Build.HARDWARE.contains("ranchu") - || Build.PRODUCT.contains("sdk") - || Build.PRODUCT.contains("sdk_gphone") - || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")); - } - - private static Retrofit retrofit = null; - - public static Retrofit getClient(Context context) { - //create an http logging using an interceptor - HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); - interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); - - OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(interceptor) - .addInterceptor(new AuthInterceptor(new TokenManager(context))) - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .build(); - - //build the retrofit object with all needed properties - retrofit = new Retrofit.Builder() - .baseUrl(BASE_URL) - .addConverterFactory(GsonConverterFactory.create()) //JSON converter - .client(client) //logging interceptor - OkHttpClient - .build(); - - return retrofit; - } - - //associate the retrofit object with the API interface - public static PetApi getPetApi(Context context) { - return getClient(context).create(PetApi.class); - } - - public static ServiceApi getServiceApi(Context context) { - return getClient(context).create(ServiceApi.class); - } - - public static SupplierApi getSupplierApi(Context context) { - return getClient(context).create(SupplierApi.class); - } - - public static AdoptionApi getAdoptionApi(Context context) { - return getClient(context).create(AdoptionApi.class); - } - - public static AppointmentApi getAppointmentApi(Context context) { - return getClient(context).create(AppointmentApi.class); - } - - public static ProductApi getProductApi(Context context) { - return getClient(context).create(ProductApi.class); - } - - public static SaleApi getSaleApi(Context context) { - return getClient(context).create(SaleApi.class); - } - - public static PurchaseOrderApi getPurchaseOrderApi(Context context) { - return getClient(context).create(PurchaseOrderApi.class); - } - - public static ProductSupplierApi getProductSupplierApi(Context context) { - return getClient(context).create(ProductSupplierApi.class); - } - - public static InventoryApi getInventoryApi(Context context) { - return getClient(context).create(InventoryApi.class); - } - - public static AuthApi getAuthApi(Context context) { - return getClient(context).create(AuthApi.class); - } - - public static ChatApi getChatApi(Context context) { - return getClient(context).create(ChatApi.class); - } - - public static CustomerApi getCustomerApi(Context context) { - return getClient(context).create(CustomerApi.class); - } - - public static MessageApi getMessageApi(Context context) { - return getClient(context).create(MessageApi.class); - } - - public static StoreApi getStoreApi(Context context) { - return getClient(context).create(StoreApi.class); - } - - public static CategoryApi getCategoryApi(Context context) { - return getClient(context).create(CategoryApi.class); - } - - public static RefundApi getRefundApi(Context context) { - return getClient(context).create(RefundApi.class); - } - - public static EmployeeApi getEmployeeApi(Context context) { - return getClient(context).create(EmployeeApi.class); - } - -} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java b/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java index 53611e66..8346e221 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java @@ -8,6 +8,8 @@ import retrofit2.http.GET; import retrofit2.http.Query; public interface UserApi { + String AVATAR_PATH = "api/v1/users/%d/avatar/file"; + @GET("api/v1/users") Call> getUsers(@Query("role") String role, @Query("page") int page, @Query("size") int size); } diff --git a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java index 4191de71..1a4ab665 100644 --- a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java +++ b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java @@ -191,4 +191,10 @@ public class NetworkModule { public static RefundApi provideRefundApi(Retrofit retrofit) { return retrofit.create(RefundApi.class); } + + @Provides + @Singleton + public static CouponApi provideCouponApi(Retrofit retrofit) { + return retrofit.create(CouponApi.class); + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CouponDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CouponDTO.java new file mode 100644 index 00000000..87b3eb7e --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CouponDTO.java @@ -0,0 +1,52 @@ +package com.example.petstoremobile.dtos; + +import java.math.BigDecimal; + +public class CouponDTO { + private Long couponId; + private String couponCode; + private String discountType; // FIXED, PERCENT + private BigDecimal discountValue; + private BigDecimal minOrderAmount; + private Boolean active; + private String startsAt; + private String endsAt; + private Integer usageLimit; + private String createdAt; + private String updatedAt; + + public CouponDTO() {} + + public Long getCouponId() { return couponId; } + public void setCouponId(Long couponId) { this.couponId = couponId; } + + public String getCouponCode() { return couponCode; } + public void setCouponCode(String couponCode) { this.couponCode = couponCode; } + + public String getDiscountType() { return discountType; } + public void setDiscountType(String discountType) { this.discountType = discountType; } + + public BigDecimal getDiscountValue() { return discountValue; } + public void setDiscountValue(BigDecimal discountValue) { this.discountValue = discountValue; } + + public BigDecimal getMinOrderAmount() { return minOrderAmount; } + public void setMinOrderAmount(BigDecimal minOrderAmount) { this.minOrderAmount = minOrderAmount; } + + public Boolean getActive() { return active; } + public void setActive(Boolean active) { this.active = active; } + + public String getStartsAt() { return startsAt; } + public void setStartsAt(String startsAt) { this.startsAt = startsAt; } + + public String getEndsAt() { return endsAt; } + public void setEndsAt(String endsAt) { this.endsAt = endsAt; } + + public Integer getUsageLimit() { return usageLimit; } + public void setUsageLimit(Integer usageLimit) { this.usageLimit = usageLimit; } + + 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; } +} 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 901a2af5..0daf21e9 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 @@ -92,6 +92,7 @@ public class ListFragment extends Fragment { binding.drawerSale.setOnClickListener(v -> navigateTo(R.id.nav_sale)); binding.drawerStaff.setOnClickListener(v -> navigateTo(R.id.nav_staff)); binding.drawerAnalytics.setOnClickListener(v -> navigateTo(R.id.nav_analytics)); + binding.drawerCoupons.setOnClickListener(v -> navigateTo(R.id.nav_coupon)); return binding.getRoot(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java new file mode 100644 index 00000000..548d6018 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java @@ -0,0 +1,145 @@ +package com.example.petstoremobile.fragments.listfragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.CouponAdapter; +import com.example.petstoremobile.databinding.FragmentCouponBinding; +import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.CouponListViewModel; + +import java.util.ArrayList; +import java.util.List; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class CouponFragment extends Fragment implements CouponAdapter.OnCouponClickListener { + private FragmentCouponBinding binding; + private CouponListViewModel viewModel; + private CouponAdapter adapter; + private final List couponList = new ArrayList<>(); + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + binding = FragmentCouponBinding.inflate(inflater, container, false); + viewModel = new ViewModelProvider(this).get(CouponListViewModel.class); + + setupRecyclerView(); + setupSearch(); + setupStatusFilter(); + setupTypeFilter(); + setupSwipeRefresh(); + observeViewModel(); + + viewModel.loadCoupons(0, 100, null, null, null); + + binding.fabAddCoupon.setOnClickListener(v -> openDetail(-1)); + binding.btnBulkDeleteCoupons.setOnClickListener(v -> confirmBulkDelete()); + + UIUtils.setupHamburgerMenu(binding.btnHamburgerCoupon, this); + UIUtils.setupFilterToggle(binding.btnToggleFilterCoupon, binding.layoutFilterCoupon, binding.etSearchCoupon, + binding.spinnerTypeCoupon, binding.spinnerStatusCoupon); + + return binding.getRoot(); + } + + private void observeViewModel() { + viewModel.getCoupons().observe(getViewLifecycleOwner(), list -> { + couponList.clear(); + couponList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshCoupon.setRefreshing(loading); + }); + } + + private void setupRecyclerView() { + adapter = new CouponAdapter(couponList, this); + binding.recyclerViewCoupon.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewCoupon.setAdapter(adapter); + } + + private void setupStatusFilter() { + String[] statuses = {"All Statuses", "Active", "Inactive"}; + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCoupon, statuses, this::applyFilters); + } + + private void setupTypeFilter() { + String[] types = {"All Types", "FIXED", "PERCENT"}; + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerTypeCoupon, types, this::applyFilters); + } + + private void setupSearch() { + UIUtils.attachSearch(binding.etSearchCoupon, this::applyFilters); + } + + private void setupSwipeRefresh() { + binding.swipeRefreshCoupon.setOnRefreshListener(this::applyFilters); + } + + private void applyFilters() { + String statusStr = binding.spinnerStatusCoupon.getSelectedItem() != null ? + binding.spinnerStatusCoupon.getSelectedItem().toString() : "All Statuses"; + Boolean active = null; + if (statusStr.equals("Active")) active = true; + else if (statusStr.equals("Inactive")) active = false; + + String typeStr = binding.spinnerTypeCoupon.getSelectedItem() != null ? + binding.spinnerTypeCoupon.getSelectedItem().toString() : "All Types"; + String discountType = typeStr.equals("All Types") ? null : typeStr; + + viewModel.loadCoupons(0, 100, active, discountType, null); + } + + private void openDetail(long id) { + Bundle args = new Bundle(); + args.putLong("couponId", id); + Navigation.findNavController(requireView()).navigate(R.id.couponDetailFragment, args); + } + + @Override + public void onCouponClick(CouponDTO coupon) { + openDetail(coupon.getCouponId()); + } + + @Override + public void onSelectionChanged(int count) { + binding.btnBulkDeleteCoupons.setVisibility(count > 0 ? View.VISIBLE : View.GONE); + } + + private void confirmBulkDelete() { + new AlertDialog.Builder(requireContext()) + .setTitle("Confirm Bulk Delete") + .setMessage("Are you sure you want to delete the selected coupons?") + .setPositiveButton("Delete", (dialog, which) -> { + List ids = new ArrayList<>(adapter.getSelectedIds()); + viewModel.bulkDeleteCoupons(ids).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + adapter.setSelectionMode(false); + applyFilters(); + } else if (resource.status == Resource.Status.ERROR) { + UIUtils.showToast(requireContext(), resource.message); + } + }); + }) + .setNegativeButton("Cancel", null) + .show(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java index 62ec931f..8540b5f8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java @@ -10,13 +10,19 @@ import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.EmployeeAdapter; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentStaffBinding; import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.StaffListViewModel; import dagger.hilt.android.AndroidEntryPoint; import java.util.*; +import javax.inject.Inject; +import javax.inject.Named; + @AndroidEntryPoint public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener { @@ -25,6 +31,9 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye private List staffList = new ArrayList<>(); private EmployeeAdapter adapter; + @Inject @Named("baseUrl") String baseUrl; + @Inject TokenManager tokenManager; + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -33,15 +42,19 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye setupRecyclerView(); setupSearch(); + setupStatusFilter(); + setupStoreFilter(); setupSwipeRefresh(); observeViewModel(); viewModel.loadStaff(); + viewModel.loadStores(); binding.fabAddStaff.setOnClickListener(v -> openDetail(-1)); UIUtils.setupHamburgerMenu(binding.btnHamburgerStaff, this); - UIUtils.setupFilterToggle(binding.btnToggleFilterStaff, binding.layoutFilterStaff, binding.etSearchStaff); + UIUtils.setupFilterToggle(binding.btnToggleFilterStaff, binding.layoutFilterStaff, binding.etSearchStaff, + binding.spinnerStoreStaff, binding.spinnerStatusStaff); return binding.getRoot(); } @@ -53,6 +66,11 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye adapter.notifyDataSetChanged(); }); + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreStaff, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { binding.swipeRefreshStaff.setRefreshing(loading); }); @@ -60,12 +78,37 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye private void setupRecyclerView() { adapter = new EmployeeAdapter(staffList, this); + adapter.setBaseUrl(baseUrl); + adapter.setToken(tokenManager.getToken()); binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewStaff.setAdapter(adapter); } + private void setupStatusFilter() { + String[] statuses = {"All Statuses", "Active", "Inactive"}; + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusStaff, statuses, this::applyFilters); + } + + private void setupStoreFilter() { + SpinnerUtils.setupFilterSpinner(binding.spinnerStoreStaff, this::applyFilters); + } + private void setupSearch() { - UIUtils.attachSearch(binding.etSearchStaff, () -> viewModel.filter(binding.etSearchStaff.getText().toString())); + UIUtils.attachSearch(binding.etSearchStaff, this::applyFilters); + } + + private void applyFilters() { + String query = binding.etSearchStaff.getText().toString().trim(); + String status = binding.spinnerStatusStaff.getSelectedItem() != null ? + binding.spinnerStatusStaff.getSelectedItem().toString() : "All Statuses"; + + Long storeId = null; + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStoreStaff.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStoreStaff.getSelectedItemPosition() - 1).getStoreId(); + } + + viewModel.filter(query, storeId, status); } private void setupSwipeRefresh() { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java new file mode 100644 index 00000000..7646358f --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java @@ -0,0 +1,175 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +import android.app.DatePickerDialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.Navigation; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.FragmentCouponDetailBinding; +import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.CouponDetailViewModel; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class CouponDetailFragment extends Fragment { + private FragmentCouponDetailBinding binding; + private CouponDetailViewModel viewModel; + private long couponId = -1; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + binding = FragmentCouponDetailBinding.inflate(inflater, container, false); + viewModel = new ViewModelProvider(this).get(CouponDetailViewModel.class); + + if (getArguments() != null) { + couponId = getArguments().getLong("couponId", -1); + viewModel.setCouponId(couponId, couponId > 0); + } + + setupDiscountTypeSpinner(); + setupDatePicker(binding.etStartsAtDetail, null, () -> { + String selectedDate = binding.etStartsAtDetail.getText().toString(); + if (selectedDate.isEmpty()) { + binding.etEndsAtDetail.setText(""); + } else { + String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + if (selectedDate.equals(today)) { + binding.cbActiveDetail.setChecked(true); + } + } + }); + setupDatePicker(binding.etEndsAtDetail, binding.etStartsAtDetail, null); + + if (couponId > 0) { + loadCouponDetails(); + binding.btnDeleteCouponDetail.setVisibility(View.VISIBLE); + } else { + binding.tvTitleCouponDetail.setText("Add Coupon"); + } + + binding.btnBackCouponDetail.setOnClickListener(v -> Navigation.findNavController(v).popBackStack()); + binding.btnSaveCouponDetail.setOnClickListener(v -> saveCoupon()); + binding.btnDeleteCouponDetail.setOnClickListener(v -> confirmDelete()); + + return binding.getRoot(); + } + + private void setupDiscountTypeSpinner() { + String[] types = {"FIXED", "PERCENT"}; + ArrayAdapter adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, types); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + binding.spinnerDiscountTypeDetail.setAdapter(adapter); + } + + private void setupDatePicker(android.widget.EditText editText, android.widget.EditText dependOn, Runnable onDateSet) { + editText.setFocusable(false); + editText.setClickable(true); + editText.setCursorVisible(false); + editText.setOnClickListener(v -> { + if (dependOn != null && dependOn.getText().toString().trim().isEmpty()) { + UIUtils.showToast(requireContext(), "Please select a start date first"); + return; + } + UIUtils.showDatePicker(requireContext(), editText, onDateSet); + }); + } + + private void loadCouponDetails() { + viewModel.loadCoupon(couponId).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + CouponDTO coupon = resource.data; + binding.etCouponCodeDetail.setText(coupon.getCouponCode()); + binding.spinnerDiscountTypeDetail.setSelection(coupon.getDiscountType().equals("FIXED") ? 0 : 1); + binding.etDiscountValueDetail.setText(coupon.getDiscountValue().stripTrailingZeros().toPlainString()); + binding.etMinOrderAmountDetail.setText(coupon.getMinOrderAmount().stripTrailingZeros().toPlainString()); + binding.etUsageLimitDetail.setText(coupon.getUsageLimit() != null ? String.valueOf(coupon.getUsageLimit()) : ""); + binding.cbActiveDetail.setChecked(Boolean.TRUE.equals(coupon.getActive())); + + if (coupon.getStartsAt() != null) binding.etStartsAtDetail.setText(coupon.getStartsAt().substring(0, 10)); + if (coupon.getEndsAt() != null) binding.etEndsAtDetail.setText(coupon.getEndsAt().substring(0, 10)); + } + }); + } + + private void saveCoupon() { + if (!InputValidator.isNotEmpty(binding.etCouponCodeDetail, "Coupon Code")) return; + if (!InputValidator.isGreaterThanZero(binding.etDiscountValueDetail, "Discount Value")) return; + if (!InputValidator.isPositiveDecimal(binding.etMinOrderAmountDetail, "Min Order Amount")) return; + + String startsAt = binding.etStartsAtDetail.getText().toString().trim(); + String endsAt = binding.etEndsAtDetail.getText().toString().trim(); + + if (!startsAt.isEmpty() && !endsAt.isEmpty()) { + if (startsAt.compareTo(endsAt) > 0) { + binding.etEndsAtDetail.setError("End date must be after start date"); + UIUtils.showToast(requireContext(), "End date must be after start date"); + return; + } + } + + String usageLimitStr = binding.etUsageLimitDetail.getText().toString().trim(); + if (!usageLimitStr.isEmpty()) { + if (!InputValidator.isPositiveInteger(binding.etUsageLimitDetail, "Usage Limit")) return; + } + + CouponDTO dto = new CouponDTO(); + dto.setCouponCode(binding.etCouponCodeDetail.getText().toString().trim().toUpperCase()); + dto.setDiscountType(binding.spinnerDiscountTypeDetail.getSelectedItem().toString()); + dto.setDiscountValue(new BigDecimal(binding.etDiscountValueDetail.getText().toString().trim())); + dto.setMinOrderAmount(new BigDecimal(binding.etMinOrderAmountDetail.getText().toString().trim())); + + String usageLimit = binding.etUsageLimitDetail.getText().toString().trim(); + dto.setUsageLimit(usageLimit.isEmpty() ? null : Integer.parseInt(usageLimit)); + + dto.setActive(binding.cbActiveDetail.isChecked()); + + dto.setStartsAt(startsAt.isEmpty() ? null : startsAt + "T00:00:00"); + + dto.setEndsAt(endsAt.isEmpty() ? null : endsAt + "T23:59:59"); + + viewModel.saveCoupon(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Navigation.findNavController(requireView()).popBackStack(); + } else if (resource.status == Resource.Status.ERROR) { + UIUtils.showToast(requireContext(), resource.message); + } + }); + } + + private void confirmDelete() { + new AlertDialog.Builder(requireContext()) + .setTitle("Delete Coupon") + .setMessage("Are you sure you want to delete this coupon?") + .setPositiveButton("Delete", (dialog, which) -> { + viewModel.deleteCoupon().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Navigation.findNavController(requireView()).popBackStack(); + } else if (resource.status == Resource.Status.ERROR) { + UIUtils.showToast(requireContext(), resource.message); + } + }); + }) + .setNegativeButton("Cancel", null) + .show(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java new file mode 100644 index 00000000..338043be --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java @@ -0,0 +1,48 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; + +import com.example.petstoremobile.api.CouponApi; +import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.utils.Resource; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class CouponRepository extends BaseRepository { + private final CouponApi couponApi; + + @Inject + public CouponRepository(CouponApi couponApi) { + super("CouponRepository"); + this.couponApi = couponApi; + } + + public LiveData>> getAllCoupons(int page, int size, Boolean active, String discountType, String sort) { + return executeCall(couponApi.getAllCoupons(page, size, active, discountType, sort)); + } + + public LiveData> getCouponById(Long id) { + return executeCall(couponApi.getCouponById(id)); + } + + public LiveData> createCoupon(CouponDTO coupon) { + return executeCall(couponApi.createCoupon(coupon)); + } + + public LiveData> updateCoupon(Long id, CouponDTO coupon) { + return executeCall(couponApi.updateCoupon(id, coupon)); + } + + public LiveData> deleteCoupon(Long id) { + return executeCall(couponApi.deleteCoupon(id)); + } + + public LiveData> bulkDeleteCoupons(List ids) { + return executeCall(couponApi.bulkDeleteCoupons(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java index 82a10cf7..1f866b0b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java @@ -34,6 +34,7 @@ public class ErrorUtils { try { if (response.errorBody() != null) { String errorJson = response.errorBody().string(); + Log.e(TAG, "Full Error JSON: " + errorJson); ErrorResponse errorResponse = gson.fromJson(errorJson, ErrorResponse.class); if (errorResponse != null && errorResponse.getMessage() != null) { return errorResponse.getMessage(); diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java index a10389b6..03802f65 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java @@ -35,6 +35,24 @@ public class InputValidator { } } + // Checks if the value is a positive decimal number greater than 0 + public static boolean isGreaterThanZero(EditText field, String fieldName) { + String value = field.getText().toString().trim(); + try { + double num = Double.parseDouble(value); + if (num <= 0) { + field.setError(fieldName + " must be greater than 0"); + field.requestFocus(); + return false; + } + return true; + } catch (NumberFormatException e) { + field.setError(fieldName + " must be a number"); + field.requestFocus(); + return false; + } + } + // Checks if the value is a positive decimal number public static boolean isPositiveDecimal(EditText field, String fieldName) { String value = field.getText().toString().trim(); diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java index 09b98cb6..947fb87e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java @@ -134,6 +134,10 @@ public class UIUtils { }, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)); + d.setButton(DatePickerDialog.BUTTON_NEUTRAL, "Clear", (dialog, which) -> { + editText.setText(""); + if (onDateSet != null) onDateSet.run(); + }); d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000); d.show(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponDetailViewModel.java new file mode 100644 index 00000000..49f52ef5 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponDetailViewModel.java @@ -0,0 +1,54 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.repositories.CouponRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class CouponDetailViewModel extends ViewModel { + private final CouponRepository repository; + private long couponId = -1; + private boolean isEditing = false; + + @Inject + public CouponDetailViewModel(CouponRepository repository) { + this.repository = repository; + } + + public LiveData> loadCoupon(long id) { + return repository.getCouponById(id); + } + + public void setCouponId(long id, boolean isEditing) { + this.couponId = id; + this.isEditing = isEditing; + } + + public long getCouponId() { + return couponId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> saveCoupon(CouponDTO dto) { + if (isEditing && couponId > 0) { + return repository.updateCoupon(couponId, dto); + } else { + return repository.createCoupon(dto); + } + } + + public LiveData> deleteCoupon() { + return repository.deleteCoupon(couponId); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java new file mode 100644 index 00000000..ad27ae6c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java @@ -0,0 +1,50 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.repositories.CouponRepository; +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 CouponListViewModel extends ViewModel { + private final CouponRepository repository; + private final MutableLiveData> coupons = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public CouponListViewModel(CouponRepository repository) { + this.repository = repository; + } + + public LiveData> getCoupons() { return coupons; } + public LiveData getIsLoading() { return isLoading; } + + public void loadCoupons(int page, int size, Boolean active, String discountType, String sort) { + isLoading.setValue(true); + repository.getAllCoupons(page, size, active, discountType, sort).observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + coupons.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public LiveData> bulkDeleteCoupons(List ids) { + return repository.bulkDeleteCoupons(ids); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java index 1bd317ca..d448d853 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java @@ -5,7 +5,9 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.repositories.EmployeeRepository; +import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.utils.Resource; import java.util.ArrayList; @@ -18,18 +20,24 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class StaffListViewModel extends ViewModel { private final EmployeeRepository repository; + private final StoreRepository storeRepository; private final MutableLiveData> employees = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> filteredEmployees = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); private String lastQuery = ""; + private Long lastStoreId = null; + private String lastStatus = "All Statuses"; @Inject - public StaffListViewModel(EmployeeRepository repository) { + public StaffListViewModel(EmployeeRepository repository, StoreRepository storeRepository) { this.repository = repository; + this.storeRepository = storeRepository; } public LiveData> getFilteredEmployees() { return filteredEmployees; } + public LiveData> getStores() { return stores; } public LiveData getIsLoading() { return isLoading; } public void loadStaff() { @@ -38,7 +46,7 @@ public class StaffListViewModel extends ViewModel { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { employees.setValue(resource.data.getContent()); - filter(lastQuery); + filter(lastQuery, lastStoreId, lastStatus); isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false); @@ -47,25 +55,45 @@ public class StaffListViewModel extends ViewModel { }); } - public void filter(String query) { + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public void filter(String query, Long storeId, String status) { this.lastQuery = query; + this.lastStoreId = storeId; + this.lastStatus = status; + List all = employees.getValue(); if (all == null) return; - if (query.isEmpty()) { - filteredEmployees.setValue(new ArrayList<>(all)); - } else { - List filtered = new ArrayList<>(); - String lower = query.toLowerCase(); - for (EmployeeDTO e : all) { - if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower)) - || (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower)) - || (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower)) - || (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) { - filtered.add(e); - } + List filtered = new ArrayList<>(); + String lowerQuery = query.toLowerCase(); + + for (EmployeeDTO e : all) { + // Search Query Filter + boolean matchesQuery = query.isEmpty() || + (e.getFullName() != null && e.getFullName().toLowerCase().contains(lowerQuery)) || + (e.getUsername() != null && e.getUsername().toLowerCase().contains(lowerQuery)) || + (e.getEmail() != null && e.getEmail().toLowerCase().contains(lowerQuery)) || + (e.getPhone() != null && e.getPhone().toLowerCase().contains(lowerQuery)); + + // Store Filter + boolean matchesStore = storeId == null || (e.getPrimaryStoreId() != null && e.getPrimaryStoreId().equals(storeId)); + + // Status Filter + boolean matchesStatus = status.equals("All Statuses") || + (status.equals("Active") && Boolean.TRUE.equals(e.getActive())) || + (status.equals("Inactive") && Boolean.FALSE.equals(e.getActive())); + + if (matchesQuery && matchesStore && matchesStatus) { + filtered.add(e); } - filteredEmployees.setValue(filtered); } + filteredEmployees.setValue(filtered); } } diff --git a/android/app/src/main/res/layout/fragment_coupon.xml b/android/app/src/main/res/layout/fragment_coupon.xml new file mode 100644 index 00000000..4497790f --- /dev/null +++ b/android/app/src/main/res/layout/fragment_coupon.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/fragment_coupon_detail.xml b/android/app/src/main/res/layout/fragment_coupon_detail.xml new file mode 100644 index 00000000..64bf1f8a --- /dev/null +++ b/android/app/src/main/res/layout/fragment_coupon_detail.xml @@ -0,0 +1,258 @@ + + + + + + + + + + +