Close chat #169

Closed
RecentRunner wants to merge 291 commits from close-chat into main
81 changed files with 3972 additions and 883 deletions
Showing only changes of commit 9d1ccb8e68 - Show all commits

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

@@ -2,13 +2,14 @@ package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.ConversationDTO;
import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.UpdateConversationStatusRequest;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
//api calls to get conversations
@@ -20,4 +21,7 @@ public interface ChatApi {
@GET("api/v1/chat/conversations/{conversationId}")
Call<ConversationDTO> getConversationById(@Path("conversationId") Long conversationId);
@PUT("api/v1/chat/conversations/{conversationId}")
Call<ConversationDTO> updateConversationStatus(@Path("conversationId") Long conversationId, @Body UpdateConversationStatusRequest request);
}

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

@@ -44,6 +44,9 @@ public interface PetApi {
@GET("api/v1/dropdowns/adoption-pets")
Call<List<DropdownDTO>> getAdoptionPets();
@GET("api/v1/dropdowns/pets")
Call<List<DropdownDTO>> getPetDropdowns();
// Get pet by id
@GET("api/v1/pets/{id}")
Call<PetDTO> getPetById(@Path("id") Long id);

View File

@@ -1,11 +1,14 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductDTO;
import okhttp3.MultipartBody;
import retrofit2.Call;
import retrofit2.http.*;
import java.util.List;
public interface ProductApi {
String PRODUCT_IMAGE_PATH = "api/v1/products/%d/image";
@@ -35,4 +38,10 @@ public interface ProductApi {
@DELETE("api/v1/products/{id}/image")
Call<Void> deleteProductImage(@Path("id") Long id);
@GET("api/v1/dropdowns/products")
Call<List<DropdownDTO>> getProductDropdowns();
@GET("api/v1/dropdowns/categories")
Call<List<DropdownDTO>> getCategoryDropdowns();
}

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

@@ -19,6 +19,7 @@ public interface SaleApi {
@Query("q") String query,
@Query("paymentMethod") String paymentMethod,
@Query("storeId") Long storeId,
@Query("isRefund") Boolean isRefund,
@Query("sort") String sort);
@GET("api/v1/sales/{id}")

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

@@ -2,8 +2,7 @@ package com.example.petstoremobile.dtos;
public class EmployeeDTO {
private long EmployeeId;
private Long userId;
private Long id;
private String username;
private String firstName;
private String lastName;
@@ -11,16 +10,18 @@ public class EmployeeDTO {
private String email;
private String phone;
private String role;
private String staffRole;
private Boolean active;
private String createAt;
private Integer loyaltyPoints;
private Long primaryStoreId;
private String createdAt;
private String updatedAt;
private String password;
// Constructor for create and update the employee
public EmployeeDTO() {}
public EmployeeDTO(String username, String password, String firstName, String lastName,
String email, String phone, String role, boolean active) {
String email, String phone, String role, String staffRole, boolean active, Long primaryStoreId) {
this.username = username;
this.password = password;
this.firstName = firstName;
@@ -28,75 +29,128 @@ public class EmployeeDTO {
this.email = email;
this.phone = phone;
this.role = role;
this.staffRole = staffRole;
this.active = active;
}
// password field for request only
private String password;
public long getEmployeeId() {
return EmployeeId;
this.primaryStoreId = primaryStoreId;
}
public Long getUserId() {
public Long getId() {
return id;
}
return userId;
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public String getFirstName() {
public void setUsername(String username) {
this.username = username;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public String getFullName() {
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getFullName() {
return fullName;
}
public String getEmail() {
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getEmail() {
return email;
}
public String getPhone() {
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public String getRole() {
public void setPhone(String phone) {
this.phone = phone;
}
public String getRole() {
return role;
}
public Boolean getActive() {
public void setRole(String role) {
this.role = role;
}
public String getStaffRole() {
return staffRole;
}
public void setStaffRole(String staffRole) {
this.staffRole = staffRole;
}
public Boolean getActive() {
return active;
}
public String getCreateAt() {
public void setActive(Boolean active) {
this.active = active;
}
return createAt;
public Integer getLoyaltyPoints() {
return loyaltyPoints;
}
public void setLoyaltyPoints(Integer loyaltyPoints) {
this.loyaltyPoints = loyaltyPoints;
}
public Long getPrimaryStoreId() {
return primaryStoreId;
}
public void setPrimaryStoreId(Long primaryStoreId) {
this.primaryStoreId = primaryStoreId;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getUpdatedAt() {
return updatedAt;
}
public String getPassword() {
public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -9,6 +9,8 @@ public class SaleDTO {
private String saleDate;
private Long employeeId;
private String employeeName;
private Long customerId;
private String customerName;
private Long storeId;
private String storeName;
private BigDecimal totalAmount;
@@ -25,9 +27,6 @@ public class SaleDTO {
private List<SaleItemDTO> items;
private String createdAt;
// Request fields
private Long customerId;
// Constructor for create request
public SaleDTO(Long storeId, String paymentMethod, List<SaleItemDTO> items,
Boolean isRefund, Long originalSaleId, Long customerId) {
@@ -119,6 +118,10 @@ public class SaleDTO {
return customerId;
}
public String getCustomerName() {
return customerName;
}
// Nested SaleItemDTO
public static class SaleItemDTO {
private Long saleItemId;

View File

@@ -0,0 +1,17 @@
package com.example.petstoremobile.dtos;
public class UpdateConversationStatusRequest {
private String status;
public UpdateConversationStatusRequest(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}

View File

@@ -127,6 +127,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
binding.btnAttach.setOnClickListener(v -> selectAttachment());
binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment());
binding.btnCloseChat.setOnClickListener(v -> closeChat());
setupDrawerToggles();
setupRecyclerViews();
@@ -356,6 +357,30 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
viewModel.loadMessageHistory(activeConversationId);
}
private void closeChat() {
if (activeConversationId == null) return;
DialogUtils.showConfirmDialog(requireContext(), "Close Chat",
"Are you sure you want to close this chat? This will notify the customer.", () -> {
viewModel.sendMessage(activeConversationId, "The Chat has been closed").observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS) {
viewModel.addMessageLocally(resource.data);
viewModel.closeConversation(activeConversationId).observe(getViewLifecycleOwner(), statusResource -> {
if (statusResource == null) return;
setLoading(statusResource.status == Resource.Status.LOADING);
if (statusResource.status == Resource.Status.SUCCESS) {
viewModel.loadConversations();
setConversationActive(true, "CLOSED");
} else if (statusResource.status == Resource.Status.ERROR) {
Toast.makeText(requireContext(), "Failed to close chat: " + statusResource.message, Toast.LENGTH_SHORT).show();
}
});
}
});
});
}
private void sendMessage() {
if (activeConversationId == null) return;
String text = binding.etMessage.getText().toString().trim();
@@ -489,6 +514,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
private void setConversationActive(boolean active, String status) {
boolean isClosed = "CLOSED".equalsIgnoreCase(status);
UIUtils.setViewsEnabled(active && !isClosed, binding.btnSend, binding.etMessage, binding.btnAttach);
binding.btnCloseChat.setVisibility(active && !isClosed ? View.VISIBLE : View.GONE);
if (!active) {
activeConversationId = null;

View File

@@ -43,15 +43,13 @@ public class ListFragment extends Fragment {
// Check user role and restrict access for STAFF
String role = tokenManager.getRole();
if ("STAFF".equalsIgnoreCase(role)) {
binding.drawerSuppliers.setVisibility(View.GONE);
binding.drawerInventory.setVisibility(View.GONE);
}
// Only show for ADMIN
if ("ADMIN".equalsIgnoreCase(role)) {
binding.sectionAdmin.setVisibility(View.GONE);
} else if ("ADMIN".equalsIgnoreCase(role)) {
binding.sectionAdmin.setVisibility(View.VISIBLE);
binding.drawerStaff.setVisibility(View.VISIBLE);
} else {
binding.drawerStaff.setVisibility(View.GONE);
// Default or other roles
binding.sectionAdmin.setVisibility(View.GONE);
}
//add Listeners to the drawer so user won't be able to interact with the innerContainer (the list fragments)
@@ -92,6 +90,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

@@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AnalyticsViewModel;
import dagger.hilt.android.AndroidEntryPoint;
@@ -20,6 +21,10 @@ public class AnalyticsFragment extends Fragment {
private FragmentAnalyticsBinding binding;
private AnalyticsViewModel viewModel;
private boolean filtersExpanded = false;
private static final String[] TOP_N_OPTIONS = {"5", "10", "15", "20"};
private static final int[] TOP_N_VALUES = { 5, 10, 15, 20 };
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
@@ -27,19 +32,114 @@ public class AnalyticsFragment extends Fragment {
binding = FragmentAnalyticsBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class);
setupFilterPanel();
observeViewModel();
viewModel.loadAnalytics();
binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics());
UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this);
return binding.getRoot();
}
// Filter Panel
private void setupFilterPanel() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerTopN, TOP_N_OPTIONS);
// Toggle expand/collapse
binding.rowFilterHeader.setOnClickListener(v -> toggleFilters());
// Date pickers
binding.etFilterStartDate.setOnClickListener(v ->
UIUtils.showDatePicker(requireContext(), binding.etFilterStartDate, this::updateFilterSummary));
binding.etFilterEndDate.setOnClickListener(v ->
UIUtils.showDatePicker(requireContext(), binding.etFilterEndDate, this::updateFilterSummary));
// Quick presets
binding.btnPresetToday.setOnClickListener(v -> applyPreset(0, 0));
binding.btnPreset7D.setOnClickListener(v -> applyPreset(-6, 0));
binding.btnPreset30D.setOnClickListener(v -> applyPreset(-29, 0));
binding.btnPreset3M.setOnClickListener(v -> applyPreset(-89, 0));
binding.btnPreset1Y.setOnClickListener(v -> applyPreset(-364, 0));
binding.btnPresetAll.setOnClickListener(v -> {
binding.etFilterStartDate.setText("");
binding.etFilterEndDate.setText("");
updateFilterSummary();
});
binding.btnFilterApply.setOnClickListener(v -> applyFiltersFromUI());
binding.btnFilterReset.setOnClickListener(v -> resetFilters());
}
private void toggleFilters() {
filtersExpanded = !filtersExpanded;
binding.llFilterContent.setVisibility(filtersExpanded ? View.VISIBLE : View.GONE);
binding.tvFilterToggleIcon.setText(filtersExpanded ? "" : "");
}
private void applyPreset(int startOffset, int endOffset) {
binding.etFilterStartDate.setText(getDateString(startOffset));
binding.etFilterEndDate.setText(getDateString(endOffset));
updateFilterSummary();
applyFiltersFromUI();
}
private void applyFiltersFromUI() {
AnalyticsViewModel.FilterState filter = new AnalyticsViewModel.FilterState();
filter.startDate = binding.etFilterStartDate.getText().toString().trim();
filter.endDate = binding.etFilterEndDate.getText().toString().trim();
Object pm = binding.spinnerFilterPayment.getSelectedItem();
filter.paymentMethod = pm != null ? pm.toString() : "All";
int topNPos = binding.spinnerTopN.getSelectedItemPosition();
filter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5;
updateFilterSummary();
viewModel.applyFilter(filter);
}
private void resetFilters() {
binding.etFilterStartDate.setText("");
binding.etFilterEndDate.setText("");
binding.spinnerTopN.setSelection(0);
// Reset payment method to "All"
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, "All");
updateFilterSummary();
viewModel.resetFilter();
}
private void updateFilterSummary() {
String start = binding.etFilterStartDate.getText().toString().trim();
String end = binding.etFilterEndDate.getText().toString().trim();
if (start.isEmpty() && end.isEmpty()) {
binding.tvFilterSummary.setText("All time");
} else if (start.isEmpty()) {
binding.tvFilterSummary.setText("Up to " + shortDate(end));
} else if (end.isEmpty()) {
binding.tvFilterSummary.setText("From " + shortDate(start));
} else {
binding.tvFilterSummary.setText(shortDate(start) + " " + shortDate(end));
}
}
private String shortDate(String date) {
return (date != null && date.length() >= 10) ? date.substring(5) : date;
}
private String getDateString(int offsetDays) {
Calendar c = Calendar.getInstance();
c.add(Calendar.DAY_OF_YEAR, offsetDays);
return String.format(Locale.US, "%04d-%02d-%02d",
c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH));
}
// ViewModel Observation
private void observeViewModel() {
viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay);
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
if (loading) {
@@ -53,6 +153,15 @@ public class AnalyticsFragment extends Fragment {
viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> {
if (error != null) showError(error);
});
viewModel.getAvailablePaymentMethods().observe(getViewLifecycleOwner(), methods -> {
if (methods == null || methods.isEmpty()) return;
String currentSelection = binding.spinnerFilterPayment.getSelectedItem() != null
? binding.spinnerFilterPayment.getSelectedItem().toString() : "All";
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerFilterPayment,
methods.toArray(new String[0]));
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, currentSelection);
});
}
@Override
@@ -61,10 +170,12 @@ public class AnalyticsFragment extends Fragment {
binding = null;
}
// Display
private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) {
if (data == null) return;
// Summary
// Summary cards
binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP));
binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions));
binding.tvAvgTransaction.setText("$" + data.avgTransaction);
@@ -73,11 +184,12 @@ public class AnalyticsFragment extends Fragment {
// Top Revenue Products
binding.llTopRevenue.removeAllViews();
if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) {
BigDecimal maxRevenue = data.topRevenueProducts.get(0).getValue();
if (maxRevenue.compareTo(BigDecimal.ZERO) == 0) maxRevenue = BigDecimal.ONE;
BigDecimal maxRev = data.topRevenueProducts.get(0).getValue();
if (maxRev.compareTo(BigDecimal.ZERO) == 0) maxRev = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.topRevenueProducts) {
addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35");
addBarRow(binding.llTopRevenue, e.getKey(),
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxRev.floatValue(), "#ff6b35");
}
} else {
addEmptyRow(binding.llTopRevenue, "No data");
@@ -99,15 +211,13 @@ public class AnalyticsFragment extends Fragment {
// Payment Methods
binding.llPaymentMethods.removeAllViews();
if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) {
int maxPayment = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1);
String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
int maxPay = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1);
String[] payColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
int ci = 0;
for (Map.Entry<String, Integer> e : data.paymentMethodStats) {
addBarRow(binding.llPaymentMethods, e.getKey(),
e.getValue() + " transactions",
(float) e.getValue() / maxPayment,
paymentColors[ci % paymentColors.length]);
ci++;
(float) e.getValue() / maxPay, payColors[ci++ % payColors.length]);
}
} else {
addEmptyRow(binding.llPaymentMethods, "No data");
@@ -116,36 +226,37 @@ public class AnalyticsFragment extends Fragment {
// Employee Performance
binding.llEmployeePerformance.removeAllViews();
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
BigDecimal maxEmp = data.employeePerformance.get(data.employeePerformance.size() - 1).getValue();
BigDecimal maxEmp = data.employeePerformance.get(0).getValue();
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
maxEmp = data.employeePerformance.get(0).getValue();
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
addBarRow(binding.llEmployeePerformance, e.getKey(),
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxEmp.floatValue(),
"#1a759f");
e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f");
}
} else {
addEmptyRow(binding.llEmployeePerformance, "No data");
}
// Daily Revenue
binding.tvDailyRevenueTitle.setText(data.dailyRevenueTitle);
binding.llDailyRevenue.removeAllViews();
if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) {
BigDecimal maxDaily = data.dailyRevenue.stream().map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE);
BigDecimal maxDaily = data.dailyRevenue.stream()
.map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE);
if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.dailyRevenue) {
String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey();
addBarRow(binding.llDailyRevenue, label,
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxDaily.floatValue(),
"#ff6b35");
e.getValue().floatValue() / maxDaily.floatValue(), "#ff6b35");
}
} else {
addEmptyRow(binding.llDailyRevenue, "No data");
}
}
// Chart Helpers
private void addBarRow(LinearLayout parent, String label, String value, float ratio, String color) {
if (getContext() == null) return;
LinearLayout row = new LinearLayout(getContext());
@@ -156,8 +267,7 @@ public class AnalyticsFragment extends Fragment {
labelRow.setOrientation(LinearLayout.HORIZONTAL);
TextView tvLabel = new TextView(getContext());
tvLabel.setLayoutParams(new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
tvLabel.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
tvLabel.setText(label);
tvLabel.setTextColor(Color.parseColor("#444441"));
tvLabel.setTextSize(13f);
@@ -172,22 +282,19 @@ public class AnalyticsFragment extends Fragment {
labelRow.addView(tvValue);
LinearLayout barBg = new LinearLayout(getContext());
LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, 12);
LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 12);
bgParams.setMargins(0, 4, 0, 0);
barBg.setLayoutParams(bgParams);
barBg.setBackgroundColor(Color.parseColor("#EEEEEE"));
float safeRatio = Math.max(0f, Math.min(1f, ratio));
View barFill = new View(getContext());
LinearLayout.LayoutParams fillParams = new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.MATCH_PARENT, ratio);
barFill.setLayoutParams(fillParams);
barFill.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, safeRatio));
barFill.setBackgroundColor(Color.parseColor(color));
barBg.addView(barFill);
View spacer = new View(getContext());
spacer.setLayoutParams(new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - ratio));
spacer.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - safeRatio));
barBg.addView(spacer);
row.addView(labelRow);
@@ -205,8 +312,7 @@ public class AnalyticsFragment extends Fragment {
}
private void showError(String msg) {
if (getContext() == null || binding == null)
return;
if (getContext() == null || binding == null) return;
binding.tvTotalRevenue.setText("Error");
binding.tvTotalTransactions.setText("");
binding.tvAvgTransaction.setText("");

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

@@ -16,7 +16,7 @@ import android.view.ViewGroup;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ProductAdapter;
import com.example.petstoremobile.databinding.FragmentProductBinding;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
@@ -74,7 +74,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
viewModel.getCategories().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, list,
CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId);
DropdownDTO::getLabel, "All Categories", -1L, DropdownDTO::getId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
@@ -111,9 +111,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
if (query.isEmpty()) query = null;
Long categoryId = null;
List<CategoryDTO> categories = viewModel.getCategories().getValue();
List<DropdownDTO> categories = viewModel.getCategories().getValue();
if (binding.spinnerCategory.getSelectedItemPosition() > 0 && categories != null && !categories.isEmpty()) {
categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId();
categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getId();
}
viewModel.loadProducts(query, categoryId);

View File

@@ -51,6 +51,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
setupSearch();
setupStoreFilter();
setupPaymentMethodFilter();
setupRefundStatusFilter();
setupSwipeRefresh();
setupFilterToggle();
observeViewModel();
@@ -75,7 +76,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId);
StoreDTO::getStoreName, "All Stores", null, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
@@ -91,7 +92,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale,
binding.spinnerPaymentMethod, binding.spinnerStore);
binding.spinnerPaymentMethod, binding.spinnerStore, binding.spinnerRefundStatus);
}
private void setupStoreFilter() {
@@ -99,10 +100,15 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
}
private void setupPaymentMethodFilter() {
String[] paymentMethods = {"Payments", "Cash", "Card"};
String[] paymentMethods = {"All Payments", "Cash", "Card"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerPaymentMethod, paymentMethods, () -> loadSales(true));
}
private void setupRefundStatusFilter() {
String[] refundStatuses = {"All Status", "Sale", "Refund"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerRefundStatus, refundStatuses, () -> loadSales(true));
}
private void setupRecyclerView() {
adapter = new SaleAdapter(saleList, this);
binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext()));
@@ -149,7 +155,12 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
viewModel.loadSales(reset, query, paymentMethod, storeId);
Boolean isRefund = null;
if (binding.spinnerRefundStatus.getSelectedItemPosition() > 0) {
isRefund = binding.spinnerRefundStatus.getSelectedItemPosition() == 2;
}
viewModel.loadSales(reset, query, paymentMethod, storeId, isRefund);
}
@Override
@@ -159,6 +170,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
Bundle args = new Bundle();
if (sale.getSaleId() != null) {
args.putLong("saleId", sale.getSaleId());
args.putBoolean("viewOnly", true);
}
if (sale.getIsRefund() != null) {
args.putBoolean("isRefund", sale.getIsRefund());

View File

@@ -56,11 +56,14 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
setupFilterToggle();
setupBulkDelete();
observeViewModel();
loadServices(true);
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
binding.fabAddService.setOnClickListener(v ->
NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail));
return binding.getRoot();
}
@@ -156,4 +159,4 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic
bulkDeleteHandler.onSelectionChanged(count);
}
}
}
}

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() {
@@ -76,7 +119,7 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
Bundle args = new Bundle();
if (position != -1) {
EmployeeDTO e = staffList.get(position);
args.putLong("employeeId", e.getEmployeeId());
args.putLong("employeeId", e.getId());
args.putString("username", e.getUsername() != null ? e.getUsername() : "");
args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : "");
args.putString("lastName", e.getLastName() != null ? e.getLastName() : "");

View File

@@ -32,13 +32,7 @@ public class AdoptionDetailFragment extends Fragment {
private FragmentAdoptionDetailBinding binding;
private AdoptionDetailViewModel viewModel;
private long preselectedPetId = -1;
private long preselectedCustomerId = -1;
private long preselectedStoreId = -1;
private long preselectedEmployeeId = -1;
private final String[] STATUSES = {"Pending", "Completed", "Cancelled"};
private boolean isUpdatingUI = false;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -59,7 +53,9 @@ public class AdoptionDetailFragment extends Fragment {
setupSpinners();
setupDatePicker();
observeViewModel();
loadSpinnersData();
Bundle args = getArguments();
boolean isEditing = args != null && args.containsKey("adoptionId");
viewModel.loadInitialFormData(isEditing);
handleArguments();
binding.btnAdoptionBack.setOnClickListener(v -> navigateBack());
@@ -68,14 +64,39 @@ public class AdoptionDetailFragment extends Fragment {
}
private void observeViewModel() {
viewModel.getPetList().observe(getViewLifecycleOwner(), list -> refreshPetSpinner());
viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> refreshCustomerSpinner());
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner());
viewModel.getEmployeeList().observe(getViewLifecycleOwner(), list -> refreshEmployeeSpinner());
viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState);
viewModel.getPetList().observe(getViewLifecycleOwner(), list -> {
AdoptionDetailViewModel.ViewState state = viewModel.getViewState().getValue();
Long petId = state != null ? state.selectedPetId : null;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, list,
DropdownDTO::getLabel, "-- Select Pet --", petId, DropdownDTO::getId);
});
viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> {
AdoptionDetailViewModel.ViewState state = viewModel.getViewState().getValue();
Long customerId = state != null ? state.selectedCustomerId : null;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, list,
DropdownDTO::getLabel, "-- Select Customer --", customerId, DropdownDTO::getId);
});
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> {
AdoptionDetailViewModel.ViewState state = viewModel.getViewState().getValue();
Long storeId = state != null ? state.selectedStoreId : null;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, list,
DropdownDTO::getLabel, "-- Select Store --", storeId, DropdownDTO::getId);
});
viewModel.getEmployeeList().observe(getViewLifecycleOwner(), list -> {
AdoptionDetailViewModel.ViewState state = viewModel.getViewState().getValue();
Long employeeId = state != null ? state.selectedEmployeeId : null;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, list,
DropdownDTO::getLabel, "-- Select Staff --", employeeId, DropdownDTO::getId);
});
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
if (binding != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@@ -87,21 +108,12 @@ public class AdoptionDetailFragment extends Fragment {
}
private void setupSpinners() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES);
UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet);
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, new String[]{});
binding.spinnerAdoptionCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position > 0) {
UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet);
} else {
if (!viewModel.isEditing()) {
binding.spinnerAdoptionPet.setSelection(0);
UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet);
}
}
viewModel.onCustomerSelected(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
@@ -110,127 +122,97 @@ public class AdoptionDetailFragment extends Fragment {
binding.spinnerAdoptionStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position > 0 && viewModel.getStoreList().getValue() != null && position <= viewModel.getStoreList().getValue().size()) {
DropdownDTO selectedStore = viewModel.getStoreList().getValue().get(position - 1);
loadEmployees(selectedStore.getId());
} else {
viewModel.setEmployeeList(new ArrayList<>());
}
viewModel.onStoreSelected(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAdoptionPet, p -> viewModel.onPetSelected(p));
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAdoptionStatus, p -> notifyDateStatusChange());
}
private void setupDatePicker() {
binding.etAdoptionDate.setOnClickListener(v -> UIUtils.showDatePicker(requireContext(), binding.etAdoptionDate, null));
binding.etAdoptionDate.setOnClickListener(v ->
UIUtils.showDatePicker(requireContext(), binding.etAdoptionDate, this::notifyDateStatusChange));
}
private void loadSpinnersData() {
viewModel.loadPets().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setPetList(resource.data);
}
});
viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setCustomerList(resource.data);
}
});
viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setStoreList(resource.data);
}
});
}
private void refreshPetSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, viewModel.getPetList().getValue(),
DropdownDTO::getLabel, "-- Select Pet --",
preselectedPetId, DropdownDTO::getId);
}
private void refreshCustomerSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, viewModel.getCustomerList().getValue(),
DropdownDTO::getLabel, "-- Select Customer --",
preselectedCustomerId, DropdownDTO::getId);
}
private void refreshStoreSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, viewModel.getStoreList().getValue(),
DropdownDTO::getLabel, "-- Select Store --",
preselectedStoreId, DropdownDTO::getId);
}
private void loadEmployees(Long storeId) {
viewModel.loadEmployees(storeId).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setEmployeeList(resource.data);
}
});
}
private void refreshEmployeeSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, viewModel.getEmployeeList().getValue(),
DropdownDTO::getLabel, "-- Select Staff --",
preselectedEmployeeId, DropdownDTO::getId);
private void notifyDateStatusChange() {
if (isUpdatingUI) return;
String date = binding.etAdoptionDate.getText().toString();
Object selected = binding.spinnerAdoptionStatus.getSelectedItem();
String status = selected != null ? selected.toString() : "";
viewModel.onDateChanged(date, status);
}
private void handleArguments() {
Bundle a = getArguments();
if (a != null && a.containsKey("adoptionId")) {
long adoptionId = a.getLong("adoptionId");
viewModel.setAdoptionId(adoptionId);
binding.tvAdoptionMode.setText("Edit Adoption");
binding.tvAdoptionId.setText(DateTimeUtils.formatId(adoptionId));
binding.tvAdoptionId.setVisibility(View.VISIBLE);
binding.btnDeleteAdoption.setVisibility(View.VISIBLE);
viewModel.setAdoptionId(a.getLong("adoptionId"));
loadAdoptionData();
} else {
viewModel.setAdoptionId(-1);
binding.tvAdoptionMode.setText("Add Adoption");
binding.btnDeleteAdoption.setVisibility(View.GONE);
binding.tvAdoptionId.setVisibility(View.GONE);
UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet);
return;
}
viewModel.setAdoptionId(-1);
}
private void loadAdoptionData() {
viewModel.loadAdoption().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
AdoptionDTO a = resource.data;
preselectedPetId = a.getPetId() != null ? a.getPetId() : -1;
preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1;
preselectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1;
preselectedEmployeeId = a.getEmployeeId() != null ? a.getEmployeeId() : -1;
binding.etAdoptionDate.setText(a.getAdoptionDate());
binding.etAdoptionFee.setText(a.getAdoptionFee() != null ? a.getAdoptionFee().toString() : "");
SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getAdoptionStatus());
refreshPetSpinner();
refreshCustomerSpinner();
refreshStoreSpinner();
if (preselectedCustomerId != -1) {
UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet);
}
} else if (resource.status == Resource.Status.ERROR) {
if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
private void applyViewState(AdoptionDetailViewModel.ViewState state) {
isUpdatingUI = true;
binding.tvAdoptionMode.setText(state.modeTitle);
binding.tvAdoptionId.setText(DateTimeUtils.formatId(viewModel.getAdoptionId()));
binding.tvAdoptionId.setVisibility(state.isAdoptionIdVisible ? View.VISIBLE : View.GONE);
binding.btnDeleteAdoption.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE);
binding.btnSaveAdoption.setText(state.saveButtonText);
binding.btnSaveAdoption.setVisibility(state.isSaveVisible ? View.VISIBLE : View.GONE);
UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerAdoptionCustomer);
UIUtils.setViewsEnabled(state.isPetEnabled, binding.spinnerAdoptionPet);
UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerAdoptionStore);
UIUtils.setViewsEnabled(state.isEmployeeEnabled, binding.spinnerAdoptionEmployee);
UIUtils.setViewsEnabled(state.isDateEnabled, binding.etAdoptionDate);
UIUtils.setViewsEnabled(state.isFeeEnabled, binding.etAdoptionFee);
UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAdoptionStatus);
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, state.availableStatuses);
SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, state.selectedStatus);
if (!state.adoptionDate.isEmpty()) {
binding.etAdoptionDate.setText(state.adoptionDate);
}
if (!state.adoptionFee.isEmpty()) {
binding.etAdoptionFee.setText(state.adoptionFee);
}
// Re-populate spinners with updated preselected IDs
List<DropdownDTO> pets = viewModel.getPetList().getValue();
if (pets != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet,
pets, DropdownDTO::getLabel, "-- Select Pet --", state.selectedPetId, DropdownDTO::getId);
List<DropdownDTO> customers = viewModel.getCustomerList().getValue();
if (customers != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer,
customers, DropdownDTO::getLabel, "-- Select Customer --", state.selectedCustomerId, DropdownDTO::getId);
List<DropdownDTO> stores = viewModel.getStoreList().getValue();
if (stores != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore,
stores, DropdownDTO::getLabel, "-- Select Store --", state.selectedStoreId, DropdownDTO::getId);
List<DropdownDTO> employees = viewModel.getEmployeeList().getValue();
if (employees != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee,
employees, DropdownDTO::getLabel, "-- Select Staff --", state.selectedEmployeeId, DropdownDTO::getId);
isUpdatingUI = false;
}
private void saveAdoption() {
if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionCustomer, "Customer")) return;
if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionPet, "Pet")) return;
@@ -240,31 +222,24 @@ public class AdoptionDetailFragment extends Fragment {
BigDecimal fee = BigDecimal.ZERO;
String feeStr = binding.etAdoptionFee.getText().toString().trim();
if (!feeStr.isEmpty()) {
if (!InputValidator.isPositiveDecimal(binding.etAdoptionFee, "Adoption Fee")) return;
fee = new BigDecimal(feeStr);
try { fee = new BigDecimal(feeStr); } catch (NumberFormatException ignored) {}
}
DropdownDTO customer = viewModel.getCustomerList().getValue().get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1);
DropdownDTO pet = viewModel.getPetList().getValue().get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1);
DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1);
Long employeeId = null;
if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0) {
if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0 && viewModel.getEmployeeList().getValue() != null) {
employeeId = viewModel.getEmployeeList().getValue().get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId();
}
String adoptionDate = binding.etAdoptionDate.getText().toString().trim();
String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()];
String adoptionDate = binding.etAdoptionDate.getText().toString().trim();
Object selectedStatus = binding.spinnerAdoptionStatus.getSelectedItem();
String status = selectedStatus != null ? selectedStatus.toString().toUpperCase() : "";
AdoptionDTO dto = new AdoptionDTO(
pet.getId(),
customer.getId(),
employeeId,
store.getId(),
adoptionDate,
status,
fee
);
pet.getId(), customer.getId(), employeeId, store.getId(), adoptionDate, status, fee);
viewModel.saveAdoption(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;

View File

@@ -25,6 +25,8 @@ import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AppointmentDetailViewModel;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint;
/**
@@ -35,12 +37,6 @@ public class AppointmentDetailFragment extends Fragment {
private FragmentAppointmentDetailBinding binding;
private long preselectedPetId = -1;
private long preselectedServiceId = -1;
private long preselectedCustomerId = -1;
private long preselectedStoreId = -1;
private long preselectedStaffId = -1;
private final Integer[] HOURS = {9, 10, 11, 12, 13, 14, 15, 16, 17};
private final Integer[] MINUTES = {0, 15, 30, 45};
@@ -118,27 +114,42 @@ public class AppointmentDetailFragment extends Fragment {
}
private void setupDatePicker() {
binding.etAppointmentDate.setOnClickListener(v ->
binding.etAppointmentDate.setOnClickListener(v ->
UIUtils.showDatePicker(requireContext(), binding.etAppointmentDate, this::notifyDateTimeStatusChange));
}
private void observeViewModel() {
viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState);
viewModel.getCustomers().observe(getViewLifecycleOwner(), list ->
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, list, DropdownDTO::getLabel, "-- Select Customer --", preselectedCustomerId, DropdownDTO::getId));
viewModel.getStores().observe(getViewLifecycleOwner(), list ->
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, list, DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId));
viewModel.getServices().observe(getViewLifecycleOwner(), list ->
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, list, ServiceDTO::getServiceName, "-- Select Service --", preselectedServiceId, ServiceDTO::getServiceId));
viewModel.getCustomerPets().observe(getViewLifecycleOwner(), list ->
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, list, DropdownDTO::getLabel, "-- Select Pet --", preselectedPetId, DropdownDTO::getId));
viewModel.getCustomers().observe(getViewLifecycleOwner(), list -> {
AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue();
Long id = state != null ? state.selectedCustomerId : null;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, list, DropdownDTO::getLabel, "-- Select Customer --", id, DropdownDTO::getId);
});
viewModel.getStoreEmployees().observe(getViewLifecycleOwner(), list ->
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, list, DropdownDTO::getLabel, "-- Select Staff --", preselectedStaffId, DropdownDTO::getId));
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue();
Long id = state != null ? state.selectedStoreId : null;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, list, DropdownDTO::getLabel, "-- Select Store --", id, DropdownDTO::getId);
});
viewModel.getServices().observe(getViewLifecycleOwner(), list -> {
AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue();
Long id = state != null ? state.selectedServiceId : null;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, list, ServiceDTO::getServiceName, "-- Select Service --", id, ServiceDTO::getServiceId);
});
viewModel.getCustomerPets().observe(getViewLifecycleOwner(), list -> {
AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue();
Long id = state != null ? state.selectedPetId : null;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, list, DropdownDTO::getLabel, "-- Select Pet --", id, DropdownDTO::getId);
});
viewModel.getStoreEmployees().observe(getViewLifecycleOwner(), list -> {
AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue();
Long id = state != null ? state.selectedStaffId : null;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, list, DropdownDTO::getLabel, "-- Select Staff --", id, DropdownDTO::getId);
});
}
private void setLoading(boolean loading) {
@@ -155,7 +166,7 @@ public class AppointmentDetailFragment extends Fragment {
binding.tvAppointmentId.setVisibility(state.isEditing ? View.VISIBLE : View.GONE);
binding.btnDeleteAppointment.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE);
binding.btnSaveAppointment.setVisibility(state.isSaveVisible ? View.VISIBLE : View.GONE);
UIUtils.setFieldEnabled(state.isCustomerEnabled, binding.spinnerCustomer, binding.tvLabelCustomer);
UIUtils.setFieldEnabled(state.isStoreEnabled, binding.spinnerStore, binding.tvLabelStore);
UIUtils.setFieldEnabled(state.isPetEnabled, binding.spinnerPet, binding.tvLabelPet);
@@ -166,10 +177,29 @@ public class AppointmentDetailFragment extends Fragment {
UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerMinute);
UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAppointmentStatus);
Object selected = binding.spinnerAppointmentStatus.getSelectedItem();
String current = selected != null ? selected.toString() : "";
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, state.availableStatuses);
SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, current);
SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, state.selectedStatus);
// Re-populate dropdown spinners with current selected IDs from ViewState
List<DropdownDTO> customers = viewModel.getCustomers().getValue();
if (customers != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer,
customers, DropdownDTO::getLabel, "-- Select Customer --", state.selectedCustomerId, DropdownDTO::getId);
List<DropdownDTO> stores = viewModel.getStores().getValue();
if (stores != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore,
stores, DropdownDTO::getLabel, "-- Select Store --", state.selectedStoreId, DropdownDTO::getId);
List<ServiceDTO> services = viewModel.getServices().getValue();
if (services != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService,
services, ServiceDTO::getServiceName, "-- Select Service --", state.selectedServiceId, ServiceDTO::getServiceId);
List<DropdownDTO> pets = viewModel.getCustomerPets().getValue();
if (pets != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet,
pets, DropdownDTO::getLabel, "-- Select Pet --", state.selectedPetId, DropdownDTO::getId);
List<DropdownDTO> staff = viewModel.getStoreEmployees().getValue();
if (staff != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff,
staff, DropdownDTO::getLabel, "-- Select Staff --", state.selectedStaffId, DropdownDTO::getId);
isUpdatingUI = false;
}
@@ -200,20 +230,15 @@ public class AppointmentDetailFragment extends Fragment {
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
AppointmentDTO a = resource.data;
preselectedPetId = a.getPetId() != null ? a.getPetId() : -1;
preselectedServiceId = a.getServiceId() != null ? a.getServiceId() : -1;
preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1;
preselectedStoreId = a.getStoreId() != null ? a.getStoreId() : -1;
preselectedStaffId = a.getEmployeeId() != null ? a.getEmployeeId() : -1;
binding.etAppointmentDate.setText(a.getAppointmentDate());
parseAndSetTimeSpinners(a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00");
String status = a.getAppointmentStatus();
if (status != null && !status.isEmpty()) {
SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, DateTimeUtils.formatStatusFromBackend(status));
}
notifyDateTimeStatusChange();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load appointment", Toast.LENGTH_SHORT).show();
}
});
}

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

@@ -92,7 +92,7 @@ public class InventoryDetailFragment extends Fragment {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setProductList(resource.data.getContent());
viewModel.setProductList(resource.data);
}
});
}
@@ -105,8 +105,8 @@ public class InventoryDetailFragment extends Fragment {
private void refreshProductSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, viewModel.getProductList().getValue(),
ProductDTO::getProdName, "-- Select Product --",
preselectedProductId, ProductDTO::getProdId);
DropdownDTO::getLabel, "-- Select Product --",
preselectedProductId, DropdownDTO::getId);
}
private void handleArguments() {
@@ -156,9 +156,9 @@ public class InventoryDetailFragment extends Fragment {
int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim());
DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1);
ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1);
DropdownDTO product = viewModel.getProductList().getValue().get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1);
InventoryDTO request = new InventoryDTO(product.getProdId(), store.getId(), quantity);
InventoryDTO request = new InventoryDTO(product.getId(), store.getId(), quantity);
setButtonsEnabled(false);
viewModel.saveInventory(request).observe(getViewLifecycleOwner(), resource -> {

View File

@@ -41,9 +41,7 @@ public class PetDetailFragment extends Fragment {
private FragmentPetDetailBinding binding;
private PetDetailViewModel viewModel;
private Long selectedCustomerId = null;
private Long selectedStoreId = null;
private boolean isUpdatingUI = false;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -65,6 +63,7 @@ public class PetDetailFragment extends Fragment {
setupSpinner();
observeViewModel();
handleArguments();
viewModel.loadInitialFormData();
binding.btnBack.setOnClickListener(v -> navigateBack());
binding.btnSavePet.setOnClickListener(v -> savePet());
@@ -72,23 +71,18 @@ public class PetDetailFragment extends Fragment {
}
private void observeViewModel() {
viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> updateCustomerSpinnerSelection());
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> updateStoreSpinnerSelection());
viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setCustomerList(resource.data);
}
viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState);
viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> {
PetDetailViewModel.ViewState state = viewModel.getViewState().getValue();
Long selectedCustomerId = state != null ? state.selectedCustomerId : null;
updateCustomerSpinnerSelection(selectedCustomerId);
});
viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setStoreList(resource.data);
}
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> {
PetDetailViewModel.ViewState state = viewModel.getViewState().getValue();
Long selectedStoreId = state != null ? state.selectedStoreId : null;
updateStoreSpinnerSelection(selectedStoreId);
});
}
@@ -119,12 +113,12 @@ public class PetDetailFragment extends Fragment {
String status = binding.spinnerPetStatus.getSelectedItem().toString();
Long customerId = null;
if (binding.spinnerCustomer.getSelectedItemPosition() > 0) {
if (binding.spinnerCustomer.getSelectedItemPosition() > 0 && viewModel.getCustomerList().getValue() != null) {
customerId = viewModel.getCustomerList().getValue().get(binding.spinnerCustomer.getSelectedItemPosition() - 1).getId();
}
Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0) {
if (binding.spinnerStore.getSelectedItemPosition() > 0 && viewModel.getStoreList().getValue() != null) {
storeId = viewModel.getStoreList().getValue().get(binding.spinnerStore.getSelectedItemPosition() - 1).getId();
}
@@ -193,24 +187,12 @@ public class PetDetailFragment extends Fragment {
private void handleArguments() {
if (getArguments() != null && getArguments().containsKey("petId")) {
long petId = getArguments().getLong("petId");
viewModel.setPetId(petId);
binding.tvMode.setText("Edit Pet");
binding.tvPetId.setText(DateTimeUtils.formatId(petId));
binding.tvPetId.setVisibility(View.VISIBLE);
binding.btnDeletePet.setVisibility(View.VISIBLE);
UIUtils.setViewsEnabled(false, binding.etPetSpecies, binding.etPetBreed);
viewModel.setPetId(getArguments().getLong("petId"));
loadPetData();
} else {
viewModel.setPetId(-1);
binding.tvMode.setText("Add Pet");
binding.tvPetId.setVisibility(View.GONE);
binding.btnDeletePet.setVisibility(View.GONE);
binding.btnSavePet.setText("Add");
UIUtils.setViewsEnabled(true, binding.etPetSpecies, binding.etPetBreed);
return;
}
viewModel.setPetId(-1);
}
private void loadPetData() {
@@ -226,20 +208,13 @@ public class PetDetailFragment extends Fragment {
if (p.getPetPrice() != null) {
binding.etPetPrice.setText(String.format(Locale.getDefault(), "%.2f", p.getPetPrice()));
}
SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, p.getPetStatus());
selectedCustomerId = p.getCustomerId();
updateCustomerSpinnerSelection();
selectedStoreId = p.getStoreId();
updateStoreSpinnerSelection();
} else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load pet: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
private void updateCustomerSpinnerSelection() {
private void updateCustomerSpinnerSelection(Long selectedCustomerId) {
SpinnerUtils.populateSpinner(
requireContext(),
binding.spinnerCustomer,
@@ -251,7 +226,7 @@ public class PetDetailFragment extends Fragment {
);
}
private void updateStoreSpinnerSelection() {
private void updateStoreSpinnerSelection(Long selectedStoreId) {
SpinnerUtils.populateSpinner(
requireContext(),
binding.spinnerStore,
@@ -264,36 +239,76 @@ public class PetDetailFragment extends Fragment {
}
private void setupSpinner() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus,
new String[]{"Available", "Adopted", "Owned"});
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, new String[]{});
binding.spinnerPetStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String status = parent.getItemAtPosition(position).toString();
clearSpinnerError(binding.spinnerCustomer);
clearSpinnerError(binding.spinnerStore);
if ("Available".equalsIgnoreCase(status)) {
binding.spinnerCustomer.setSelection(0);
UIUtils.setViewsEnabled(false, binding.spinnerCustomer);
} else {
UIUtils.setViewsEnabled(true, binding.spinnerCustomer);
}
if ("Owned".equalsIgnoreCase(status)) {
binding.spinnerStore.setSelection(0);
UIUtils.setViewsEnabled(false, binding.spinnerStore);
} else {
UIUtils.setViewsEnabled(true, binding.spinnerStore);
}
if (isUpdatingUI) return;
viewModel.onCustomerSelected(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (isUpdatingUI) return;
viewModel.onStoreSelected(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
binding.spinnerPetStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (isUpdatingUI) return;
String status = parent.getItemAtPosition(position).toString();
clearSpinnerError(binding.spinnerCustomer);
clearSpinnerError(binding.spinnerStore);
viewModel.onStatusSelected(status);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
}
private void applyViewState(PetDetailViewModel.ViewState state) {
isUpdatingUI = true;
binding.tvMode.setText(state.modeTitle);
binding.tvPetId.setText(DateTimeUtils.formatId(viewModel.getPetId()));
binding.tvPetId.setVisibility(state.isPetIdVisible ? View.VISIBLE : View.GONE);
binding.btnDeletePet.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE);
binding.btnSavePet.setText(state.saveButtonText);
UIUtils.setViewsEnabled(state.isSpeciesEnabled, binding.etPetSpecies);
UIUtils.setViewsEnabled(state.isBreedEnabled, binding.etPetBreed);
UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer);
UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore);
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, state.availableStatuses);
SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, state.selectedStatus);
updateCustomerSpinnerSelection(state.selectedCustomerId);
updateStoreSpinnerSelection(state.selectedStoreId);
if (!state.isCustomerEnabled && binding.spinnerCustomer.getSelectedItemPosition() != 0) {
binding.spinnerCustomer.setSelection(0);
}
if (!state.isStoreEnabled && binding.spinnerStore.getSelectedItemPosition() != 0) {
binding.spinnerStore.setSelection(0);
}
isUpdatingUI = false;
}
private void clearSpinnerError(Spinner spinner) {

View File

@@ -110,12 +110,12 @@ public class ProductDetailFragment extends Fragment {
private void observeViewModel() {
viewModel.getCategoryList().observe(getViewLifecycleOwner(), list -> updateCategorySpinner());
viewModel.loadCategories().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setCategoryList(resource.data.getContent());
viewModel.setCategoryList(resource.data);
}
});
}
@@ -128,8 +128,8 @@ public class ProductDetailFragment extends Fragment {
private void updateCategorySpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, viewModel.getCategoryList().getValue(),
CategoryDTO::getCategoryName, "-- Select Category --",
preselectedCategoryId, CategoryDTO::getCategoryId);
DropdownDTO::getLabel, "-- Select Category --",
preselectedCategoryId, DropdownDTO::getId);
}
@Override
@@ -248,8 +248,8 @@ public class ProductDetailFragment extends Fragment {
String desc = binding.etProductDesc.getText().toString().trim();
BigDecimal price = new BigDecimal(binding.etProductPrice.getText().toString().trim());
CategoryDTO category = viewModel.getCategoryList().getValue().get(binding.spinnerProductCategory.getSelectedItemPosition() - 1);
ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price);
DropdownDTO category = viewModel.getCategoryList().getValue().get(binding.spinnerProductCategory.getSelectedItemPosition() - 1);
ProductDTO dto = new ProductDTO(name, category.getId(), desc, price);
viewModel.saveProduct(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
@@ -268,7 +268,7 @@ public class ProductDetailFragment extends Fragment {
}
private void confirmDelete() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () ->
DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () ->
viewModel.deleteProduct().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);

View File

@@ -40,7 +40,11 @@ public class SaleDetailFragment extends Fragment {
observeViewModel();
handleArguments();
if (!viewModel.isViewOnly()) {
if (viewModel.isViewOnly()) {
binding.llAddItemRow.setVisibility(View.GONE);
binding.btnSaveSale.setVisibility(View.GONE);
UIUtils.setViewsEnabled(false, binding.spinnerSaleStore, binding.spinnerSaleCustomer, binding.spinnerPaymentMethod);
} else {
loadData();
setupAddItem();
}
@@ -84,8 +88,9 @@ public class SaleDetailFragment extends Fragment {
binding.tvSaleMode.setText("Sale #" + saleId);
binding.tvSaleDetailId.setText("ID: " + saleId);
if (!a.getBoolean("isRefund", false)) {
binding.btnRefundSale.setVisibility(View.VISIBLE);
boolean isRefund = a.getBoolean("isRefund", false);
if (isRefund) {
binding.btnRefundSale.setVisibility(View.GONE);
}
if (viewOnly) {
@@ -96,6 +101,16 @@ public class SaleDetailFragment extends Fragment {
binding.spinnerPaymentMethod);
binding.llAddItemRow.setVisibility(View.GONE);
binding.llExtraInfo.setVisibility(View.VISIBLE);
binding.llCustomerInfo.setVisibility(View.VISIBLE);
binding.tvCustomerLabel.setVisibility(View.GONE);
binding.spinnerSaleCustomer.setVisibility(View.GONE);
binding.spinnerSaleStore.setVisibility(View.GONE);
binding.spinnerPaymentMethod.setVisibility(View.GONE);
binding.tvSaleStore.setVisibility(View.VISIBLE);
binding.tvSalePaymentMethod.setVisibility(View.VISIBLE);
// Show refund button only if it's not already a refund
binding.btnRefundSale.setVisibility(isRefund ? View.GONE : View.VISIBLE);
}
loadSaleDetails();
@@ -157,6 +172,13 @@ public class SaleDetailFragment extends Fragment {
binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "");
binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0));
binding.tvSaleStore.setText(sale.getStoreName() != null ? sale.getStoreName() : "");
binding.tvSaleCustomer.setText(sale.getCustomerName() != null ? sale.getCustomerName() : "No Customer");
binding.tvSalePaymentMethod.setText(sale.getPaymentMethod() != null ? sale.getPaymentMethod() : "");
if (sale.getIsRefund() != null && sale.getIsRefund()) {
binding.btnRefundSale.setVisibility(View.GONE);
}
SpinnerUtils.setSelectionByValue(binding.spinnerPaymentMethod, sale.getPaymentMethod());

View File

@@ -11,9 +11,9 @@ import androidx.navigation.fragment.NavHostFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Toast;
import com.example.petstoremobile.R;
import com.example.petstoremobile.databinding.FragmentServiceDetailBinding;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.utils.ActivityLogger;
@@ -21,6 +21,7 @@ import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ServiceDetailViewModel;
import dagger.hilt.android.AndroidEntryPoint;
@@ -51,6 +52,7 @@ public class ServiceDetailFragment extends Fragment {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
observeViewModel();
handleArguments();
binding.btnBack.setOnClickListener(v -> navigateBack());
@@ -58,8 +60,12 @@ public class ServiceDetailFragment extends Fragment {
binding.btnDeleteService.setOnClickListener(v -> deleteService());
}
private void observeViewModel() {
viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState);
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
if (binding != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@@ -126,34 +132,48 @@ public class ServiceDetailFragment extends Fragment {
private void handleArguments() {
if (getArguments() != null && getArguments().containsKey("serviceId")) {
long serviceId = getArguments().getLong("serviceId");
viewModel.setServiceId(serviceId);
binding.tvMode.setText("Edit Service");
binding.tvServiceId.setText(DateTimeUtils.formatId(serviceId));
binding.btnDeleteService.setVisibility(View.VISIBLE);
viewModel.setServiceId(getArguments().getLong("serviceId"));
loadServiceData();
} else {
viewModel.setServiceId(-1);
binding.tvMode.setText("Add Service");
binding.tvServiceId.setVisibility(View.GONE);
binding.btnDeleteService.setVisibility(View.GONE);
binding.btnSaveService.setText("Add");
return;
}
viewModel.setServiceId(-1);
}
private void loadServiceData() {
viewModel.loadService().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
ServiceDTO s = resource.data;
binding.etServiceName.setText(s.getServiceName());
binding.etServiceDesc.setText(s.getServiceDesc());
binding.etServiceDuration.setText(String.valueOf(s.getServiceDuration()));
binding.etServicePrice.setText(String.valueOf(s.getServicePrice()));
} else if (resource.status == Resource.Status.ERROR) {
if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load service: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
private void applyViewState(ServiceDetailViewModel.ViewState state) {
binding.tvMode.setText(state.modeTitle);
binding.tvServiceId.setText(DateTimeUtils.formatId(viewModel.getServiceId()));
binding.tvServiceId.setVisibility(state.isServiceIdVisible ? View.VISIBLE : View.GONE);
binding.btnDeleteService.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE);
binding.btnSaveService.setText(state.saveButtonText);
UIUtils.setViewsEnabled(state.isFieldsEnabled,
binding.etServiceName,
binding.etServiceDesc,
binding.etServiceDuration,
binding.etServicePrice);
updateIfDifferent(binding.etServiceName, state.serviceName);
updateIfDifferent(binding.etServiceDesc, state.serviceDesc);
updateIfDifferent(binding.etServiceDuration, state.serviceDuration);
updateIfDifferent(binding.etServicePrice, state.servicePrice);
}
private void updateIfDifferent(EditText field, String value) {
String current = field.getText() != null ? field.getText().toString() : "";
String next = value != null ? value : "";
if (!current.equals(next)) {
field.setText(next);
}
}
}

View File

@@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.R;
import com.example.petstoremobile.databinding.FragmentStaffDetailBinding;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator;
@@ -16,6 +17,9 @@ import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.StaffDetailViewModel;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
@@ -25,8 +29,11 @@ public class StaffDetailFragment extends Fragment {
private StaffDetailViewModel viewModel;
private final String[] ROLES = {"STAFF", "ADMIN"};
private final String[] STAFF_ROLES = {"STORE_MANAGER", "SALES_ASSOCIATE", "GROOMER", "VETERINARIAN"};
private final String[] STATUSES = {"Active", "Inactive"};
private long preselectedStoreId = -1;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -34,6 +41,8 @@ public class StaffDetailFragment extends Fragment {
viewModel = new ViewModelProvider(this).get(StaffDetailViewModel.class);
setupSpinners();
observeViewModel();
loadStores();
handleArguments();
binding.btnStaffBack.setOnClickListener(v -> navigateBack());
@@ -45,11 +54,32 @@ public class StaffDetailFragment extends Fragment {
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner());
}
private void setupSpinners() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffRole, ROLES);
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffType, STAFF_ROLES);
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES);
}
private void loadStores() {
viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
viewModel.setStoreList(resource.data);
}
});
}
private void refreshStoreSpinner() {
List<DropdownDTO> list = viewModel.getStoreList().getValue();
if (list == null) return;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaffStore, list,
DropdownDTO::getLabel, "-- Select Store --",
preselectedStoreId, DropdownDTO::getId);
}
private void handleArguments() {
Bundle a = getArguments();
if (a != null && a.getBoolean("isEditing", false)) {
@@ -59,16 +89,9 @@ public class StaffDetailFragment extends Fragment {
binding.tvStaffMode.setText("Edit Staff Account");
binding.tvStaffId.setText("ID: " + employeeId);
binding.tvStaffId.setVisibility(View.VISIBLE);
binding.etStaffUsername.setText(a.getString("username", ""));
binding.etStaffFirstName.setText(a.getString("firstName", ""));
binding.etStaffLastName.setText(a.getString("lastName", ""));
binding.etStaffEmail.setText(a.getString("email", ""));
binding.etStaffPhone.setText(a.getString("phone", ""));
binding.btnDeleteStaff.setVisibility(View.VISIBLE);
SpinnerUtils.setSelectionByValue(binding.spinnerStaffRole, a.getString("role", "STAFF"));
binding.spinnerStaffStatus.setSelection(a.getBoolean("active", true) ? 0 : 1);
loadEmployeeData(employeeId);
} else {
viewModel.setEmployeeId(-1, false);
binding.tvStaffMode.setText("Add Staff Account");
@@ -77,6 +100,29 @@ public class StaffDetailFragment extends Fragment {
}
}
private void loadEmployeeData(long id) {
viewModel.loadEmployee(id).observe(getViewLifecycleOwner(), resource -> {
if (resource != null) {
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
EmployeeDTO e = resource.data;
binding.etStaffUsername.setText(e.getUsername());
binding.etStaffFirstName.setText(e.getFirstName());
binding.etStaffLastName.setText(e.getLastName());
binding.etStaffEmail.setText(e.getEmail());
binding.etStaffPhone.setText(e.getPhone());
SpinnerUtils.setSelectionByValue(binding.spinnerStaffRole, e.getRole());
SpinnerUtils.setSelectionByValue(binding.spinnerStaffType, e.getStaffRole());
binding.spinnerStaffStatus.setSelection(Boolean.TRUE.equals(e.getActive()) ? 0 : 1);
preselectedStoreId = e.getPrimaryStoreId() != null ? e.getPrimaryStoreId() : -1;
refreshStoreSpinner();
}
}
});
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
@@ -100,6 +146,7 @@ public class StaffDetailFragment extends Fragment {
if (!InputValidator.isNotEmpty(binding.etStaffLastName, "Last Name")) return;
if (!InputValidator.isValidEmail(binding.etStaffEmail)) return;
if (!InputValidator.isValidPhone(binding.etStaffPhone)) return;
if (!InputValidator.isSpinnerSelected(binding.spinnerStaffStore, "Primary Store")) return;
String username = binding.etStaffUsername.getText().toString().trim();
String password = binding.etStaffPassword.getText().toString().trim();
@@ -108,7 +155,11 @@ public class StaffDetailFragment extends Fragment {
String email = binding.etStaffEmail.getText().toString().trim();
String phone = binding.etStaffPhone.getText().toString().trim();
String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()];
String staffRole = STAFF_ROLES[binding.spinnerStaffType.getSelectedItemPosition()];
boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0;
List<DropdownDTO> stores = viewModel.getStoreList().getValue();
Long storeId = stores.get(binding.spinnerStaffStore.getSelectedItemPosition() - 1).getId();
EmployeeDTO dto = new EmployeeDTO(
username,
@@ -118,7 +169,9 @@ public class StaffDetailFragment extends Fragment {
email,
phone,
role,
active
staffRole,
active,
storeId
);
viewModel.saveEmployee(dto).observe(getViewLifecycleOwner(), resource -> {

View File

@@ -11,11 +11,13 @@ import androidx.navigation.fragment.NavHostFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Toast;
import com.example.petstoremobile.databinding.FragmentSupplierDetailBinding;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.utils.ActivityLogger;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource;
@@ -51,6 +53,7 @@ public class SupplierDetailFragment extends Fragment {
super.onViewCreated(view, savedInstanceState);
UIUtils.formatPhoneInput(binding.etSupPhone);
observeViewModel();
handleArguments();
binding.btnBack.setOnClickListener(v -> navigateBack());
@@ -58,8 +61,12 @@ public class SupplierDetailFragment extends Fragment {
binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier());
}
private void observeViewModel() {
viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState);
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
if (binding != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@@ -129,36 +136,50 @@ public class SupplierDetailFragment extends Fragment {
private void handleArguments() {
if (getArguments() != null && getArguments().containsKey("supId")) {
long supId = getArguments().getLong("supId");
viewModel.setSupId(supId);
binding.tvMode.setText("Edit Supplier");
binding.tvSupId.setText("ID: " + supId);
binding.tvSupId.setVisibility(View.VISIBLE);
binding.btnDeleteSupplier.setVisibility(View.VISIBLE);
viewModel.setSupId(getArguments().getLong("supId"));
loadSupplierData();
} else {
viewModel.setSupId(-1);
binding.tvMode.setText("Add Supplier");
binding.tvSupId.setVisibility(View.GONE);
binding.btnDeleteSupplier.setVisibility(View.GONE);
binding.btnSaveSupplier.setText("Add");
return;
}
viewModel.setSupId(-1);
}
private void loadSupplierData() {
viewModel.loadSupplier().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
SupplierDTO s = resource.data;
binding.etSupCompany.setText(s.getSupCompany());
binding.etSupContactFirstName.setText(s.getSupContactFirstName());
binding.etSupContactLastName.setText(s.getSupContactLastName());
binding.etSupEmail.setText(s.getSupEmail());
binding.etSupPhone.setText(s.getSupPhone());
} else if (resource.status == Resource.Status.ERROR) {
if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Failed to load supplier: " + resource.message, Toast.LENGTH_SHORT).show();
}
});
}
private void applyViewState(SupplierDetailViewModel.ViewState state) {
binding.tvMode.setText(state.modeTitle);
binding.tvSupId.setText(DateTimeUtils.formatId(viewModel.getSupId()));
binding.tvSupId.setVisibility(state.isSupIdVisible ? View.VISIBLE : View.GONE);
binding.btnDeleteSupplier.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE);
binding.btnSaveSupplier.setText(state.saveButtonText);
UIUtils.setViewsEnabled(state.isFieldsEnabled,
binding.etSupCompany,
binding.etSupContactFirstName,
binding.etSupContactLastName,
binding.etSupEmail,
binding.etSupPhone);
updateIfDifferent(binding.etSupCompany, state.supCompany);
updateIfDifferent(binding.etSupContactFirstName, state.supFirstName);
updateIfDifferent(binding.etSupContactLastName, state.supLastName);
updateIfDifferent(binding.etSupEmail, state.supEmail);
updateIfDifferent(binding.etSupPhone, state.supPhone);
}
private void updateIfDifferent(EditText field, String value) {
String current = field.getText() != null ? field.getText().toString() : "";
String next = value != null ? value : "";
if (!current.equals(next)) {
field.setText(next);
}
}
}

View File

@@ -10,6 +10,7 @@ import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SendMessageRequest;
import com.example.petstoremobile.dtos.UpdateConversationStatusRequest;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
@@ -73,6 +74,13 @@ public class ChatRepository extends BaseRepository {
return executeCall(messageApi.downloadAttachment(messageId));
}
/**
* Updates the status of a conversation (e.g., OPEN to CLOSED).
*/
public LiveData<Resource<ConversationDTO>> updateConversationStatus(Long conversationId, UpdateConversationStatusRequest request) {
return executeCall(chatApi.updateConversationStatus(conversationId, request));
}
/**
* Fetches a paginated list of customers.
*/

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

@@ -47,6 +47,20 @@ public class PetRepository extends BaseRepository {
return executeCall(petApi.getAdoptionPets());
}
/**
* Retrieves all pets from the dropdowns API.
*/
public LiveData<Resource<List<DropdownDTO>>> getPetDropdowns() {
return executeCall(petApi.getPetDropdowns());
}
/**
* Retrieves available pets for a specific store.
*/
public LiveData<Resource<PageResponse<PetDTO>>> getAvailablePetsByStore(Long storeId) {
return executeCall(petApi.getAllPets(0, 200, null, "available", null, storeId, null, "petName"));
}
/**
* Retrieves a specific pet by its ID from the API.
*/

View File

@@ -3,10 +3,13 @@ package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import com.example.petstoremobile.api.ProductApi;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -70,4 +73,18 @@ public class ProductRepository extends BaseRepository {
public LiveData<Resource<Void>> deleteProductImage(Long id) {
return executeCall(productApi.deleteProductImage(id));
}
/**
* Retrieves a list of product dropdowns from the API.
*/
public LiveData<Resource<List<DropdownDTO>>> getProductDropdowns() {
return executeCall(productApi.getProductDropdowns());
}
/**
* Retrieves a list of category dropdowns from the API.
*/
public LiveData<Resource<List<DropdownDTO>>> getCategoryDropdowns() {
return executeCall(productApi.getCategoryDropdowns());
}
}

View File

@@ -20,8 +20,8 @@ public class SaleRepository extends BaseRepository {
this.saleApi = saleApi;
}
public LiveData<Resource<PageResponse<SaleDTO>>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, String sortBy) {
return executeCall(saleApi.getAllSales(page, size, query, paymentMethod, storeId, sortBy));
public LiveData<Resource<PageResponse<SaleDTO>>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, Boolean isRefund, String sortBy) {
return executeCall(saleApi.getAllSales(page, size, query, paymentMethod, storeId, isRefund, sortBy));
}
public LiveData<Resource<SaleDTO>> getSaleById(Long id) {

View File

@@ -63,6 +63,31 @@ public class DateTimeUtils {
}
}
/**
* Checks if a given date is strictly before today (today and future return false).
* format: date = "YYYY-MM-DD"
*/
public static boolean isDateBeforeToday(String date) {
if (date == null || date.isEmpty()) return false;
try {
String[] parts = date.split("-");
Calendar today = Calendar.getInstance();
today.set(Calendar.HOUR_OF_DAY, 0);
today.set(Calendar.MINUTE, 0);
today.set(Calendar.SECOND, 0);
today.set(Calendar.MILLISECOND, 0);
Calendar selected = Calendar.getInstance();
selected.set(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]) - 1,
Integer.parseInt(parts[2]), 0, 0, 0);
selected.set(Calendar.MILLISECOND, 0);
return selected.before(today);
} catch (Exception e) {
Log.e(TAG, "Error parsing date: " + e.getMessage());
return false;
}
}
/**
* Checks if a given date and time are in the past.
* format: date = "YYYY-MM-DD", time = "HH:MM"

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

@@ -11,13 +11,31 @@ import java.io.InputStream;
public class FileUtils {
public static File getFileFromUri(Context context, Uri uri) {
try {
if ("content".equals(uri.getScheme())) {
String authority = uri.getAuthority();
if (authority != null && authority.equals(context.getPackageName() + ".fileprovider")) {
String lastSegment = uri.getLastPathSegment();
if (lastSegment != null) {
String fileName = lastSegment.contains("/")
? lastSegment.substring(lastSegment.lastIndexOf('/') + 1)
: lastSegment;
File cachedFile = new File(context.getCacheDir(), fileName);
if (cachedFile.exists() && cachedFile.length() > 0) {
return cachedFile;
}
}
}
}
String fileName = getFileName(context, uri);
if (fileName == null) fileName = "upload_" + System.currentTimeMillis();
InputStream inputStream = context.getContentResolver().openInputStream(uri);
if (inputStream == null) return null;
File tempFile = new File(context.getCacheDir(), fileName);
FileOutputStream outputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[1024];
byte[] buffer = new byte[4096];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
@@ -47,4 +65,4 @@ public class FileUtils {
}
return result;
}
}
}

View File

@@ -129,9 +129,16 @@ public class ImagePickerHelper {
* Prepares a temporary file and launches the camera app.
*/
private void launchCamera() {
File photoFile = new File(fragment.requireContext().getCacheDir(), tempFileName);
photoUri = FileProvider.getUriForFile(fragment.requireContext(), fragment.requireContext().getPackageName() + ".fileprovider", photoFile);
cameraLauncher.launch(photoUri);
try {
File photoFile = new File(fragment.requireContext().getCacheDir(), tempFileName);
if (!photoFile.exists()) photoFile.createNewFile();
photoUri = FileProvider.getUriForFile(fragment.requireContext(),
fragment.requireContext().getPackageName() + ".fileprovider", photoFile);
cameraLauncher.launch(photoUri);
} catch (Exception e) {
android.widget.Toast.makeText(fragment.requireContext(),
"Could not prepare camera", android.widget.Toast.LENGTH_SHORT).show();
}
}
/**
@@ -157,4 +164,4 @@ public class ImagePickerHelper {
.setNegativeButton("Cancel", null)
.show();
}
}
}

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

@@ -10,10 +10,13 @@ import com.example.petstoremobile.repositories.AdoptionRepository;
import com.example.petstoremobile.repositories.CustomerRepository;
import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
@@ -27,12 +30,12 @@ public class AdoptionDetailViewModel extends ViewModel {
private final StoreRepository storeRepository;
private long adoptionId = -1;
private boolean isEditing = false;
private final MutableLiveData<List<DropdownDTO>> petList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> customerList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> employeeList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<ViewState> viewState = new MutableLiveData<>(new ViewState());
@Inject
public AdoptionDetailViewModel(AdoptionRepository adoptionRepository, PetRepository petRepository,
@@ -45,7 +48,7 @@ public class AdoptionDetailViewModel extends ViewModel {
public void setAdoptionId(long id) {
this.adoptionId = id;
this.isEditing = id != -1;
initMode(id != -1);
}
public long getAdoptionId() {
@@ -53,50 +56,294 @@ public class AdoptionDetailViewModel extends ViewModel {
}
public boolean isEditing() {
return isEditing;
ViewState current = viewState.getValue();
return current != null && current.isEditing;
}
public LiveData<ViewState> getViewState() {
return viewState;
}
public void initMode(boolean isEditing) {
updateViewState(state -> {
state.isEditing = isEditing;
state.modeTitle = isEditing ? "Edit Adoption" : "Add Adoption";
state.saveButtonText = isEditing ? "Save" : "Add";
state.isAdoptionIdVisible = isEditing;
state.isDeleteVisible = isEditing;
state.isFeeEnabled = false; // fee is always read-only
if (!isEditing) {
state.isCustomerEnabled = true;
state.isStoreEnabled = true;
state.isPetEnabled = false; // until customer selected
state.isEmployeeEnabled = false; // until store selected
state.isDateEnabled = true;
state.isStatusEnabled = true;
state.availableStatuses = new String[]{"Pending"};
state.selectedStatus = "Pending";
} else {
// edit: date-based logic applied after load
state.isCustomerEnabled = false;
state.isStoreEnabled = false;
state.isPetEnabled = false;
state.isEmployeeEnabled = false;
state.isDateEnabled = false;
state.isStatusEnabled = false;
}
});
}
public void loadInitialFormData(boolean isEditing) {
// Pets are loaded dynamically based on store selection; no pre-load needed.
customerRepository.getCustomerDropdowns().observeForever(r -> {
if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
customerList.setValue(r.data);
}
});
storeRepository.getStoreDropdowns().observeForever(r -> {
if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
storeList.setValue(r.data);
}
});
}
public void onCustomerSelected(int position) {
List<DropdownDTO> list = customerList.getValue();
Long customerId = (position > 0 && list != null && position <= list.size())
? list.get(position - 1).getId() : null;
updateViewState(state -> state.selectedCustomerId = customerId);
}
public void onStoreSelected(int position) {
List<DropdownDTO> list = storeList.getValue();
if (position > 0 && list != null && position <= list.size()) {
Long storeId = list.get(position - 1).getId();
updateViewState(state -> {
state.selectedStoreId = storeId;
if (!state.isCancelled && !state.isEditing) {
state.isEmployeeEnabled = true;
state.isPetEnabled = true;
}
});
loadEmployeesForStore(storeId);
if (!isEditing()) loadAvailablePetsByStore(storeId);
} else {
employeeList.setValue(new ArrayList<>());
petList.setValue(new ArrayList<>());
updateViewState(state -> {
state.selectedStoreId = null;
state.selectedEmployeeId = null;
state.selectedPetId = null;
state.isEmployeeEnabled = false;
state.isPetEnabled = false;
});
}
}
public void onPetSelected(int position) {
List<DropdownDTO> list = petList.getValue();
if (position > 0 && list != null && position <= list.size()) {
Long petId = list.get(position - 1).getId();
updateViewState(s -> s.selectedPetId = petId);
loadPetPrice(petId);
} else {
updateViewState(s -> {
s.selectedPetId = null;
s.adoptionFee = "";
});
}
}
private void loadAvailablePetsByStore(Long storeId) {
petRepository.getAvailablePetsByStore(storeId).observeForever(r -> {
if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
List<DropdownDTO> dropdowns = new ArrayList<>();
for (com.example.petstoremobile.dtos.PetDTO pet : r.data.getContent()) {
dropdowns.add(new DropdownDTO(pet.getPetId(), pet.getPetName()));
}
petList.setValue(dropdowns);
}
});
}
private void loadPetPrice(Long petId) {
petRepository.getPetById(petId).observeForever(r -> {
if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
com.example.petstoremobile.dtos.PetDTO pet = r.data;
// In edit mode, add the pet to the list so the spinner can display its name
if (isEditing()) {
List<DropdownDTO> single = new ArrayList<>();
single.add(new DropdownDTO(pet.getPetId(), pet.getPetName()));
petList.setValue(single);
}
if (pet.getPetPrice() != null) {
String price = String.format(Locale.getDefault(), "%.2f", pet.getPetPrice());
updateViewState(s -> s.adoptionFee = price);
}
}
});
}
private void loadEmployeesForStore(Long storeId) {
storeRepository.getStoreEmployees(storeId).observeForever(r -> {
if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
employeeList.setValue(r.data);
}
});
}
/**
* Called when the date or status changes in the UI. Applies date-based field enabling.
*/
public void onDateChanged(String date, String currentStatus) {
updateViewState(s -> {
if (s.isCancelled) return;
s.availableStatuses = calculateAvailableStatuses(s.isEditing, date);
List<String> available = Arrays.asList(s.availableStatuses);
if (!currentStatus.isEmpty() && available.contains(currentStatus)) {
s.selectedStatus = currentStatus;
} else if (!available.contains(s.selectedStatus) && s.availableStatuses.length > 0) {
s.selectedStatus = s.availableStatuses[0];
}
if (!s.isEditing) return; // add mode: field enabling handled separately
boolean isPast = DateTimeUtils.isDateBeforeToday(date);
if (isPast) {
setAllEditableFieldsEnabled(s, false);
s.isStatusEnabled = true;
} else if (!date.isEmpty()) {
setAllEditableFieldsEnabled(s, false);
s.isEmployeeEnabled = true;
s.isDateEnabled = true;
s.isStatusEnabled = true;
}
});
}
private String[] calculateAvailableStatuses(boolean isEditing, String date) {
if (!isEditing) return new String[]{"Pending"};
if (date == null || date.isEmpty()) return new String[]{};
if (DateTimeUtils.isDateBeforeToday(date)) return new String[]{"Completed", "Missed"};
return new String[]{"Pending", "Cancelled"};
}
/** Disables all editable fields (fee is always disabled separately). */
private void setAllEditableFieldsEnabled(ViewState s, boolean enabled) {
s.isCustomerEnabled = enabled;
s.isStoreEnabled = enabled;
s.isPetEnabled = enabled;
s.isEmployeeEnabled = enabled;
s.isDateEnabled = enabled;
// fee never editable
}
public LiveData<Resource<AdoptionDTO>> loadAdoption() {
return adoptionRepository.getAdoptionById(adoptionId);
}
MutableLiveData<Resource<AdoptionDTO>> result = new MutableLiveData<>();
adoptionRepository.getAdoptionById(adoptionId).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
AdoptionDTO a = resource.data;
String formattedStatus = DateTimeUtils.formatStatusFromBackend(
a.getAdoptionStatus() != null ? a.getAdoptionStatus() : "");
String adoptionDate = a.getAdoptionDate() != null ? a.getAdoptionDate() : "";
public LiveData<Resource<List<DropdownDTO>>> loadPets() {
return petRepository.getAdoptionPets();
}
updateViewState(state -> {
state.selectedPetId = a.getPetId() != null ? a.getPetId() : -1;
state.selectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1;
state.selectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1;
state.selectedEmployeeId = a.getEmployeeId() != null ? a.getEmployeeId() : -1;
state.adoptionDate = adoptionDate;
state.adoptionFee = a.getAdoptionFee() != null ? a.getAdoptionFee().toPlainString() : "";
state.selectedStatus = formattedStatus;
state.adoptionStatus = formattedStatus;
public LiveData<Resource<List<DropdownDTO>>> loadCustomers() {
return customerRepository.getCustomerDropdowns();
}
if ("Cancelled".equalsIgnoreCase(formattedStatus)) {
state.isCancelled = true;
state.isCustomerEnabled = false;
state.isStoreEnabled = false;
state.isPetEnabled = false;
state.isEmployeeEnabled = false;
state.isStatusEnabled = false;
state.isDateEnabled = false;
state.isFeeEnabled = false;
state.isSaveVisible = false;
state.availableStatuses = new String[]{"Cancelled"};
} else {
state.availableStatuses = calculateAvailableStatuses(true, adoptionDate);
boolean isPast = DateTimeUtils.isDateBeforeToday(adoptionDate);
if (isPast) {
setAllEditableFieldsEnabled(state, false);
state.isStatusEnabled = true;
} else if (!adoptionDate.isEmpty()) {
setAllEditableFieldsEnabled(state, false);
state.isEmployeeEnabled = true;
state.isDateEnabled = true;
state.isStatusEnabled = true;
}
}
});
public LiveData<Resource<List<DropdownDTO>>> loadStores() {
return storeRepository.getStoreDropdowns();
}
public LiveData<Resource<List<DropdownDTO>>> loadEmployees(Long storeId) {
return storeRepository.getStoreEmployees(storeId);
if (a.getSourceStoreId() != null) loadEmployeesForStore(a.getSourceStoreId());
if (a.getPetId() != null) loadPetPrice(a.getPetId());
}
result.setValue(resource);
});
return result;
}
public LiveData<Resource<AdoptionDTO>> saveAdoption(AdoptionDTO dto) {
if (isEditing) {
if (isEditing()) {
return adoptionRepository.updateAdoption(adoptionId, dto);
} else {
return adoptionRepository.createAdoption(dto);
}
return adoptionRepository.createAdoption(dto);
}
public LiveData<Resource<Void>> deleteAdoption() {
return adoptionRepository.deleteAdoption(adoptionId);
}
public void setPetList(List<DropdownDTO> list) { petList.setValue(list); }
public LiveData<List<DropdownDTO>> getPetList() { return petList; }
public void setCustomerList(List<DropdownDTO> list) { customerList.setValue(list); }
public LiveData<List<DropdownDTO>> getCustomerList() { return customerList; }
public void setStoreList(List<DropdownDTO> list) { storeList.setValue(list); }
public LiveData<List<DropdownDTO>> getStoreList() { return storeList; }
public LiveData<List<DropdownDTO>> getEmployeeList() { return employeeList; }
public void setEmployeeList(List<DropdownDTO> list) { employeeList.setValue(list); }
public LiveData<List<DropdownDTO>> getEmployeeList() { return employeeList; }
private void updateViewState(Action<ViewState> action) {
ViewState current = viewState.getValue();
if (current != null) {
action.run(current);
viewState.setValue(current);
}
}
private interface Action<T> {
void run(T target);
}
public static class ViewState {
public boolean isEditing = false;
public boolean isAdoptionIdVisible = false;
public boolean isDeleteVisible = false;
public boolean isCancelled = false;
public boolean isPetEnabled = false;
public boolean isEmployeeEnabled = false;
public boolean isCustomerEnabled = true;
public boolean isStoreEnabled = true;
public boolean isStatusEnabled = true;
public boolean isDateEnabled = true;
public boolean isFeeEnabled = false; // always read-only
public boolean isSaveVisible = true;
public String[] availableStatuses = new String[]{};
public String selectedStatus = "";
public String modeTitle = "Add Adoption";
public String saveButtonText = "Add";
public Long selectedPetId = null;
public Long selectedCustomerId = null;
public Long selectedStoreId = null;
public Long selectedEmployeeId = null;
public String adoptionDate = "";
public String adoptionFee = "";
public String adoptionStatus = "";
}
}

View File

@@ -16,6 +16,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
@@ -30,6 +31,10 @@ public class AnalyticsViewModel extends ViewModel {
private final MutableLiveData<AnalyticsData> analyticsData = new MutableLiveData<>();
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
private final MutableLiveData<List<String>> availablePaymentMethods = new MutableLiveData<>(new ArrayList<>());
private List<SaleDTO> cachedSales = new ArrayList<>();
private FilterState currentFilter = new FilterState();
@Inject
public AnalyticsViewModel(SaleRepository saleRepository) {
@@ -39,14 +44,17 @@ public class AnalyticsViewModel extends ViewModel {
public LiveData<AnalyticsData> getAnalyticsData() { return analyticsData; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public LiveData<String> getErrorMessage() { return errorMessage; }
public LiveData<List<String>> getAvailablePaymentMethods() { return availablePaymentMethods; }
public void loadAnalytics() {
isLoading.setValue(true);
errorMessage.setValue(null);
saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc").observeForever(resource -> {
saleRepository.getAllSales(0, 2000, null, null, null, null, "saleDate,desc").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
computeAnalytics(resource.data.getContent());
cachedSales = resource.data.getContent();
derivePaymentMethods();
applyCurrentFilter();
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
errorMessage.setValue(resource.message);
@@ -56,11 +64,53 @@ public class AnalyticsViewModel extends ViewModel {
});
}
private void computeAnalytics(List<SaleDTO> sales) {
public void applyFilter(FilterState filter) {
currentFilter = filter;
applyCurrentFilter();
}
public void resetFilter() {
currentFilter = new FilterState();
applyCurrentFilter();
}
private void applyCurrentFilter() {
List<SaleDTO> filtered = filterSales(cachedSales, currentFilter);
computeAnalytics(filtered, currentFilter);
}
private void derivePaymentMethods() {
java.util.Set<String> methods = new java.util.TreeSet<>();
for (SaleDTO s : cachedSales) {
if (s.getPaymentMethod() != null && !s.getPaymentMethod().isEmpty()) {
methods.add(s.getPaymentMethod());
}
}
List<String> result = new ArrayList<>();
result.add("All");
result.addAll(methods);
availablePaymentMethods.setValue(result);
}
private List<SaleDTO> filterSales(List<SaleDTO> sales, FilterState filter) {
List<SaleDTO> result = new ArrayList<>();
for (SaleDTO s : sales) {
String date = s.getSaleDate() != null && s.getSaleDate().length() >= 10
? s.getSaleDate().substring(0, 10) : "";
if (!filter.startDate.isEmpty() && !date.isEmpty() && date.compareTo(filter.startDate) < 0) continue;
if (!filter.endDate.isEmpty() && !date.isEmpty() && date.compareTo(filter.endDate) > 0) continue;
if (!filter.paymentMethod.equals("All") && !filter.paymentMethod.isEmpty()) {
if (!filter.paymentMethod.equalsIgnoreCase(s.getPaymentMethod())) continue;
}
result.add(s);
}
return result;
}
private void computeAnalytics(List<SaleDTO> sales, FilterState filter) {
List<SaleDTO> regularSales = new ArrayList<>();
for (SaleDTO s : sales) {
if (!Boolean.TRUE.equals(s.getIsRefund()))
regularSales.add(s);
if (!Boolean.TRUE.equals(s.getIsRefund())) regularSales.add(s);
}
AnalyticsData data = new AnalyticsData();
@@ -83,72 +133,127 @@ public class AnalyticsViewModel extends ViewModel {
: BigDecimal.ZERO;
data.totalItems = totalItems;
// Product Maps
Map<String, BigDecimal> revenueByProduct = new LinkedHashMap<>();
Map<String, Integer> quantityByProduct = new LinkedHashMap<>();
Map<String, Integer> paymentCount = new LinkedHashMap<>();
Map<String, BigDecimal> employeeRevenue = new LinkedHashMap<>();
for (SaleDTO s : regularSales) {
// Payments
String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown";
paymentCount.merge(method, 1, Integer::sum);
// Employee
String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown";
if (s.getTotalAmount() != null) employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add);
// Items
if (s.getItems() != null) {
for (SaleDTO.SaleItemDTO item : s.getItems()) {
String name = item.getProductName() != null ? item.getProductName() : "Unknown";
int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0;
BigDecimal lineTotal = item.getUnitPrice() != null
? item.getUnitPrice().multiply(BigDecimal.valueOf(qty))
: BigDecimal.ZERO;
? item.getUnitPrice().multiply(BigDecimal.valueOf(qty)) : BigDecimal.ZERO;
revenueByProduct.merge(name, lineTotal, BigDecimal::add);
quantityByProduct.merge(name, qty, Integer::sum);
}
}
}
// Sort Top Revenue
int topN = filter.topN > 0 ? filter.topN : 5;
data.topRevenueProducts = new ArrayList<>(revenueByProduct.entrySet());
data.topRevenueProducts.sort((a, b) -> b.getValue().compareTo(a.getValue()));
if (data.topRevenueProducts.size() > 5) data.topRevenueProducts = data.topRevenueProducts.subList(0, 5);
if (data.topRevenueProducts.size() > topN) data.topRevenueProducts = data.topRevenueProducts.subList(0, topN);
// Sort Top Quantity
data.topQuantityProducts = new ArrayList<>(quantityByProduct.entrySet());
data.topQuantityProducts.sort((a, b) -> b.getValue() - a.getValue());
if (data.topQuantityProducts.size() > 5) data.topQuantityProducts = data.topQuantityProducts.subList(0, 5);
if (data.topQuantityProducts.size() > topN) data.topQuantityProducts = data.topQuantityProducts.subList(0, topN);
// Payment Stats
data.paymentMethodStats = new ArrayList<>(paymentCount.entrySet());
data.paymentMethodStats.sort((a, b) -> b.getValue() - a.getValue());
// Employee Performance
data.employeePerformance = new ArrayList<>(employeeRevenue.entrySet());
data.employeePerformance.sort((a, b) -> b.getValue().compareTo(a.getValue()));
// Daily Revenue (last 7 days)
Map<String, BigDecimal> dailyMap = new TreeMap<>();
for (int i = 6; i >= 0; i--) {
Calendar day = Calendar.getInstance();
day.add(Calendar.DAY_OF_YEAR, -i);
String key = String.format("%04d-%02d-%02d",
day.get(Calendar.YEAR), day.get(Calendar.MONTH) + 1, day.get(Calendar.DAY_OF_MONTH));
dailyMap.put(key, BigDecimal.ZERO);
// Daily revenue display to filter date range, max 60 days
String rangeStart = filter.startDate;
String rangeEnd = filter.endDate;
if (rangeStart.isEmpty() && rangeEnd.isEmpty()) {
rangeEnd = todayString(0);
rangeStart = todayString(-6);
} else if (rangeStart.isEmpty()) {
rangeStart = shiftDate(rangeEnd, -6);
} else if (rangeEnd.isEmpty()) {
rangeEnd = todayString(0);
}
List<String> dateRange = buildDateRange(rangeStart, rangeEnd, 60);
Map<String, BigDecimal> dailyMap = new TreeMap<>();
for (String d : dateRange) dailyMap.put(d, BigDecimal.ZERO);
for (SaleDTO s : regularSales) {
if (s.getSaleDate() != null && s.getTotalAmount() != null) {
String date = s.getSaleDate().length() >= 10 ? s.getSaleDate().substring(0, 10) : s.getSaleDate();
if (dailyMap.containsKey(date)) dailyMap.merge(date, s.getTotalAmount(), BigDecimal::add);
String d = s.getSaleDate().length() >= 10 ? s.getSaleDate().substring(0, 10) : s.getSaleDate();
if (dailyMap.containsKey(d)) dailyMap.merge(d, s.getTotalAmount(), BigDecimal::add);
}
}
data.dailyRevenue = new ArrayList<>(dailyMap.entrySet());
data.dailyRevenueTitle = buildDailyTitle(filter, rangeStart, rangeEnd);
analyticsData.setValue(data);
}
private String todayString(int offsetDays) {
Calendar c = Calendar.getInstance();
c.add(Calendar.DAY_OF_YEAR, offsetDays);
return String.format(Locale.US, "%04d-%02d-%02d",
c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH));
}
private String shiftDate(String date, int offsetDays) {
try {
String[] p = date.split("-");
Calendar c = Calendar.getInstance();
c.set(Integer.parseInt(p[0]), Integer.parseInt(p[1]) - 1, Integer.parseInt(p[2]), 0, 0, 0);
c.add(Calendar.DAY_OF_YEAR, offsetDays);
return String.format(Locale.US, "%04d-%02d-%02d",
c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH));
} catch (Exception e) {
return date;
}
}
private List<String> buildDateRange(String start, String end, int maxDays) {
List<String> dates = new ArrayList<>();
try {
String[] sp = start.split("-");
String[] ep = end.split("-");
Calendar cur = Calendar.getInstance();
cur.set(Integer.parseInt(sp[0]), Integer.parseInt(sp[1]) - 1, Integer.parseInt(sp[2]), 0, 0, 0);
Calendar endCal = Calendar.getInstance();
endCal.set(Integer.parseInt(ep[0]), Integer.parseInt(ep[1]) - 1, Integer.parseInt(ep[2]), 0, 0, 0);
int count = 0;
while (!cur.after(endCal) && count < maxDays) {
dates.add(String.format(Locale.US, "%04d-%02d-%02d",
cur.get(Calendar.YEAR), cur.get(Calendar.MONTH) + 1, cur.get(Calendar.DAY_OF_MONTH)));
cur.add(Calendar.DAY_OF_YEAR, 1);
count++;
}
} catch (Exception ignored) {}
return dates;
}
private String buildDailyTitle(FilterState filter, String rangeStart, String rangeEnd) {
if (filter.startDate.isEmpty() && filter.endDate.isEmpty()) return "Daily Revenue (Last 7 Days)";
String s = rangeStart.length() >= 10 ? rangeStart.substring(5) : rangeStart;
String e = rangeEnd.length() >= 10 ? rangeEnd.substring(5) : rangeEnd;
return "Daily Revenue (" + s + " " + e + ")";
}
public static class FilterState {
public String startDate = "";
public String endDate = "";
public String paymentMethod = "All";
public int topN = 5;
}
public static class AnalyticsData {
public BigDecimal totalRevenue;
public int totalTransactions;
@@ -159,5 +264,6 @@ public class AnalyticsViewModel extends ViewModel {
public List<Map.Entry<String, Integer>> paymentMethodStats;
public List<Map.Entry<String, BigDecimal>> employeePerformance;
public List<Map.Entry<String, BigDecimal>> dailyRevenue;
public String dailyRevenueTitle = "Daily Revenue";
}
}

View File

@@ -41,6 +41,7 @@ public class AppointmentDetailViewModel extends ViewModel {
private final MutableLiveData<ViewState> viewState = new MutableLiveData<>(new ViewState());
private long appointmentId = -1;
private boolean isOriginallyCancel = false;
private Long currentCustomerId;
private Long currentStoreId;
private Long currentPetId;
@@ -229,18 +230,21 @@ public class AppointmentDetailViewModel extends ViewModel {
repository.getAppointmentById(appointmentId).observeForever(resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
AppointmentDTO a = resource.data;
isOriginallyCancel = "CANCELLED".equalsIgnoreCase(a.getAppointmentStatus());
currentCustomerId = a.getCustomerId();
currentStoreId = a.getStoreId();
currentPetId = a.getPetId();
currentServiceId = a.getServiceId();
currentStaffId = a.getEmployeeId();
String formattedStatus = DateTimeUtils.formatStatusFromBackend(a.getAppointmentStatus());
updateViewState(s -> {
s.selectedCustomerId = currentCustomerId;
s.selectedStoreId = currentStoreId;
s.selectedPetId = currentPetId;
s.selectedServiceId = currentServiceId;
s.selectedStaffId = currentStaffId;
s.selectedStatus = formattedStatus;
});
if (currentCustomerId != null) loadPetsForCustomer(currentCustomerId);
@@ -278,10 +282,16 @@ public class AppointmentDetailViewModel extends ViewModel {
public void onDateOrTimeChanged(String date, String time, String currentStatus) {
updateViewState(s -> {
s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus);
// Keep selectedStatus if still valid; prefer explicit currentStatus from UI if valid
java.util.List<String> available = java.util.Arrays.asList(s.availableStatuses);
if (!currentStatus.isEmpty() && available.contains(currentStatus)) {
s.selectedStatus = currentStatus;
} else if (!available.contains(s.selectedStatus) && s.availableStatuses.length > 0) {
s.selectedStatus = s.availableStatuses[0];
}
boolean isPast = DateTimeUtils.isDateTimeInPast(date, time);
boolean isCancelled = "Cancelled".equalsIgnoreCase(currentStatus);
if (isCancelled) {
if (isOriginallyCancel) {
s.isPast = true;
setAllFieldsEnabled(s, false);
s.isStatusEnabled = false;
@@ -311,7 +321,7 @@ public class AppointmentDetailViewModel extends ViewModel {
private String[] calculateAvailableStatuses(boolean isEditing, String date, String currentTime, String currentStatus) {
if (!isEditing) return new String[]{"Booked"};
if (date == null || date.isEmpty()) return new String[]{};
if ("Cancelled".equalsIgnoreCase(currentStatus)) return new String[]{"Cancelled"};
if (isOriginallyCancel) return new String[]{"Cancelled"};
if (DateTimeUtils.isDateTimeInPast(date, currentTime)) return new String[]{"Completed", "Missed"};
return new String[]{"Booked", "Cancelled"};
}
@@ -348,6 +358,7 @@ public class AppointmentDetailViewModel extends ViewModel {
s.isPetEnabled = false; // until customer selected
s.isStaffEnabled = false; // until store selected
s.availableStatuses = new String[]{"Booked"};
s.selectedStatus = "Booked";
}
});
}
@@ -391,6 +402,7 @@ public class AppointmentDetailViewModel extends ViewModel {
public boolean isTimeEnabled = true;
public boolean isStatusEnabled = true;
public String[] availableStatuses = new String[]{};
public String selectedStatus = "";
// Selected IDs
public Long selectedCustomerId = null;

View File

@@ -9,6 +9,7 @@ import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SendMessageRequest;
import com.example.petstoremobile.dtos.UpdateConversationStatusRequest;
import com.example.petstoremobile.models.Chat;
import com.example.petstoremobile.models.Message;
import com.example.petstoremobile.repositories.ChatRepository;
@@ -126,6 +127,10 @@ public class ChatListViewModel extends ViewModel {
return chatRepository.downloadAttachment(messageId);
}
public LiveData<Resource<ConversationDTO>> closeConversation(Long conversationId) {
return chatRepository.updateConversationStatus(conversationId, new UpdateConversationStatusRequest("CLOSED"));
}
public void addMessageLocally(MessageDTO dto) {
List<Message> current = new ArrayList<>(messageList.getValue());
current.add(dtoToModel(dto));

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

@@ -30,7 +30,7 @@ public class InventoryDetailViewModel extends ViewModel {
private boolean isEditing = false;
private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<ProductDTO>> productList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> productList = new MutableLiveData<>(new ArrayList<>());
@Inject
public InventoryDetailViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository, ProductRepository productRepository) {
@@ -55,8 +55,8 @@ public class InventoryDetailViewModel extends ViewModel {
return storeRepository.getStoreDropdowns();
}
public LiveData<Resource<PageResponse<ProductDTO>>> loadProducts() {
return productRepository.getAllProducts(null, null, 0, 500, "prodName");
public LiveData<Resource<List<DropdownDTO>>> loadProducts() {
return productRepository.getProductDropdowns();
}
public LiveData<Resource<InventoryDTO>> saveInventory(InventoryDTO dto) {
@@ -74,6 +74,6 @@ public class InventoryDetailViewModel extends ViewModel {
public void setStoreList(List<DropdownDTO> list) { storeList.setValue(list); }
public LiveData<List<DropdownDTO>> getStoreList() { return storeList; }
public void setProductList(List<ProductDTO> list) { productList.setValue(list); }
public LiveData<List<ProductDTO>> getProductList() { return productList; }
public void setProductList(List<DropdownDTO> list) { productList.setValue(list); }
public LiveData<List<DropdownDTO>> getProductList() { return productList; }
}

View File

@@ -20,17 +20,22 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class PetDetailViewModel extends ViewModel {
private static final String STATUS_AVAILABLE = "Available";
private static final String STATUS_ADOPTED = "Adopted";
private static final String STATUS_OWNED = "Owned";
private final PetRepository petRepository;
private final CustomerRepository customerRepository;
private final StoreRepository storeRepository;
private final MutableLiveData<PetDTO> petState = new MutableLiveData<>();
private final MutableLiveData<List<DropdownDTO>> customerList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private final MutableLiveData<ViewState> viewState = new MutableLiveData<>(new ViewState());
private long petId = -1;
private boolean isEditing = false;
private Long selectedCustomerId = null;
private Long selectedStoreId = null;
@Inject
public PetDetailViewModel(PetRepository petRepository, CustomerRepository customerRepository, StoreRepository storeRepository) {
@@ -39,9 +44,23 @@ public class PetDetailViewModel extends ViewModel {
this.storeRepository = storeRepository;
}
public void loadInitialFormData() {
customerRepository.getCustomerDropdowns().observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
customerList.setValue(resource.data);
}
});
storeRepository.getStoreDropdowns().observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList.setValue(resource.data);
}
});
}
public void setPetId(long id) {
this.petId = id;
this.isEditing = id != -1;
initMode(id != -1);
}
public long getPetId() {
@@ -49,46 +68,108 @@ public class PetDetailViewModel extends ViewModel {
}
public boolean isEditing() {
return isEditing;
ViewState current = viewState.getValue();
return current != null && current.isEditing;
}
public LiveData<ViewState> getViewState() {
return viewState;
}
public void onCustomerSelected(int position) {
List<DropdownDTO> list = customerList.getValue();
if (position > 0 && list != null && position <= list.size()) {
selectedCustomerId = list.get(position - 1).getId();
} else {
selectedCustomerId = null;
}
updateViewState(state -> state.selectedCustomerId = selectedCustomerId);
}
public void onStoreSelected(int position) {
List<DropdownDTO> list = storeList.getValue();
if (position > 0 && list != null && position <= list.size()) {
selectedStoreId = list.get(position - 1).getId();
} else {
selectedStoreId = null;
}
updateViewState(state -> state.selectedStoreId = selectedStoreId);
}
public void onStatusSelected(String status) {
updateViewState(state -> {
state.selectedStatus = normalizeStatus(status);
applyStatusRules(state, true);
});
}
public void initMode(boolean isEditing) {
updateViewState(state -> {
state.isEditing = isEditing;
state.modeTitle = isEditing ? "Edit Pet" : "Add Pet";
state.saveButtonText = isEditing ? "Save" : "Add";
state.isPetIdVisible = isEditing;
state.isDeleteVisible = isEditing;
state.isSpeciesEnabled = !isEditing;
state.isBreedEnabled = !isEditing;
if (isEditing) {
state.isCustomerEnabled = true;
state.isStoreEnabled = true;
}
if (!isEditing) {
selectedCustomerId = null;
selectedStoreId = null;
state.selectedCustomerId = null;
state.selectedStoreId = null;
state.selectedStatus = STATUS_AVAILABLE;
state.isCustomerEnabled = false;
state.isStoreEnabled = true;
}
});
}
public LiveData<Resource<PetDTO>> loadPet() {
return petRepository.getPetById(petId);
}
MutableLiveData<Resource<PetDTO>> result = new MutableLiveData<>();
petRepository.getPetById(petId).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
PetDTO pet = resource.data;
selectedCustomerId = pet.getCustomerId();
selectedStoreId = pet.getStoreId();
public LiveData<Resource<List<DropdownDTO>>> loadCustomers() {
return customerRepository.getCustomerDropdowns();
}
updateViewState(state -> {
state.selectedCustomerId = selectedCustomerId;
state.selectedStoreId = selectedStoreId;
state.selectedStatus = normalizeStatus(pet.getPetStatus());
applyStatusRules(state, false);
});
}
public LiveData<Resource<List<DropdownDTO>>> loadStores() {
return storeRepository.getStoreDropdowns();
result.setValue(resource);
});
return result;
}
public LiveData<Resource<PetDTO>> savePet(PetDTO petDTO) {
if (isEditing) {
if (isEditing()) {
petDTO.setPetId(petId);
return petRepository.updatePet(petId, petDTO);
} else {
return petRepository.createPet(petDTO);
}
return petRepository.createPet(petDTO);
}
public LiveData<Resource<Void>> deletePet() {
return petRepository.deletePet(petId);
}
public void setCustomerList(List<DropdownDTO> list) {
customerList.setValue(list);
}
public LiveData<List<DropdownDTO>> getCustomerList() {
return customerList;
}
public void setStoreList(List<DropdownDTO> list) {
storeList.setValue(list);
}
public LiveData<List<DropdownDTO>> getStoreList() {
return storeList;
}
@@ -100,4 +181,66 @@ public class PetDetailViewModel extends ViewModel {
public void setLoading(boolean loading) {
isLoading.setValue(loading);
}
private void applyStatusRules(ViewState state, boolean clearInvalidSelections) {
if (STATUS_AVAILABLE.equalsIgnoreCase(state.selectedStatus)) {
state.isCustomerEnabled = false;
state.isStoreEnabled = true;
if (clearInvalidSelections) {
selectedCustomerId = null;
state.selectedCustomerId = null;
}
return;
}
if (STATUS_OWNED.equalsIgnoreCase(state.selectedStatus)) {
state.isCustomerEnabled = true;
state.isStoreEnabled = false;
if (clearInvalidSelections) {
selectedStoreId = null;
state.selectedStoreId = null;
}
return;
}
state.isCustomerEnabled = true;
state.isStoreEnabled = true;
}
private String normalizeStatus(String status) {
if (status == null) return STATUS_AVAILABLE;
String normalized = status.trim();
if (STATUS_ADOPTED.equalsIgnoreCase(normalized)) return STATUS_ADOPTED;
if (STATUS_OWNED.equalsIgnoreCase(normalized)) return STATUS_OWNED;
return STATUS_AVAILABLE;
}
private void updateViewState(Action<ViewState> action) {
ViewState current = viewState.getValue();
if (current != null) {
action.run(current);
viewState.setValue(current);
}
}
private interface Action<T> {
void run(T target);
}
public static class ViewState {
public boolean isEditing = false;
public boolean isDeleteVisible = false;
public boolean isPetIdVisible = false;
public boolean isSpeciesEnabled = true;
public boolean isBreedEnabled = true;
public boolean isCustomerEnabled = false;
public boolean isStoreEnabled = true;
public String modeTitle = "Add Pet";
public String saveButtonText = "Add";
public String[] availableStatuses = new String[]{STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED};
public String selectedStatus = STATUS_AVAILABLE;
public Long selectedCustomerId = null;
public Long selectedStoreId = null;
}
}

View File

@@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.repositories.CategoryRepository;
@@ -24,7 +25,7 @@ public class ProductDetailViewModel extends ViewModel {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
private final MutableLiveData<List<CategoryDTO>> categoryList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> categoryList = new MutableLiveData<>(new ArrayList<>());
private long prodId = -1;
private boolean isEditing = false;
@@ -47,8 +48,8 @@ public class ProductDetailViewModel extends ViewModel {
return isEditing;
}
public LiveData<Resource<PageResponse<CategoryDTO>>> loadCategories() {
return categoryRepository.getAllCategories(0, 100);
public LiveData<Resource<List<DropdownDTO>>> loadCategories() {
return productRepository.getCategoryDropdowns();
}
public LiveData<Resource<ProductDTO>> loadProduct() {
@@ -75,11 +76,12 @@ public class ProductDetailViewModel extends ViewModel {
return productRepository.deleteProductImage(prodId);
}
public void setCategoryList(List<CategoryDTO> list) {
public void setCategoryList(List<DropdownDTO> list) {
categoryList.setValue(list);
}
public LiveData<List<CategoryDTO>> getCategoryList() {
public LiveData<List<DropdownDTO>> getCategoryList() {
return categoryList;
}
}

View File

@@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.CategoryDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.repositories.CategoryRepository;
import com.example.petstoremobile.repositories.ProductRepository;
@@ -23,7 +23,7 @@ public class ProductListViewModel extends ViewModel {
private final CategoryRepository categoryRepository;
private final MutableLiveData<List<ProductDTO>> products = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<CategoryDTO>> categories = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> categories = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
@@ -33,7 +33,7 @@ public class ProductListViewModel extends ViewModel {
}
public LiveData<List<ProductDTO>> getProducts() { return products; }
public LiveData<List<CategoryDTO>> getCategories() { return categories; }
public LiveData<List<DropdownDTO>> getCategories() { return categories; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadProducts(String query, Long categoryId) {
@@ -51,9 +51,9 @@ public class ProductListViewModel extends ViewModel {
}
public void loadCategories() {
categoryRepository.getAllCategories(0, 100).observeForever(resource -> {
productRepository.getCategoryDropdowns().observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
categories.setValue(resource.data.getContent());
categories.setValue(resource.data);
}
});
}

View File

@@ -34,7 +34,7 @@ public class RefundViewModel extends ViewModel {
}
public LiveData<Resource<PageResponse<SaleDTO>>> loadAllSales() {
return saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc");
return saleRepository.getAllSales(0, 1000, null, null, null, null, "saleDate,desc");
}
public void setAllSales(List<SaleDTO> sales) {

View File

@@ -42,7 +42,7 @@ public class SaleListViewModel extends ViewModel {
public LiveData<Boolean> getIsLoading() { return isLoading; }
public boolean isLastPage() { return isLastPage; }
public void loadSales(boolean reset, String query, String paymentMethod, Long storeId) {
public void loadSales(boolean reset, String query, String paymentMethod, Long storeId, Boolean isRefund) {
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
if (reset) {
@@ -51,7 +51,7 @@ public class SaleListViewModel extends ViewModel {
}
isLoading.setValue(true);
saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observeForever(resource -> {
saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, isRefund, "saleDate,desc").observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<SaleDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue());

View File

@@ -1,6 +1,7 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.ServiceDTO;
@@ -14,8 +15,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class ServiceDetailViewModel extends ViewModel {
private final ServiceRepository repository;
private final MutableLiveData<ViewState> viewState = new MutableLiveData<>(new ViewState());
private long serviceId = -1;
private boolean isEditing = false;
@Inject
public ServiceDetailViewModel(ServiceRepository repository) {
@@ -24,7 +26,7 @@ public class ServiceDetailViewModel extends ViewModel {
public void setServiceId(long id) {
this.serviceId = id;
this.isEditing = id != -1;
initMode(id != -1);
}
public long getServiceId() {
@@ -32,23 +34,88 @@ public class ServiceDetailViewModel extends ViewModel {
}
public boolean isEditing() {
return isEditing;
ViewState current = viewState.getValue();
return current != null && current.isEditing;
}
public LiveData<ViewState> getViewState() {
return viewState;
}
public void initMode(boolean isEditing) {
updateViewState(state -> {
state.isEditing = isEditing;
state.modeTitle = isEditing ? "Edit Service" : "Add Service";
state.saveButtonText = isEditing ? "Save" : "Add";
state.isServiceIdVisible = isEditing;
state.isDeleteVisible = isEditing;
state.isFieldsEnabled = true;
});
}
public LiveData<Resource<ServiceDTO>> loadService() {
return repository.getServiceById(serviceId);
MutableLiveData<Resource<ServiceDTO>> result = new MutableLiveData<>();
repository.getServiceById(serviceId).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
ServiceDTO service = resource.data;
updateViewState(state -> {
state.serviceName = safeText(service.getServiceName());
state.serviceDesc = safeText(service.getServiceDesc());
state.serviceDuration = service.getServiceDuration() != null ? String.valueOf(service.getServiceDuration()) : "";
state.servicePrice = service.getServicePrice() != null ? String.valueOf(service.getServicePrice()) : "";
});
}
result.setValue(resource);
});
return result;
}
public LiveData<Resource<ServiceDTO>> saveService(ServiceDTO dto) {
if (isEditing) {
updateViewState(state -> {
state.serviceName = safeText(dto.getServiceName());
state.serviceDesc = safeText(dto.getServiceDesc());
state.serviceDuration = dto.getServiceDuration() != null ? String.valueOf(dto.getServiceDuration()) : "";
state.servicePrice = dto.getServicePrice() != null ? String.valueOf(dto.getServicePrice()) : "";
});
if (isEditing()) {
dto.setServiceId(serviceId);
return repository.updateService(serviceId, dto);
} else {
return repository.createService(dto);
}
return repository.createService(dto);
}
public LiveData<Resource<Void>> deleteService() {
return repository.deleteService(serviceId);
}
private String safeText(String value) {
return value == null ? "" : value.trim();
}
private void updateViewState(Action<ViewState> action) {
ViewState current = viewState.getValue();
if (current != null) {
action.run(current);
viewState.setValue(current);
}
}
private interface Action<T> {
void run(T target);
}
public static class ViewState {
public boolean isEditing = false;
public boolean isDeleteVisible = false;
public boolean isServiceIdVisible = false;
public boolean isFieldsEnabled = true;
public String modeTitle = "Add Service";
public String saveButtonText = "Add";
public String serviceName = "";
public String serviceDesc = "";
public String serviceDuration = "";
public String servicePrice = "";
}
}

View File

@@ -1,12 +1,18 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.repositories.EmployeeRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@@ -14,12 +20,31 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class StaffDetailViewModel extends ViewModel {
private final EmployeeRepository repository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>());
private long employeeId = -1;
private boolean isEditing = false;
@Inject
public StaffDetailViewModel(EmployeeRepository repository) {
public StaffDetailViewModel(EmployeeRepository repository, StoreRepository storeRepository) {
this.repository = repository;
this.storeRepository = storeRepository;
}
public LiveData<Resource<List<DropdownDTO>>> loadStores() {
return storeRepository.getStoreDropdowns();
}
public LiveData<List<DropdownDTO>> getStoreList() {
return storeList;
}
public void setStoreList(List<DropdownDTO> list) {
storeList.setValue(list);
}
public LiveData<Resource<EmployeeDTO>> loadEmployee(long id) {
return repository.getEmployeeById(id);
}
public void setEmployeeId(long id, boolean isEditing) {

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

@@ -1,6 +1,7 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.SupplierDTO;
@@ -14,8 +15,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class SupplierDetailViewModel extends ViewModel {
private final SupplierRepository repository;
private final MutableLiveData<ViewState> viewState = new MutableLiveData<>(new ViewState());
private long supId = -1;
private boolean isEditing = false;
@Inject
public SupplierDetailViewModel(SupplierRepository repository) {
@@ -24,7 +26,7 @@ public class SupplierDetailViewModel extends ViewModel {
public void setSupId(long id) {
this.supId = id;
this.isEditing = id != -1;
initMode(id != -1);
}
public long getSupId() {
@@ -32,23 +34,82 @@ public class SupplierDetailViewModel extends ViewModel {
}
public boolean isEditing() {
return isEditing;
ViewState current = viewState.getValue();
return current != null && current.isEditing;
}
public LiveData<ViewState> getViewState() {
return viewState;
}
public void initMode(boolean isEditing) {
updateViewState(state -> {
state.isEditing = isEditing;
state.modeTitle = isEditing ? "Edit Supplier" : "Add Supplier";
state.saveButtonText = isEditing ? "Save" : "Add";
state.isSupIdVisible = isEditing;
state.isDeleteVisible = isEditing;
state.isFieldsEnabled = true;
});
}
public LiveData<Resource<SupplierDTO>> loadSupplier() {
return repository.getSupplierById(supId);
MutableLiveData<Resource<SupplierDTO>> result = new MutableLiveData<>();
repository.getSupplierById(supId).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
SupplierDTO s = resource.data;
updateViewState(state -> {
state.supCompany = safeText(s.getSupCompany());
state.supFirstName = safeText(s.getSupContactFirstName());
state.supLastName = safeText(s.getSupContactLastName());
state.supEmail = safeText(s.getSupEmail());
state.supPhone = safeText(s.getSupPhone());
});
}
result.setValue(resource);
});
return result;
}
public LiveData<Resource<SupplierDTO>> saveSupplier(SupplierDTO dto) {
if (isEditing) {
if (isEditing()) {
dto.setSupId(supId);
return repository.updateSupplier(supId, dto);
} else {
return repository.createSupplier(dto);
}
return repository.createSupplier(dto);
}
public LiveData<Resource<Void>> deleteSupplier() {
return repository.deleteSupplier(supId);
}
private String safeText(String value) {
return value == null ? "" : value.trim();
}
private void updateViewState(Action<ViewState> action) {
ViewState current = viewState.getValue();
if (current != null) {
action.run(current);
viewState.setValue(current);
}
}
private interface Action<T> {
void run(T target);
}
public static class ViewState {
public boolean isEditing = false;
public boolean isDeleteVisible = false;
public boolean isSupIdVisible = false;
public boolean isFieldsEnabled = true;
public String modeTitle = "Add Supplier";
public String saveButtonText = "Add";
public String supCompany = "";
public String supFirstName = "";
public String supLastName = "";
public String supEmail = "";
public String supPhone = "";
}
}

View File

@@ -84,17 +84,17 @@
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Pet -->
<!-- Store -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pet"
android:text="Source Store"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerAdoptionPet"
android:id="@+id/spinnerAdoptionStore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
@@ -114,16 +114,17 @@
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Pet -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Source Store"
android:text="Pet"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerAdoptionStore"
android:id="@+id/spinnerAdoptionPet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>

View File

@@ -45,9 +45,267 @@
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp">
<LinearLayout
android:id="@+id/rowFilterHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Filters"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/tvFilterSummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="All time"
android:textColor="@color/text_light"
android:textSize="12sp"
android:layout_marginEnd="8dp"/>
<TextView
android:id="@+id/tvFilterToggleIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="▼"
android:textColor="@color/text_light"
android:textSize="12sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/llFilterContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="12dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Quick Range"
android:textColor="@color/text_light"
android:textSize="11sp"
android:layout_marginBottom="6dp"/>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
android:layout_marginBottom="12dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnPresetToday"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="Today"
android:textSize="11sp"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"
android:layout_marginEnd="6dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"/>
<Button
android:id="@+id/btnPreset7D"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="7D"
android:textSize="11sp"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"
android:layout_marginEnd="6dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"/>
<Button
android:id="@+id/btnPreset30D"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="30D"
android:textSize="11sp"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"
android:layout_marginEnd="6dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"/>
<Button
android:id="@+id/btnPreset3M"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="3M"
android:textSize="11sp"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"
android:layout_marginEnd="6dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"/>
<Button
android:id="@+id/btnPreset1Y"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="1Y"
android:textSize="11sp"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"
android:layout_marginEnd="6dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"/>
<Button
android:id="@+id/btnPresetAll"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="All"
android:textSize="11sp"
android:backgroundTint="@color/text_light"
android:textColor="@color/white"
android:paddingStart="10dp"
android:paddingEnd="10dp"/>
</LinearLayout>
</HorizontalScrollView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Date Range"
android:textColor="@color/text_light"
android:textSize="11sp"
android:layout_marginBottom="6dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<EditText
android:id="@+id/etFilterStartDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Start date"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:drawableEnd="@android:drawable/ic_menu_my_calendar"
android:textSize="13sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" "
android:textColor="@color/text_light"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"/>
<EditText
android:id="@+id/etFilterEndDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="End date"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:drawableEnd="@android:drawable/ic_menu_my_calendar"
android:textSize="13sp"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Payment Method"
android:textColor="@color/text_light"
android:textSize="11sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerFilterPayment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Top N Products"
android:textColor="@color/text_light"
android:textSize="11sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerTopN"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnFilterApply"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="6dp"
android:text="Apply"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnFilterReset"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Reset"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
@@ -299,6 +557,7 @@
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/tvDailyRevenueTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Daily Revenue (Last 7 Days)"

View File

@@ -34,8 +34,9 @@
<TextView
android:id="@+id/tvChatTitle"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Customer Chat"
android:textColor="@color/white"
android:textSize="20sp"
@@ -43,6 +44,16 @@
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
<Button
android:id="@+id/btnCloseChat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Close Chat"
android:textSize="12sp"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"
android:visibility="gone"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView

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,286 @@
</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>
<!-- BUSINESS SECTION -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="BUSINESS"
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/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/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/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="Sales"
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/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/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/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/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>
<!-- STORE SECTION -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="STORE"
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/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/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/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/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/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/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/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>
<!-- ADMIN SECTION -->
<LinearLayout
android:id="@+id/sectionAdmin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<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>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ADMIN"
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/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/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/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
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="Purchase Orders"
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="Product Suppliers"
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">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Staff Accounts"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -117,8 +117,25 @@
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"/>
</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/spinnerRefundStatus"
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"/>
<Button
android:id="@+id/btnOpenRefund"

View File

@@ -83,6 +83,40 @@
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/tvSaleStore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/text_dark"
android:layout_marginBottom="16dp"
android:visibility="gone"/>
<LinearLayout
android:id="@+id/llCustomerInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Customer"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvSaleCustomer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/text_dark"
android:layout_marginBottom="16dp"/>
</LinearLayout>
<TextView
android:id="@+id/tvCustomerLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Customer (Optional)"
@@ -111,6 +145,15 @@
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/tvSalePaymentMethod"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/text_dark"
android:layout_marginBottom="16dp"
android:visibility="gone"/>
<LinearLayout
android:id="@+id/llExtraInfo"
android:layout_width="match_parent"

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

@@ -173,7 +173,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Role"
android:text="User Role"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
@@ -184,7 +184,34 @@
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Status -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Staff Role"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerStaffType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Primary Store"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerStaffStore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

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

@@ -27,8 +27,9 @@ public class SaleController {
@RequestParam(required = false) String q,
@RequestParam(required = false) String paymentMethod,
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) Boolean isRefund,
Pageable pageable) {
return ResponseEntity.ok(saleService.getAllSales(q, paymentMethod, storeId, pageable));
return ResponseEntity.ok(saleService.getAllSales(q, paymentMethod, storeId, isRefund, pageable));
}
@GetMapping("/{id}")

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;
@@ -12,4 +16,8 @@ public interface CouponRepository extends JpaRepository<Coupon, Long> {
Optional<Coupon> findByCouponCode(String couponCode);
Optional<Coupon> findByCouponCodeIgnoreCase(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

@@ -20,8 +20,9 @@ public interface SaleRepository extends JpaRepository<Sale, Long> {
"LOWER(s.store.storeName) LIKE LOWER(CONCAT('%', :q, '%'))" +
")) AND " +
"(:paymentMethod IS NULL OR LOWER(s.paymentMethod) = LOWER(:paymentMethod)) AND " +
"(:isRefund IS NULL OR s.isRefund = :isRefund) AND " +
"(:storeId IS NULL OR s.store.storeId = :storeId)")
Page<Sale> searchSales(@Param("q") String query, @Param("paymentMethod") String paymentMethod, @Param("storeId") Long storeId, Pageable pageable);
Page<Sale> searchSales(@Param("q") String query, @Param("paymentMethod") String paymentMethod, @Param("storeId") Long storeId, @Param("isRefund") Boolean isRefund, Pageable pageable);
List<Sale> findByOriginalSaleSaleId(Long originalSaleId);
}

View File

@@ -29,6 +29,7 @@ public class AdoptionService {
private static final String ADOPTION_STATUS_PENDING = "Pending";
private static final String ADOPTION_STATUS_COMPLETED = "Completed";
private static final String ADOPTION_STATUS_CANCELLED = "Cancelled";
private static final String ADOPTION_STATUS_MISSED = "Missed";
private static final String PET_STATUS_AVAILABLE = "Available";
private static final String PET_STATUS_ADOPTED = "Adopted";
@@ -218,7 +219,10 @@ public class AdoptionService {
if (ADOPTION_STATUS_CANCELLED.equalsIgnoreCase(trimmedStatus)) {
return ADOPTION_STATUS_CANCELLED;
}
throw new IllegalArgumentException("Adoption status must be Pending, Completed, or Cancelled");
if (ADOPTION_STATUS_MISSED.equalsIgnoreCase(trimmedStatus)) {
return ADOPTION_STATUS_MISSED;
}
throw new IllegalArgumentException("Adoption status must be Pending, Completed, Cancelled, or Missed");
}
private void validatePetAvailability(Pet pet, Long adoptionId, Long currentPetId) {

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

View File

@@ -39,8 +39,8 @@ public class SaleService {
}
@Transactional(readOnly = true)
public Page<SaleResponse> getAllSales(String query, String paymentMethod, Long storeId, Pageable pageable) {
Page<Sale> sales = saleRepository.searchSales(normalizeFilter(query), normalizeFilter(paymentMethod), storeId, pageable);
public Page<SaleResponse> getAllSales(String query, String paymentMethod, Long storeId, Boolean isRefund, Pageable pageable) {
Page<Sale> sales = saleRepository.searchSales(normalizeFilter(query), normalizeFilter(paymentMethod), storeId, isRefund, pageable);
return sales.map(this::mapToResponse);
}