fixed staff accounts and added coupons andriod

This commit is contained in:
Alex
2026-04-10 07:17:19 -06:00
parent dff379c99d
commit 1e7d56499b
30 changed files with 1939 additions and 368 deletions

View File

@@ -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<CouponAdapter.ViewHolder> {
private final List<CouponDTO> coupons;
private final OnCouponClickListener listener;
private boolean selectionMode = false;
private final Set<Long> selectedIds = new HashSet<>();
public interface OnCouponClickListener {
void onCouponClick(CouponDTO coupon);
void onSelectionChanged(int count);
}
public CouponAdapter(List<CouponDTO> 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<Long> 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);
}
}
}

View File

@@ -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<EmployeeAdapter.EmployeeViewHolder> {
private List<EmployeeDTO> 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<EmployeeAdapter.Employ
this.listener = listener;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setToken(String token) {
this.token = token;
}
public static class EmployeeViewHolder extends RecyclerView.ViewHolder {
private final ItemEmployeeBinding binding;
@@ -66,8 +79,13 @@ public class EmployeeAdapter extends RecyclerView.Adapter<EmployeeAdapter.Employ
binding.tvEmployeeStatus.setText(active ? "Active" : "Inactive");
binding.tvEmployeeStatus.setTextColor(active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336"));
// Placeholder for profile image - matching Pet style
binding.ivEmployeeProfile.setImageResource(R.drawable.placeholder);
// Profile image
if (baseUrl != null && e.getId() != null) {
String imageUrl = baseUrl + String.format(UserApi.AVATAR_PATH, e.getId());
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), binding.ivEmployeeProfile, imageUrl, token, R.drawable.placeholder);
} else {
binding.ivEmployeeProfile.setImageResource(R.drawable.placeholder);
}
holder.itemView.setOnClickListener(v -> listener.onEmployeeClick(position));
}

View File

@@ -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<PageResponse<CouponDTO>> 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<CouponDTO> getCouponById(@Path("id") Long id);
@GET("api/v1/coupons/code/{code}")
Call<CouponDTO> getCouponByCode(@Path("code") String code);
@POST("api/v1/coupons")
Call<CouponDTO> createCoupon(@Body CouponDTO coupon);
@PUT("api/v1/coupons/{id}")
Call<CouponDTO> updateCoupon(@Path("id") Long id, @Body CouponDTO coupon);
@DELETE("api/v1/coupons/{id}")
Call<Void> deleteCoupon(@Path("id") Long id);
@DELETE("api/v1/coupons")
Call<Void> bulkDeleteCoupons(@Query("ids") List<Long> ids);
}

View File

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

View File

@@ -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<PageResponse<UserDTO>> getUsers(@Query("role") String role, @Query("page") int page, @Query("size") int size);
}

View File

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

View File

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

View File

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

View File

@@ -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<CouponDTO> 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<Long> 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();
}
}

View File

@@ -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<EmployeeDTO> 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<StoreDTO> 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() {

View File

@@ -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<String> 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();
}
}

View File

@@ -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<Resource<PageResponse<CouponDTO>>> getAllCoupons(int page, int size, Boolean active, String discountType, String sort) {
return executeCall(couponApi.getAllCoupons(page, size, active, discountType, sort));
}
public LiveData<Resource<CouponDTO>> getCouponById(Long id) {
return executeCall(couponApi.getCouponById(id));
}
public LiveData<Resource<CouponDTO>> createCoupon(CouponDTO coupon) {
return executeCall(couponApi.createCoupon(coupon));
}
public LiveData<Resource<CouponDTO>> updateCoupon(Long id, CouponDTO coupon) {
return executeCall(couponApi.updateCoupon(id, coupon));
}
public LiveData<Resource<Void>> deleteCoupon(Long id) {
return executeCall(couponApi.deleteCoupon(id));
}
public LiveData<Resource<Void>> bulkDeleteCoupons(List<Long> ids) {
return executeCall(couponApi.bulkDeleteCoupons(ids));
}
}

View File

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

View File

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

View File

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

View File

@@ -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<Resource<CouponDTO>> 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<Resource<CouponDTO>> saveCoupon(CouponDTO dto) {
if (isEditing && couponId > 0) {
return repository.updateCoupon(couponId, dto);
} else {
return repository.createCoupon(dto);
}
}
public LiveData<Resource<Void>> deleteCoupon() {
return repository.deleteCoupon(couponId);
}
}

View File

@@ -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<List<CouponDTO>> coupons = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public CouponListViewModel(CouponRepository repository) {
this.repository = repository;
}
public LiveData<List<CouponDTO>> getCoupons() { return coupons; }
public LiveData<Boolean> 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<Resource<Void>> bulkDeleteCoupons(List<Long> ids) {
return repository.bulkDeleteCoupons(ids);
}
}

View File

@@ -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<List<EmployeeDTO>> employees = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<EmployeeDTO>> filteredEmployees = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> 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<List<EmployeeDTO>> getFilteredEmployees() { return filteredEmployees; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> 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<EmployeeDTO> all = employees.getValue();
if (all == null) return;
if (query.isEmpty()) {
filteredEmployees.setValue(new ArrayList<>(all));
} else {
List<EmployeeDTO> 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<EmployeeDTO> 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);
}
}

View File

@@ -0,0 +1,160 @@
<?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/headerCoupon"
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/btnHamburgerCoupon"
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="Coupons"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginStart="8dp"/>
<ImageButton
android:id="@+id/btnBulkDeleteCoupons"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_delete"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="@color/white"
android:contentDescription="Bulk Delete Coupons"
android:visibility="gone"/>
<ImageButton
android:id="@+id/btnToggleFilterCoupon"
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/layoutFilterCoupon"
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/etSearchCoupon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="Search coupons..."
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>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp"
android:gravity="center_vertical">
<Spinner
android:id="@+id/spinnerTypeCoupon"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:layout_marginEnd="4dp"/>
<Spinner
android:id="@+id/spinnerStatusCoupon"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:layout_marginStart="4dp"/>
</LinearLayout>
</LinearLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshCoupon"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewCoupon"
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/fabAddCoupon"
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 Coupon"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@color/white"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,258 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_grey">
<!-- Top Bar -->
<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/tvTitleCouponDetail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Coupon Detail"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeleteCouponDetail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"
android:visibility="gone"/>
</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">
<!-- Coupon Code -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Coupon Code"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCouponCodeDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter coupon code"
android:inputType="textCapCharacters"
android:layout_marginBottom="16dp"/>
<!-- Discount Type -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Discount Type"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerDiscountTypeDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Discount Value -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Discount Value"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etDiscountValueDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="0.00"
android:inputType="numberDecimal"
android:layout_marginBottom="16dp"/>
<!-- Min Order Amount -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Min Order Amount"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etMinOrderAmountDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="0.00"
android:inputType="numberDecimal"
android:layout_marginBottom="16dp"/>
<!-- Usage Limit -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Usage Limit"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etUsageLimitDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="No limit"
android:inputType="number"
android:layout_marginBottom="16dp"/>
<!-- Dates -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginEnd="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Starts At"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etStartsAtDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="YYYY-MM-DD"
android:focusable="false"
android:clickable="true"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ends At"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etEndsAtDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="YYYY-MM-DD"
android:focusable="false"
android:clickable="true"/>
</LinearLayout>
</LinearLayout>
<!-- Active -->
<CheckBox
android:id="@+id/cbActiveDetail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Active"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- Bottom Action Bar -->
<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/btnBackCouponDetail"
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/btnSaveCouponDetail"
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

@@ -34,9 +34,10 @@
android:layout_width="260dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:orientation="vertical"
android:background="@color/primary_dark">
android:background="@color/primary_dark"
android:orientation="vertical">
<!-- Header: Logo and Store Name (Static) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -69,221 +70,255 @@
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MANAGE"
android:textColor="@color/text_light"
android:textSize="11sp"
android:letterSpacing="0.15"
android:paddingStart="16dp"
android:paddingTop="24dp"
android:paddingBottom="8dp"/>
<LinearLayout
android:id="@+id/drawerPets"
<!-- Scrollable Menu Items -->
<ScrollView
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_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pets"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
android:orientation="vertical"
android:paddingBottom="24dp">
<LinearLayout
android:id="@+id/drawerServices"
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="Services"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MANAGE"
android:textColor="@color/text_light"
android:textSize="11sp"
android:letterSpacing="0.15"
android:paddingStart="16dp"
android:paddingTop="24dp"
android:paddingBottom="8dp"/>
<LinearLayout
android:id="@+id/drawerSuppliers"
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="Suppliers"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerPets"
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="Pets"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerAppointments"
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="Appointments"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerServices"
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="Services"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerAdoptions"
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="Adoptions"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerSuppliers"
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="Suppliers"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerInventory"
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="Inventory"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerAppointments"
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="Appointments"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerProducts"
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="Products"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerAdoptions"
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="Adoptions"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerSale"
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="Sale"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerInventory"
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="Inventory"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerAnalytics"
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="Analytics"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerProducts"
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="Products"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerStaff"
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"
android:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Staff Accounts"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerSale"
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="Sale"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerPurchaseOrderView"
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="PurchaseOrder"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerAnalytics"
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="Analytics"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerProductSupplier"
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="ProductSupplier"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerStaff"
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"
android:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Staff Accounts"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerPurchaseOrderView"
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="PurchaseOrder"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerProductSupplier"
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="ProductSupplier"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerCoupons"
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="Coupons"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -91,6 +91,34 @@
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp"
android:gravity="center_vertical">
<Spinner
android:id="@+id/spinnerStoreStaff"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:layout_marginEnd="4dp"/>
<Spinner
android:id="@+id/spinnerStatusStaff"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:layout_marginStart="4dp"/>
</LinearLayout>
</LinearLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/white"
android:layout_marginBottom="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<CheckBox
android:id="@+id/cbSelectCoupon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:visibility="gone"/>
<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/tvCouponCode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="COUPONCODE"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvCouponStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingTop="3dp"
android:paddingEnd="8dp"
android:paddingBottom="3dp"
android:text="ACTIVE"
android:textAllCaps="true"
android:textColor="@color/white"
android:textSize="11sp" />
</LinearLayout>
<TextView
android:id="@+id/tvCouponDiscount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="10% OFF"
android:textColor="@color/accent_coral"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvCouponMinOrder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="Min order: $50.00"
android:textColor="#888888"
android:textSize="13sp" />
<TextView
android:id="@+id/tvCouponExpiry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="Expires: 2026-12-31"
android:textColor="#888888"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -18,8 +18,8 @@
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/ivEmployeeProfile"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginEnd="16dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder"

View File

@@ -157,4 +157,21 @@
android:label="Analytics"
tools:layout="@layout/fragment_analytics" />
<fragment
android:id="@+id/nav_coupon"
android:name="com.example.petstoremobile.fragments.listfragments.CouponFragment"
android:label="Coupons"
tools:layout="@layout/fragment_coupon" />
<fragment
android:id="@+id/couponDetailFragment"
android:name="com.example.petstoremobile.fragments.listfragments.detailfragments.CouponDetailFragment"
android:label="Coupon Details"
tools:layout="@layout/fragment_coupon_detail">
<argument
android:name="couponId"
app:argType="long"
android:defaultValue="-1L" />
</fragment>
</navigation>

View File

@@ -0,0 +1,65 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.dto.common.CouponRequest;
import com.petshop.backend.dto.common.CouponResponse;
import com.petshop.backend.service.CouponService;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/coupons")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public class CouponController {
private final CouponService couponService;
public CouponController(CouponService couponService) {
this.couponService = couponService;
}
@GetMapping
public ResponseEntity<Page<CouponResponse>> getAllCoupons(
@RequestParam(required = false) String q,
@RequestParam(required = false) Boolean active,
Pageable pageable) {
return ResponseEntity.ok(couponService.getAllCoupons(q, active, pageable));
}
@GetMapping("/{id}")
public ResponseEntity<CouponResponse> getCouponById(@PathVariable Long id) {
return ResponseEntity.ok(couponService.getCouponById(id));
}
@GetMapping("/code/{code}")
public ResponseEntity<CouponResponse> getCouponByCode(@PathVariable String code) {
return ResponseEntity.ok(couponService.getCouponByCode(code));
}
@PostMapping
public ResponseEntity<CouponResponse> createCoupon(@Valid @RequestBody CouponRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(couponService.createCoupon(request));
}
@PutMapping("/{id}")
public ResponseEntity<CouponResponse> updateCoupon(@PathVariable Long id, @Valid @RequestBody CouponRequest request) {
return ResponseEntity.ok(couponService.updateCoupon(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCoupon(@PathVariable Long id) {
couponService.deleteCoupon(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
public ResponseEntity<Void> bulkDeleteCoupons(@Valid @RequestBody BulkDeleteRequest request) {
couponService.bulkDeleteCoupons(request);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,53 @@
package com.petshop.backend.dto.common;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class CouponRequest {
@NotBlank(message = "Coupon code is required")
private String couponCode;
@NotBlank(message = "Discount type is required")
private String discountType;
@NotNull(message = "Discount value is required")
@Positive(message = "Discount value must be positive")
private BigDecimal discountValue;
private BigDecimal minOrderAmount;
private Boolean active = true;
private LocalDateTime startsAt;
private LocalDateTime endsAt;
private Integer usageLimit;
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 LocalDateTime getStartsAt() { return startsAt; }
public void setStartsAt(LocalDateTime startsAt) { this.startsAt = startsAt; }
public LocalDateTime getEndsAt() { return endsAt; }
public void setEndsAt(LocalDateTime endsAt) { this.endsAt = endsAt; }
public Integer getUsageLimit() { return usageLimit; }
public void setUsageLimit(Integer usageLimit) { this.usageLimit = usageLimit; }
}

View File

@@ -0,0 +1,65 @@
package com.petshop.backend.dto.common;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class CouponResponse {
private Long couponId;
private String couponCode;
private String discountType;
private BigDecimal discountValue;
private BigDecimal minOrderAmount;
private Boolean active;
private LocalDateTime startsAt;
private LocalDateTime endsAt;
private Integer usageLimit;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public CouponResponse(Long couponId, String couponCode, String discountType, BigDecimal discountValue, BigDecimal minOrderAmount, Boolean active, LocalDateTime startsAt, LocalDateTime endsAt, Integer usageLimit, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.couponId = couponId;
this.couponCode = couponCode;
this.discountType = discountType;
this.discountValue = discountValue;
this.minOrderAmount = minOrderAmount;
this.active = active;
this.startsAt = startsAt;
this.endsAt = endsAt;
this.usageLimit = usageLimit;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
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 LocalDateTime getStartsAt() { return startsAt; }
public void setStartsAt(LocalDateTime startsAt) { this.startsAt = startsAt; }
public LocalDateTime getEndsAt() { return endsAt; }
public void setEndsAt(LocalDateTime endsAt) { this.endsAt = endsAt; }
public Integer getUsageLimit() { return usageLimit; }
public void setUsageLimit(Integer usageLimit) { this.usageLimit = usageLimit; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

View File

@@ -1,7 +1,11 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.Coupon;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@@ -10,4 +14,9 @@ import java.util.Optional;
public interface CouponRepository extends JpaRepository<Coupon, Long> {
Optional<Coupon> findByCouponCode(String couponCode);
@Query("SELECT c FROM Coupon c WHERE " +
"(:q IS NULL OR LOWER(c.couponCode) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:active IS NULL OR c.active = :active)")
Page<Coupon> searchCoupons(@Param("q") String query, @Param("active") Boolean active, Pageable pageable);
}

View File

@@ -0,0 +1,106 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.dto.common.CouponRequest;
import com.petshop.backend.dto.common.CouponResponse;
import com.petshop.backend.entity.Coupon;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.CouponRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CouponService {
private final CouponRepository couponRepository;
public CouponService(CouponRepository couponRepository) {
this.couponRepository = couponRepository;
}
public Page<CouponResponse> getAllCoupons(String query, Boolean active, Pageable pageable) {
return couponRepository.searchCoupons(query, active, pageable).map(this::mapToResponse);
}
public CouponResponse getCouponById(Long id) {
Coupon coupon = couponRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Coupon not found with id: " + id));
return mapToResponse(coupon);
}
public CouponResponse getCouponByCode(String code) {
Coupon coupon = couponRepository.findByCouponCode(code)
.orElseThrow(() -> new ResourceNotFoundException("Coupon not found with code: " + code));
return mapToResponse(coupon);
}
@Transactional
public CouponResponse createCoupon(CouponRequest request) {
if (couponRepository.findByCouponCode(request.getCouponCode()).isPresent()) {
throw new IllegalArgumentException("Coupon code already exists: " + request.getCouponCode());
}
Coupon coupon = new Coupon();
updateCouponFields(coupon, request);
coupon = couponRepository.save(coupon);
return mapToResponse(coupon);
}
@Transactional
public CouponResponse updateCoupon(Long id, CouponRequest request) {
Coupon coupon = couponRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Coupon not found with id: " + id));
couponRepository.findByCouponCode(request.getCouponCode()).ifPresent(existing -> {
if (!existing.getCouponId().equals(id)) {
throw new IllegalArgumentException("Coupon code already exists: " + request.getCouponCode());
}
});
updateCouponFields(coupon, request);
coupon = couponRepository.save(coupon);
return mapToResponse(coupon);
}
@Transactional
public void deleteCoupon(Long id) {
if (!couponRepository.existsById(id)) {
throw new ResourceNotFoundException("Coupon not found with id: " + id);
}
couponRepository.deleteById(id);
}
@Transactional
public void bulkDeleteCoupons(BulkDeleteRequest request) {
couponRepository.deleteAllById(request.getIds());
}
private void updateCouponFields(Coupon coupon, CouponRequest request) {
coupon.setCouponCode(request.getCouponCode());
coupon.setDiscountType(request.getDiscountType());
coupon.setDiscountValue(request.getDiscountValue());
coupon.setMinOrderAmount(request.getMinOrderAmount());
coupon.setActive(request.getActive());
coupon.setStartsAt(request.getStartsAt());
coupon.setEndsAt(request.getEndsAt());
coupon.setUsageLimit(request.getUsageLimit());
}
private CouponResponse mapToResponse(Coupon coupon) {
return new CouponResponse(
coupon.getCouponId(),
coupon.getCouponCode(),
coupon.getDiscountType(),
coupon.getDiscountValue(),
coupon.getMinOrderAmount(),
coupon.getActive(),
coupon.getStartsAt(),
coupon.getEndsAt(),
coupon.getUsageLimit(),
coupon.getCreatedAt(),
coupon.getUpdatedAt()
);
}
}