diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/CouponAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/CouponAdapter.java new file mode 100644 index 00000000..a1414f56 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/CouponAdapter.java @@ -0,0 +1,137 @@ +package com.example.petstoremobile.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.dtos.CouponDTO; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class CouponAdapter extends RecyclerView.Adapter { + private final List coupons; + private final OnCouponClickListener listener; + private boolean selectionMode = false; + private final Set selectedIds = new HashSet<>(); + + public interface OnCouponClickListener { + void onCouponClick(CouponDTO coupon); + void onSelectionChanged(int count); + } + + public CouponAdapter(List coupons, OnCouponClickListener listener) { + this.coupons = coupons; + this.listener = listener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_coupon, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + CouponDTO coupon = coupons.get(position); + holder.tvCouponCode.setText(coupon.getCouponCode()); + + String discountText = ""; + if ("PERCENT".equals(coupon.getDiscountType())) { + discountText = coupon.getDiscountValue().stripTrailingZeros().toPlainString() + "% OFF"; + } else { + discountText = "$" + coupon.getDiscountValue().stripTrailingZeros().toPlainString() + " OFF"; + } + holder.tvCouponDiscount.setText(discountText); + + holder.tvCouponMinOrder.setText("Min order: $" + coupon.getMinOrderAmount().stripTrailingZeros().toPlainString()); + + if (coupon.getEndsAt() != null) { + holder.tvCouponExpiry.setText("Expires: " + coupon.getEndsAt().substring(0, 10)); + holder.tvCouponExpiry.setVisibility(View.VISIBLE); + } else { + holder.tvCouponExpiry.setVisibility(View.GONE); + } + + if (Boolean.TRUE.equals(coupon.getActive())) { + holder.tvCouponStatus.setText("ACTIVE"); + holder.tvCouponStatus.setBackgroundTintList(ContextCompat.getColorStateList(holder.itemView.getContext(), R.color.primary_dark)); + } else { + holder.tvCouponStatus.setText("INACTIVE"); + holder.tvCouponStatus.setBackgroundTintList(ContextCompat.getColorStateList(holder.itemView.getContext(), R.color.accent_coral)); + } + + holder.cbSelectCoupon.setVisibility(selectionMode ? View.VISIBLE : View.GONE); + holder.cbSelectCoupon.setChecked(selectedIds.contains(coupon.getCouponId())); + + holder.itemView.setOnClickListener(v -> { + if (selectionMode) { + toggleSelection(coupon.getCouponId()); + } else { + listener.onCouponClick(coupon); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!selectionMode) { + setSelectionMode(true); + toggleSelection(coupon.getCouponId()); + return true; + } + return false; + }); + + holder.cbSelectCoupon.setOnClickListener(v -> toggleSelection(coupon.getCouponId())); + } + + private void toggleSelection(Long id) { + if (selectedIds.contains(id)) { + selectedIds.remove(id); + } else { + selectedIds.add(id); + } + notifyDataSetChanged(); + listener.onSelectionChanged(selectedIds.size()); + } + + public void setSelectionMode(boolean selectionMode) { + this.selectionMode = selectionMode; + if (!selectionMode) selectedIds.clear(); + notifyDataSetChanged(); + listener.onSelectionChanged(selectedIds.size()); + } + + public Set getSelectedIds() { + return selectedIds; + } + + @Override + public int getItemCount() { + return coupons.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + TextView tvCouponCode, tvCouponDiscount, tvCouponMinOrder, tvCouponExpiry, tvCouponStatus; + CheckBox cbSelectCoupon; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + tvCouponCode = itemView.findViewById(R.id.tvCouponCode); + tvCouponDiscount = itemView.findViewById(R.id.tvCouponDiscount); + tvCouponMinOrder = itemView.findViewById(R.id.tvCouponMinOrder); + tvCouponExpiry = itemView.findViewById(R.id.tvCouponExpiry); + tvCouponStatus = itemView.findViewById(R.id.tvCouponStatus); + cbSelectCoupon = itemView.findViewById(R.id.cbSelectCoupon); + } + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java index 3dca0d7c..8213dbd0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java @@ -7,14 +7,19 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; +import com.example.petstoremobile.api.UserApi; import com.example.petstoremobile.databinding.ItemEmployeeBinding; import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.utils.GlideUtils; + import java.util.List; public class EmployeeAdapter extends RecyclerView.Adapter { private List list; private OnEmployeeClickListener listener; + private String baseUrl; + private String token; public interface OnEmployeeClickListener { void onEmployeeClick(int position); @@ -25,6 +30,14 @@ public class EmployeeAdapter extends RecyclerView.Adapter listener.onEmployeeClick(position)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java index 63f5ac51..7a4ab393 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java @@ -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 getConversationById(@Path("conversationId") Long conversationId); + @PUT("api/v1/chat/conversations/{conversationId}") + Call updateConversationStatus(@Path("conversationId") Long conversationId, @Body UpdateConversationStatusRequest request); + } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/CouponApi.java b/android/app/src/main/java/com/example/petstoremobile/api/CouponApi.java new file mode 100644 index 00000000..ca12cf48 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/CouponApi.java @@ -0,0 +1,44 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.dtos.PageResponse; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface CouponApi { + + @GET("api/v1/coupons") + Call> getAllCoupons( + @Query("page") int page, + @Query("size") int size, + @Query("active") Boolean active, + @Query("discountType") String discountType, + @Query("sort") String sort); + + @GET("api/v1/coupons/{id}") + Call getCouponById(@Path("id") Long id); + + @GET("api/v1/coupons/code/{code}") + Call getCouponByCode(@Path("code") String code); + + @POST("api/v1/coupons") + Call createCoupon(@Body CouponDTO coupon); + + @PUT("api/v1/coupons/{id}") + Call updateCoupon(@Path("id") Long id, @Body CouponDTO coupon); + + @DELETE("api/v1/coupons/{id}") + Call deleteCoupon(@Path("id") Long id); + + @DELETE("api/v1/coupons") + Call bulkDeleteCoupons(@Query("ids") List ids); +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java index 24250c4c..d13a5ed4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -44,6 +44,9 @@ public interface PetApi { @GET("api/v1/dropdowns/adoption-pets") Call> getAdoptionPets(); + @GET("api/v1/dropdowns/pets") + Call> getPetDropdowns(); + // Get pet by id @GET("api/v1/pets/{id}") Call getPetById(@Path("id") Long id); diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java index 1d46107b..8aeb596e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java @@ -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 deleteProductImage(@Path("id") Long id); + + @GET("api/v1/dropdowns/products") + Call> getProductDropdowns(); + + @GET("api/v1/dropdowns/categories") + Call> getCategoryDropdowns(); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java deleted file mode 100644 index 0bdf14d1..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java +++ /dev/null @@ -1,143 +0,0 @@ -package com.example.petstoremobile.api; - -import android.content.Context; -import android.os.Build; -import android.util.Log; - -import com.example.petstoremobile.BuildConfig; -import com.example.petstoremobile.api.auth.AuthApi; -import com.example.petstoremobile.api.auth.AuthInterceptor; -import com.example.petstoremobile.api.auth.TokenManager; - -import okhttp3.OkHttpClient; -import okhttp3.logging.HttpLoggingInterceptor; -import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; - -import java.util.concurrent.TimeUnit; - -//Retrofit client Used for API calls TODO: DELETE THIS FILE AFTER MERGE NOW THAT WE ARE USING HILT AND NETWORKMODULE -public class RetrofitClient { - private static final String TAG = "RetrofitClient"; - public static final String BASE_URL = getBaseUrl(); - - // Helper function to determine BASE_URL based on whether we are testing on an emulator or a real device - private static String getBaseUrl() { - String url = isEmulator() ? BuildConfig.EMULATOR_BACKEND_URL : BuildConfig.DEVICE_BACKEND_URL; - Log.i(TAG, "Using backend URL: " + url + " (emulator=" + isEmulator() + ")"); - return url; - } - - private static boolean isEmulator() { - return Build.FINGERPRINT.startsWith("generic") - || Build.FINGERPRINT.startsWith("unknown") - || Build.MODEL.contains("google_sdk") - || Build.MODEL.contains("Emulator") - || Build.MODEL.contains("Android SDK built for x86") - || Build.MANUFACTURER.contains("Genymotion") - || Build.HARDWARE.contains("goldfish") - || Build.HARDWARE.contains("ranchu") - || Build.PRODUCT.contains("sdk") - || Build.PRODUCT.contains("sdk_gphone") - || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")); - } - - private static Retrofit retrofit = null; - - public static Retrofit getClient(Context context) { - //create an http logging using an interceptor - HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); - interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); - - OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(interceptor) - .addInterceptor(new AuthInterceptor(new TokenManager(context))) - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .build(); - - //build the retrofit object with all needed properties - retrofit = new Retrofit.Builder() - .baseUrl(BASE_URL) - .addConverterFactory(GsonConverterFactory.create()) //JSON converter - .client(client) //logging interceptor - OkHttpClient - .build(); - - return retrofit; - } - - //associate the retrofit object with the API interface - public static PetApi getPetApi(Context context) { - return getClient(context).create(PetApi.class); - } - - public static ServiceApi getServiceApi(Context context) { - return getClient(context).create(ServiceApi.class); - } - - public static SupplierApi getSupplierApi(Context context) { - return getClient(context).create(SupplierApi.class); - } - - public static AdoptionApi getAdoptionApi(Context context) { - return getClient(context).create(AdoptionApi.class); - } - - public static AppointmentApi getAppointmentApi(Context context) { - return getClient(context).create(AppointmentApi.class); - } - - public static ProductApi getProductApi(Context context) { - return getClient(context).create(ProductApi.class); - } - - public static SaleApi getSaleApi(Context context) { - return getClient(context).create(SaleApi.class); - } - - public static PurchaseOrderApi getPurchaseOrderApi(Context context) { - return getClient(context).create(PurchaseOrderApi.class); - } - - public static ProductSupplierApi getProductSupplierApi(Context context) { - return getClient(context).create(ProductSupplierApi.class); - } - - public static InventoryApi getInventoryApi(Context context) { - return getClient(context).create(InventoryApi.class); - } - - public static AuthApi getAuthApi(Context context) { - return getClient(context).create(AuthApi.class); - } - - public static ChatApi getChatApi(Context context) { - return getClient(context).create(ChatApi.class); - } - - public static CustomerApi getCustomerApi(Context context) { - return getClient(context).create(CustomerApi.class); - } - - public static MessageApi getMessageApi(Context context) { - return getClient(context).create(MessageApi.class); - } - - public static StoreApi getStoreApi(Context context) { - return getClient(context).create(StoreApi.class); - } - - public static CategoryApi getCategoryApi(Context context) { - return getClient(context).create(CategoryApi.class); - } - - public static RefundApi getRefundApi(Context context) { - return getClient(context).create(RefundApi.class); - } - - public static EmployeeApi getEmployeeApi(Context context) { - return getClient(context).create(EmployeeApi.class); - } - -} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java index fa11f100..fd58bec4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java @@ -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}") diff --git a/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java b/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java index 53611e66..8346e221 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java @@ -8,6 +8,8 @@ import retrofit2.http.GET; import retrofit2.http.Query; public interface UserApi { + String AVATAR_PATH = "api/v1/users/%d/avatar/file"; + @GET("api/v1/users") Call> getUsers(@Query("role") String role, @Query("page") int page, @Query("size") int size); } diff --git a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java index 4191de71..1a4ab665 100644 --- a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java +++ b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java @@ -191,4 +191,10 @@ public class NetworkModule { public static RefundApi provideRefundApi(Retrofit retrofit) { return retrofit.create(RefundApi.class); } + + @Provides + @Singleton + public static CouponApi provideCouponApi(Retrofit retrofit) { + return retrofit.create(CouponApi.class); + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CouponDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CouponDTO.java new file mode 100644 index 00000000..e2ecb2c4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CouponDTO.java @@ -0,0 +1,52 @@ +package com.example.petstoremobile.dtos; + +import java.math.BigDecimal; + +public class CouponDTO { + private Long couponId; + private String couponCode; + private String discountType; + private BigDecimal discountValue; + private BigDecimal minOrderAmount; + private Boolean active; + private String startsAt; + private String endsAt; + private Integer usageLimit; + private String createdAt; + private String updatedAt; + + public CouponDTO() {} + + public Long getCouponId() { return couponId; } + public void setCouponId(Long couponId) { this.couponId = couponId; } + + public String getCouponCode() { return couponCode; } + public void setCouponCode(String couponCode) { this.couponCode = couponCode; } + + public String getDiscountType() { return discountType; } + public void setDiscountType(String discountType) { this.discountType = discountType; } + + public BigDecimal getDiscountValue() { return discountValue; } + public void setDiscountValue(BigDecimal discountValue) { this.discountValue = discountValue; } + + public BigDecimal getMinOrderAmount() { return minOrderAmount; } + public void setMinOrderAmount(BigDecimal minOrderAmount) { this.minOrderAmount = minOrderAmount; } + + public Boolean getActive() { return active; } + public void setActive(Boolean active) { this.active = active; } + + public String getStartsAt() { return startsAt; } + public void setStartsAt(String startsAt) { this.startsAt = startsAt; } + + public String getEndsAt() { return endsAt; } + public void setEndsAt(String endsAt) { this.endsAt = endsAt; } + + public Integer getUsageLimit() { return usageLimit; } + public void setUsageLimit(Integer usageLimit) { this.usageLimit = usageLimit; } + + public String getCreatedAt() { return createdAt; } + public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } + + public String getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java index 21577a25..f29b4beb 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java @@ -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; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java index 2c7189ee..edb2a132 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java @@ -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 items; private String createdAt; - // Request fields - private Long customerId; - // Constructor for create request public SaleDTO(Long storeId, String paymentMethod, List 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; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/UpdateConversationStatusRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/UpdateConversationStatusRequest.java new file mode 100644 index 00000000..4d7987e1 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/UpdateConversationStatusRequest.java @@ -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; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index 3c6a9d6c..5b59de80 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -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; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java index 901a2af5..e7180292 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java @@ -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(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java index 44719127..b8656b60 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java @@ -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 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 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 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 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("—"); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java new file mode 100644 index 00000000..548d6018 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java @@ -0,0 +1,145 @@ +package com.example.petstoremobile.fragments.listfragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.CouponAdapter; +import com.example.petstoremobile.databinding.FragmentCouponBinding; +import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.CouponListViewModel; + +import java.util.ArrayList; +import java.util.List; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class CouponFragment extends Fragment implements CouponAdapter.OnCouponClickListener { + private FragmentCouponBinding binding; + private CouponListViewModel viewModel; + private CouponAdapter adapter; + private final List couponList = new ArrayList<>(); + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + binding = FragmentCouponBinding.inflate(inflater, container, false); + viewModel = new ViewModelProvider(this).get(CouponListViewModel.class); + + setupRecyclerView(); + setupSearch(); + setupStatusFilter(); + setupTypeFilter(); + setupSwipeRefresh(); + observeViewModel(); + + viewModel.loadCoupons(0, 100, null, null, null); + + binding.fabAddCoupon.setOnClickListener(v -> openDetail(-1)); + binding.btnBulkDeleteCoupons.setOnClickListener(v -> confirmBulkDelete()); + + UIUtils.setupHamburgerMenu(binding.btnHamburgerCoupon, this); + UIUtils.setupFilterToggle(binding.btnToggleFilterCoupon, binding.layoutFilterCoupon, binding.etSearchCoupon, + binding.spinnerTypeCoupon, binding.spinnerStatusCoupon); + + return binding.getRoot(); + } + + private void observeViewModel() { + viewModel.getCoupons().observe(getViewLifecycleOwner(), list -> { + couponList.clear(); + couponList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshCoupon.setRefreshing(loading); + }); + } + + private void setupRecyclerView() { + adapter = new CouponAdapter(couponList, this); + binding.recyclerViewCoupon.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewCoupon.setAdapter(adapter); + } + + private void setupStatusFilter() { + String[] statuses = {"All Statuses", "Active", "Inactive"}; + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCoupon, statuses, this::applyFilters); + } + + private void setupTypeFilter() { + String[] types = {"All Types", "FIXED", "PERCENT"}; + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerTypeCoupon, types, this::applyFilters); + } + + private void setupSearch() { + UIUtils.attachSearch(binding.etSearchCoupon, this::applyFilters); + } + + private void setupSwipeRefresh() { + binding.swipeRefreshCoupon.setOnRefreshListener(this::applyFilters); + } + + private void applyFilters() { + String statusStr = binding.spinnerStatusCoupon.getSelectedItem() != null ? + binding.spinnerStatusCoupon.getSelectedItem().toString() : "All Statuses"; + Boolean active = null; + if (statusStr.equals("Active")) active = true; + else if (statusStr.equals("Inactive")) active = false; + + String typeStr = binding.spinnerTypeCoupon.getSelectedItem() != null ? + binding.spinnerTypeCoupon.getSelectedItem().toString() : "All Types"; + String discountType = typeStr.equals("All Types") ? null : typeStr; + + viewModel.loadCoupons(0, 100, active, discountType, null); + } + + private void openDetail(long id) { + Bundle args = new Bundle(); + args.putLong("couponId", id); + Navigation.findNavController(requireView()).navigate(R.id.couponDetailFragment, args); + } + + @Override + public void onCouponClick(CouponDTO coupon) { + openDetail(coupon.getCouponId()); + } + + @Override + public void onSelectionChanged(int count) { + binding.btnBulkDeleteCoupons.setVisibility(count > 0 ? View.VISIBLE : View.GONE); + } + + private void confirmBulkDelete() { + new AlertDialog.Builder(requireContext()) + .setTitle("Confirm Bulk Delete") + .setMessage("Are you sure you want to delete the selected coupons?") + .setPositiveButton("Delete", (dialog, which) -> { + List ids = new ArrayList<>(adapter.getSelectedIds()); + viewModel.bulkDeleteCoupons(ids).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + adapter.setSelectionMode(false); + applyFilters(); + } else if (resource.status == Resource.Status.ERROR) { + UIUtils.showToast(requireContext(), resource.message); + } + }); + }) + .setNegativeButton("Cancel", null) + .show(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 84f076a2..566f4ee7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -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 categories = viewModel.getCategories().getValue(); + List 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); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index fef5d994..b3fd9546 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -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()); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index 3a1b45a1..1aaf625d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -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); } } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java index 8407d3f6..8540b5f8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java @@ -10,13 +10,19 @@ import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.EmployeeAdapter; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentStaffBinding; import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.StaffListViewModel; import dagger.hilt.android.AndroidEntryPoint; import java.util.*; +import javax.inject.Inject; +import javax.inject.Named; + @AndroidEntryPoint public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener { @@ -25,6 +31,9 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye private List staffList = new ArrayList<>(); private EmployeeAdapter adapter; + @Inject @Named("baseUrl") String baseUrl; + @Inject TokenManager tokenManager; + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -33,15 +42,19 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye setupRecyclerView(); setupSearch(); + setupStatusFilter(); + setupStoreFilter(); setupSwipeRefresh(); observeViewModel(); viewModel.loadStaff(); + viewModel.loadStores(); binding.fabAddStaff.setOnClickListener(v -> openDetail(-1)); UIUtils.setupHamburgerMenu(binding.btnHamburgerStaff, this); - UIUtils.setupFilterToggle(binding.btnToggleFilterStaff, binding.layoutFilterStaff, binding.etSearchStaff); + UIUtils.setupFilterToggle(binding.btnToggleFilterStaff, binding.layoutFilterStaff, binding.etSearchStaff, + binding.spinnerStoreStaff, binding.spinnerStatusStaff); return binding.getRoot(); } @@ -53,6 +66,11 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye adapter.notifyDataSetChanged(); }); + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreStaff, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { binding.swipeRefreshStaff.setRefreshing(loading); }); @@ -60,12 +78,37 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye private void setupRecyclerView() { adapter = new EmployeeAdapter(staffList, this); + adapter.setBaseUrl(baseUrl); + adapter.setToken(tokenManager.getToken()); binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewStaff.setAdapter(adapter); } + private void setupStatusFilter() { + String[] statuses = {"All Statuses", "Active", "Inactive"}; + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusStaff, statuses, this::applyFilters); + } + + private void setupStoreFilter() { + SpinnerUtils.setupFilterSpinner(binding.spinnerStoreStaff, this::applyFilters); + } + private void setupSearch() { - UIUtils.attachSearch(binding.etSearchStaff, () -> viewModel.filter(binding.etSearchStaff.getText().toString())); + UIUtils.attachSearch(binding.etSearchStaff, this::applyFilters); + } + + private void applyFilters() { + String query = binding.etSearchStaff.getText().toString().trim(); + String status = binding.spinnerStatusStaff.getSelectedItem() != null ? + binding.spinnerStatusStaff.getSelectedItem().toString() : "All Statuses"; + + Long storeId = null; + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStoreStaff.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStoreStaff.getSelectedItemPosition() - 1).getStoreId(); + } + + viewModel.filter(query, storeId, status); } private void setupSwipeRefresh() { @@ -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() : ""); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 9b0f022b..1e109213 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -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 pets = viewModel.getPetList().getValue(); + if (pets != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, + pets, DropdownDTO::getLabel, "-- Select Pet --", state.selectedPetId, DropdownDTO::getId); + + List customers = viewModel.getCustomerList().getValue(); + if (customers != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, + customers, DropdownDTO::getLabel, "-- Select Customer --", state.selectedCustomerId, DropdownDTO::getId); + + List stores = viewModel.getStoreList().getValue(); + if (stores != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, + stores, DropdownDTO::getLabel, "-- Select Store --", state.selectedStoreId, DropdownDTO::getId); + + List 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; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index bb2b9d58..6c524d75 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -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 customers = viewModel.getCustomers().getValue(); + if (customers != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, + customers, DropdownDTO::getLabel, "-- Select Customer --", state.selectedCustomerId, DropdownDTO::getId); + + List stores = viewModel.getStores().getValue(); + if (stores != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, + stores, DropdownDTO::getLabel, "-- Select Store --", state.selectedStoreId, DropdownDTO::getId); + + List services = viewModel.getServices().getValue(); + if (services != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, + services, ServiceDTO::getServiceName, "-- Select Service --", state.selectedServiceId, ServiceDTO::getServiceId); + + List pets = viewModel.getCustomerPets().getValue(); + if (pets != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, + pets, DropdownDTO::getLabel, "-- Select Pet --", state.selectedPetId, DropdownDTO::getId); + + List 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(); } }); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java new file mode 100644 index 00000000..7646358f --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java @@ -0,0 +1,175 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +import android.app.DatePickerDialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.Navigation; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.FragmentCouponDetailBinding; +import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.CouponDetailViewModel; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class CouponDetailFragment extends Fragment { + private FragmentCouponDetailBinding binding; + private CouponDetailViewModel viewModel; + private long couponId = -1; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + binding = FragmentCouponDetailBinding.inflate(inflater, container, false); + viewModel = new ViewModelProvider(this).get(CouponDetailViewModel.class); + + if (getArguments() != null) { + couponId = getArguments().getLong("couponId", -1); + viewModel.setCouponId(couponId, couponId > 0); + } + + setupDiscountTypeSpinner(); + setupDatePicker(binding.etStartsAtDetail, null, () -> { + String selectedDate = binding.etStartsAtDetail.getText().toString(); + if (selectedDate.isEmpty()) { + binding.etEndsAtDetail.setText(""); + } else { + String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + if (selectedDate.equals(today)) { + binding.cbActiveDetail.setChecked(true); + } + } + }); + setupDatePicker(binding.etEndsAtDetail, binding.etStartsAtDetail, null); + + if (couponId > 0) { + loadCouponDetails(); + binding.btnDeleteCouponDetail.setVisibility(View.VISIBLE); + } else { + binding.tvTitleCouponDetail.setText("Add Coupon"); + } + + binding.btnBackCouponDetail.setOnClickListener(v -> Navigation.findNavController(v).popBackStack()); + binding.btnSaveCouponDetail.setOnClickListener(v -> saveCoupon()); + binding.btnDeleteCouponDetail.setOnClickListener(v -> confirmDelete()); + + return binding.getRoot(); + } + + private void setupDiscountTypeSpinner() { + String[] types = {"FIXED", "PERCENT"}; + ArrayAdapter adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, types); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + binding.spinnerDiscountTypeDetail.setAdapter(adapter); + } + + private void setupDatePicker(android.widget.EditText editText, android.widget.EditText dependOn, Runnable onDateSet) { + editText.setFocusable(false); + editText.setClickable(true); + editText.setCursorVisible(false); + editText.setOnClickListener(v -> { + if (dependOn != null && dependOn.getText().toString().trim().isEmpty()) { + UIUtils.showToast(requireContext(), "Please select a start date first"); + return; + } + UIUtils.showDatePicker(requireContext(), editText, onDateSet); + }); + } + + private void loadCouponDetails() { + viewModel.loadCoupon(couponId).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + CouponDTO coupon = resource.data; + binding.etCouponCodeDetail.setText(coupon.getCouponCode()); + binding.spinnerDiscountTypeDetail.setSelection(coupon.getDiscountType().equals("FIXED") ? 0 : 1); + binding.etDiscountValueDetail.setText(coupon.getDiscountValue().stripTrailingZeros().toPlainString()); + binding.etMinOrderAmountDetail.setText(coupon.getMinOrderAmount().stripTrailingZeros().toPlainString()); + binding.etUsageLimitDetail.setText(coupon.getUsageLimit() != null ? String.valueOf(coupon.getUsageLimit()) : ""); + binding.cbActiveDetail.setChecked(Boolean.TRUE.equals(coupon.getActive())); + + if (coupon.getStartsAt() != null) binding.etStartsAtDetail.setText(coupon.getStartsAt().substring(0, 10)); + if (coupon.getEndsAt() != null) binding.etEndsAtDetail.setText(coupon.getEndsAt().substring(0, 10)); + } + }); + } + + private void saveCoupon() { + if (!InputValidator.isNotEmpty(binding.etCouponCodeDetail, "Coupon Code")) return; + if (!InputValidator.isGreaterThanZero(binding.etDiscountValueDetail, "Discount Value")) return; + if (!InputValidator.isPositiveDecimal(binding.etMinOrderAmountDetail, "Min Order Amount")) return; + + String startsAt = binding.etStartsAtDetail.getText().toString().trim(); + String endsAt = binding.etEndsAtDetail.getText().toString().trim(); + + if (!startsAt.isEmpty() && !endsAt.isEmpty()) { + if (startsAt.compareTo(endsAt) > 0) { + binding.etEndsAtDetail.setError("End date must be after start date"); + UIUtils.showToast(requireContext(), "End date must be after start date"); + return; + } + } + + String usageLimitStr = binding.etUsageLimitDetail.getText().toString().trim(); + if (!usageLimitStr.isEmpty()) { + if (!InputValidator.isPositiveInteger(binding.etUsageLimitDetail, "Usage Limit")) return; + } + + CouponDTO dto = new CouponDTO(); + dto.setCouponCode(binding.etCouponCodeDetail.getText().toString().trim().toUpperCase()); + dto.setDiscountType(binding.spinnerDiscountTypeDetail.getSelectedItem().toString()); + dto.setDiscountValue(new BigDecimal(binding.etDiscountValueDetail.getText().toString().trim())); + dto.setMinOrderAmount(new BigDecimal(binding.etMinOrderAmountDetail.getText().toString().trim())); + + String usageLimit = binding.etUsageLimitDetail.getText().toString().trim(); + dto.setUsageLimit(usageLimit.isEmpty() ? null : Integer.parseInt(usageLimit)); + + dto.setActive(binding.cbActiveDetail.isChecked()); + + dto.setStartsAt(startsAt.isEmpty() ? null : startsAt + "T00:00:00"); + + dto.setEndsAt(endsAt.isEmpty() ? null : endsAt + "T23:59:59"); + + viewModel.saveCoupon(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Navigation.findNavController(requireView()).popBackStack(); + } else if (resource.status == Resource.Status.ERROR) { + UIUtils.showToast(requireContext(), resource.message); + } + }); + } + + private void confirmDelete() { + new AlertDialog.Builder(requireContext()) + .setTitle("Delete Coupon") + .setMessage("Are you sure you want to delete this coupon?") + .setPositiveButton("Delete", (dialog, which) -> { + viewModel.deleteCoupon().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Navigation.findNavController(requireView()).popBackStack(); + } else if (resource.status == Resource.Status.ERROR) { + UIUtils.showToast(requireContext(), resource.message); + } + }); + }) + .setNegativeButton("Cancel", null) + .show(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index ee52c960..75f1a6b3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -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 -> { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index ee1b34fc..d56ba6a6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -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) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 9f072b51..5d89e305 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -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); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java index 93a5e244..1c216ba3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java @@ -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()); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index 2374fc4c..d7ee6e4c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -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); + } + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java index 1c1bfc4e..acf171ac 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java @@ -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 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 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 -> { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index a7c64079..5eb1f43b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -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); + } + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java index bdc250c4..ec8e32b6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java @@ -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> updateConversationStatus(Long conversationId, UpdateConversationStatusRequest request) { + return executeCall(chatApi.updateConversationStatus(conversationId, request)); + } + /** * Fetches a paginated list of customers. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java new file mode 100644 index 00000000..338043be --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java @@ -0,0 +1,48 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; + +import com.example.petstoremobile.api.CouponApi; +import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.utils.Resource; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class CouponRepository extends BaseRepository { + private final CouponApi couponApi; + + @Inject + public CouponRepository(CouponApi couponApi) { + super("CouponRepository"); + this.couponApi = couponApi; + } + + public LiveData>> getAllCoupons(int page, int size, Boolean active, String discountType, String sort) { + return executeCall(couponApi.getAllCoupons(page, size, active, discountType, sort)); + } + + public LiveData> getCouponById(Long id) { + return executeCall(couponApi.getCouponById(id)); + } + + public LiveData> createCoupon(CouponDTO coupon) { + return executeCall(couponApi.createCoupon(coupon)); + } + + public LiveData> updateCoupon(Long id, CouponDTO coupon) { + return executeCall(couponApi.updateCoupon(id, coupon)); + } + + public LiveData> deleteCoupon(Long id) { + return executeCall(couponApi.deleteCoupon(id)); + } + + public LiveData> bulkDeleteCoupons(List ids) { + return executeCall(couponApi.bulkDeleteCoupons(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index 623a4daa..b1eda73f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -47,6 +47,20 @@ public class PetRepository extends BaseRepository { return executeCall(petApi.getAdoptionPets()); } + /** + * Retrieves all pets from the dropdowns API. + */ + public LiveData>> getPetDropdowns() { + return executeCall(petApi.getPetDropdowns()); + } + + /** + * Retrieves available pets for a specific store. + */ + public LiveData>> 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. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java index a6d32336..636e1430 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java @@ -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> deleteProductImage(Long id) { return executeCall(productApi.deleteProductImage(id)); } + + /** + * Retrieves a list of product dropdowns from the API. + */ + public LiveData>> getProductDropdowns() { + return executeCall(productApi.getProductDropdowns()); + } + + /** + * Retrieves a list of category dropdowns from the API. + */ + public LiveData>> getCategoryDropdowns() { + return executeCall(productApi.getCategoryDropdowns()); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java index 4f436163..36ac8b30 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java @@ -20,8 +20,8 @@ public class SaleRepository extends BaseRepository { this.saleApi = saleApi; } - public LiveData>> 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>> 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> getSaleById(Long id) { diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java index 11e8f4fd..45867cd8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java @@ -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" diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java index 82a10cf7..1f866b0b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java @@ -34,6 +34,7 @@ public class ErrorUtils { try { if (response.errorBody() != null) { String errorJson = response.errorBody().string(); + Log.e(TAG, "Full Error JSON: " + errorJson); ErrorResponse errorResponse = gson.fromJson(errorJson, ErrorResponse.class); if (errorResponse != null && errorResponse.getMessage() != null) { return errorResponse.getMessage(); diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java index bf8c8770..bf45f4f8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java @@ -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; } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java b/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java index 4d1e9bf9..2bcbb52a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java @@ -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(); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java index a10389b6..03802f65 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java @@ -35,6 +35,24 @@ public class InputValidator { } } + // Checks if the value is a positive decimal number greater than 0 + public static boolean isGreaterThanZero(EditText field, String fieldName) { + String value = field.getText().toString().trim(); + try { + double num = Double.parseDouble(value); + if (num <= 0) { + field.setError(fieldName + " must be greater than 0"); + field.requestFocus(); + return false; + } + return true; + } catch (NumberFormatException e) { + field.setError(fieldName + " must be a number"); + field.requestFocus(); + return false; + } + } + // Checks if the value is a positive decimal number public static boolean isPositiveDecimal(EditText field, String fieldName) { String value = field.getText().toString().trim(); diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java index 09b98cb6..947fb87e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java @@ -134,6 +134,10 @@ public class UIUtils { }, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)); + d.setButton(DatePickerDialog.BUTTON_NEUTRAL, "Clear", (dialog, which) -> { + editText.setText(""); + if (onDateSet != null) onDateSet.run(); + }); d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000); d.show(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java index f6e24cd6..c15766f9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java @@ -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> petList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> employeeList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData 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 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 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 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 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 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 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 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> loadAdoption() { - return adoptionRepository.getAdoptionById(adoptionId); - } + MutableLiveData> 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>> 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>> 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>> loadStores() { - return storeRepository.getStoreDropdowns(); - } - - public LiveData>> 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> saveAdoption(AdoptionDTO dto) { - if (isEditing) { + if (isEditing()) { return adoptionRepository.updateAdoption(adoptionId, dto); - } else { - return adoptionRepository.createAdoption(dto); } + return adoptionRepository.createAdoption(dto); } public LiveData> deleteAdoption() { return adoptionRepository.deleteAdoption(adoptionId); } - public void setPetList(List list) { petList.setValue(list); } public LiveData> getPetList() { return petList; } - - public void setCustomerList(List list) { customerList.setValue(list); } public LiveData> getCustomerList() { return customerList; } - - public void setStoreList(List list) { storeList.setValue(list); } public LiveData> getStoreList() { return storeList; } + public LiveData> getEmployeeList() { return employeeList; } public void setEmployeeList(List list) { employeeList.setValue(list); } - public LiveData> getEmployeeList() { return employeeList; } + + private void updateViewState(Action action) { + ViewState current = viewState.getValue(); + if (current != null) { + action.run(current); + viewState.setValue(current); + } + } + + private interface Action { + 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 = ""; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java index 76c039ac..8d43d995 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java @@ -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 = new MutableLiveData<>(); private final MutableLiveData isLoading = new MutableLiveData<>(false); private final MutableLiveData errorMessage = new MutableLiveData<>(); + private final MutableLiveData> availablePaymentMethods = new MutableLiveData<>(new ArrayList<>()); + + private List cachedSales = new ArrayList<>(); + private FilterState currentFilter = new FilterState(); @Inject public AnalyticsViewModel(SaleRepository saleRepository) { @@ -39,14 +44,17 @@ public class AnalyticsViewModel extends ViewModel { public LiveData getAnalyticsData() { return analyticsData; } public LiveData getIsLoading() { return isLoading; } public LiveData getErrorMessage() { return errorMessage; } + public LiveData> 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 sales) { + public void applyFilter(FilterState filter) { + currentFilter = filter; + applyCurrentFilter(); + } + + public void resetFilter() { + currentFilter = new FilterState(); + applyCurrentFilter(); + } + + private void applyCurrentFilter() { + List filtered = filterSales(cachedSales, currentFilter); + computeAnalytics(filtered, currentFilter); + } + + private void derivePaymentMethods() { + java.util.Set methods = new java.util.TreeSet<>(); + for (SaleDTO s : cachedSales) { + if (s.getPaymentMethod() != null && !s.getPaymentMethod().isEmpty()) { + methods.add(s.getPaymentMethod()); + } + } + List result = new ArrayList<>(); + result.add("All"); + result.addAll(methods); + availablePaymentMethods.setValue(result); + } + + private List filterSales(List sales, FilterState filter) { + List 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 sales, FilterState filter) { List 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 revenueByProduct = new LinkedHashMap<>(); Map quantityByProduct = new LinkedHashMap<>(); Map paymentCount = new LinkedHashMap<>(); Map 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 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 dateRange = buildDateRange(rangeStart, rangeEnd, 60); + Map 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 buildDateRange(String start, String end, int maxDays) { + List 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> paymentMethodStats; public List> employeePerformance; public List> dailyRevenue; + public String dailyRevenueTitle = "Daily Revenue"; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java index 685b645a..18c20e24 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java @@ -41,6 +41,7 @@ public class AppointmentDetailViewModel extends ViewModel { private final MutableLiveData 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 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; diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java index 0422561d..88123907 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java @@ -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> closeConversation(Long conversationId) { + return chatRepository.updateConversationStatus(conversationId, new UpdateConversationStatusRequest("CLOSED")); + } + public void addMessageLocally(MessageDTO dto) { List current = new ArrayList<>(messageList.getValue()); current.add(dtoToModel(dto)); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponDetailViewModel.java new file mode 100644 index 00000000..49f52ef5 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponDetailViewModel.java @@ -0,0 +1,54 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.repositories.CouponRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class CouponDetailViewModel extends ViewModel { + private final CouponRepository repository; + private long couponId = -1; + private boolean isEditing = false; + + @Inject + public CouponDetailViewModel(CouponRepository repository) { + this.repository = repository; + } + + public LiveData> loadCoupon(long id) { + return repository.getCouponById(id); + } + + public void setCouponId(long id, boolean isEditing) { + this.couponId = id; + this.isEditing = isEditing; + } + + public long getCouponId() { + return couponId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> saveCoupon(CouponDTO dto) { + if (isEditing && couponId > 0) { + return repository.updateCoupon(couponId, dto); + } else { + return repository.createCoupon(dto); + } + } + + public LiveData> deleteCoupon() { + return repository.deleteCoupon(couponId); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java new file mode 100644 index 00000000..ad27ae6c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java @@ -0,0 +1,50 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.repositories.CouponRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class CouponListViewModel extends ViewModel { + private final CouponRepository repository; + private final MutableLiveData> coupons = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public CouponListViewModel(CouponRepository repository) { + this.repository = repository; + } + + public LiveData> getCoupons() { return coupons; } + public LiveData getIsLoading() { return isLoading; } + + public void loadCoupons(int page, int size, Boolean active, String discountType, String sort) { + isLoading.setValue(true); + repository.getAllCoupons(page, size, active, discountType, sort).observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + coupons.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public LiveData> bulkDeleteCoupons(List ids) { + return repository.bulkDeleteCoupons(ids); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java index a76785af..c872ea49 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java @@ -30,7 +30,7 @@ public class InventoryDetailViewModel extends ViewModel { private boolean isEditing = false; private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); - private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> 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>> loadProducts() { - return productRepository.getAllProducts(null, null, 0, 500, "prodName"); + public LiveData>> loadProducts() { + return productRepository.getProductDropdowns(); } public LiveData> saveInventory(InventoryDTO dto) { @@ -74,6 +74,6 @@ public class InventoryDetailViewModel extends ViewModel { public void setStoreList(List list) { storeList.setValue(list); } public LiveData> getStoreList() { return storeList; } - public void setProductList(List list) { productList.setValue(list); } - public LiveData> getProductList() { return productList; } + public void setProductList(List list) { productList.setValue(list); } + public LiveData> getProductList() { return productList; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java index 68506f44..44f2680a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java @@ -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 petState = new MutableLiveData<>(); private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); - + private final MutableLiveData 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 getViewState() { + return viewState; + } + + public void onCustomerSelected(int position) { + List 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 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> loadPet() { - return petRepository.getPetById(petId); - } + MutableLiveData> 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>> loadCustomers() { - return customerRepository.getCustomerDropdowns(); - } + updateViewState(state -> { + state.selectedCustomerId = selectedCustomerId; + state.selectedStoreId = selectedStoreId; + state.selectedStatus = normalizeStatus(pet.getPetStatus()); + applyStatusRules(state, false); + }); + } - public LiveData>> loadStores() { - return storeRepository.getStoreDropdowns(); + result.setValue(resource); + }); + return result; } public LiveData> 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> deletePet() { return petRepository.deletePet(petId); } - public void setCustomerList(List list) { - customerList.setValue(list); - } - public LiveData> getCustomerList() { return customerList; } - public void setStoreList(List list) { - storeList.setValue(list); - } - public LiveData> 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 action) { + ViewState current = viewState.getValue(); + if (current != null) { + action.run(current); + viewState.setValue(current); + } + } + + private interface Action { + 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; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java index 9ec0628a..c1ac5fdd 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java @@ -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> categoryList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> 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>> loadCategories() { - return categoryRepository.getAllCategories(0, 100); + public LiveData>> loadCategories() { + return productRepository.getCategoryDropdowns(); } public LiveData> loadProduct() { @@ -75,11 +76,12 @@ public class ProductDetailViewModel extends ViewModel { return productRepository.deleteProductImage(prodId); } - public void setCategoryList(List list) { + public void setCategoryList(List list) { categoryList.setValue(list); } - public LiveData> getCategoryList() { + public LiveData> getCategoryList() { return categoryList; } } + diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java index ecd2d238..6d89ab6f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java @@ -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> products = new MutableLiveData<>(new ArrayList<>()); - private final MutableLiveData> categories = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> categories = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); @Inject @@ -33,7 +33,7 @@ public class ProductListViewModel extends ViewModel { } public LiveData> getProducts() { return products; } - public LiveData> getCategories() { return categories; } + public LiveData> getCategories() { return categories; } public LiveData 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); } }); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java index d2b13732..222a1fa4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java @@ -34,7 +34,7 @@ public class RefundViewModel extends ViewModel { } public LiveData>> 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 sales) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java index a364a7d8..297d987f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java @@ -42,7 +42,7 @@ public class SaleListViewModel extends ViewModel { public LiveData 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 currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue()); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java index fca74229..aa465a08 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java @@ -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 = 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 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> loadService() { - return repository.getServiceById(serviceId); + MutableLiveData> 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> 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> deleteService() { return repository.deleteService(serviceId); } + + private String safeText(String value) { + return value == null ? "" : value.trim(); + } + + private void updateViewState(Action action) { + ViewState current = viewState.getValue(); + if (current != null) { + action.run(current); + viewState.setValue(current); + } + } + + private interface Action { + 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 = ""; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java index 91162405..cbee3bc3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java @@ -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> 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>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData> getStoreList() { + return storeList; + } + + public void setStoreList(List list) { + storeList.setValue(list); + } + + public LiveData> loadEmployee(long id) { + return repository.getEmployeeById(id); } public void setEmployeeId(long id, boolean isEditing) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java index 1bd317ca..d448d853 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java @@ -5,7 +5,9 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.repositories.EmployeeRepository; +import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.utils.Resource; import java.util.ArrayList; @@ -18,18 +20,24 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class StaffListViewModel extends ViewModel { private final EmployeeRepository repository; + private final StoreRepository storeRepository; private final MutableLiveData> employees = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> filteredEmployees = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); private String lastQuery = ""; + private Long lastStoreId = null; + private String lastStatus = "All Statuses"; @Inject - public StaffListViewModel(EmployeeRepository repository) { + public StaffListViewModel(EmployeeRepository repository, StoreRepository storeRepository) { this.repository = repository; + this.storeRepository = storeRepository; } public LiveData> getFilteredEmployees() { return filteredEmployees; } + public LiveData> getStores() { return stores; } public LiveData getIsLoading() { return isLoading; } public void loadStaff() { @@ -38,7 +46,7 @@ public class StaffListViewModel extends ViewModel { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { employees.setValue(resource.data.getContent()); - filter(lastQuery); + filter(lastQuery, lastStoreId, lastStatus); isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false); @@ -47,25 +55,45 @@ public class StaffListViewModel extends ViewModel { }); } - public void filter(String query) { + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public void filter(String query, Long storeId, String status) { this.lastQuery = query; + this.lastStoreId = storeId; + this.lastStatus = status; + List all = employees.getValue(); if (all == null) return; - if (query.isEmpty()) { - filteredEmployees.setValue(new ArrayList<>(all)); - } else { - List filtered = new ArrayList<>(); - String lower = query.toLowerCase(); - for (EmployeeDTO e : all) { - if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower)) - || (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower)) - || (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower)) - || (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) { - filtered.add(e); - } + List filtered = new ArrayList<>(); + String lowerQuery = query.toLowerCase(); + + for (EmployeeDTO e : all) { + // Search Query Filter + boolean matchesQuery = query.isEmpty() || + (e.getFullName() != null && e.getFullName().toLowerCase().contains(lowerQuery)) || + (e.getUsername() != null && e.getUsername().toLowerCase().contains(lowerQuery)) || + (e.getEmail() != null && e.getEmail().toLowerCase().contains(lowerQuery)) || + (e.getPhone() != null && e.getPhone().toLowerCase().contains(lowerQuery)); + + // Store Filter + boolean matchesStore = storeId == null || (e.getPrimaryStoreId() != null && e.getPrimaryStoreId().equals(storeId)); + + // Status Filter + boolean matchesStatus = status.equals("All Statuses") || + (status.equals("Active") && Boolean.TRUE.equals(e.getActive())) || + (status.equals("Inactive") && Boolean.FALSE.equals(e.getActive())); + + if (matchesQuery && matchesStore && matchesStatus) { + filtered.add(e); } - filteredEmployees.setValue(filtered); } + filteredEmployees.setValue(filtered); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java index 591beb52..88078102 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java @@ -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 = 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 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> loadSupplier() { - return repository.getSupplierById(supId); + MutableLiveData> 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> 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> deleteSupplier() { return repository.deleteSupplier(supId); } + + private String safeText(String value) { + return value == null ? "" : value.trim(); + } + + private void updateViewState(Action action) { + ViewState current = viewState.getValue(); + if (current != null) { + action.run(current); + viewState.setValue(current); + } + } + + private interface Action { + 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 = ""; + } } diff --git a/android/app/src/main/res/layout/fragment_adoption_detail.xml b/android/app/src/main/res/layout/fragment_adoption_detail.xml index 0608f96f..0bbacddc 100644 --- a/android/app/src/main/res/layout/fragment_adoption_detail.xml +++ b/android/app/src/main/res/layout/fragment_adoption_detail.xml @@ -84,17 +84,17 @@ android:layout_height="wrap_content" android:layout_marginBottom="16dp"/> - + @@ -114,16 +114,17 @@ android:layout_height="wrap_content" android:layout_marginBottom="16dp"/> + diff --git a/android/app/src/main/res/layout/fragment_analytics.xml b/android/app/src/main/res/layout/fragment_analytics.xml index d06b36ec..19a7a51b 100644 --- a/android/app/src/main/res/layout/fragment_analytics.xml +++ b/android/app/src/main/res/layout/fragment_analytics.xml @@ -45,9 +45,267 @@ + + + + + + + + + + + + + + + + + + + + +