Merge branch 'AttachmentsToChat'

This commit is contained in:
Alex
2026-04-10 07:43:24 -06:00
81 changed files with 3972 additions and 883 deletions

View File

@@ -0,0 +1,137 @@
package com.example.petstoremobile.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R;
import com.example.petstoremobile.dtos.CouponDTO;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class CouponAdapter extends RecyclerView.Adapter<CouponAdapter.ViewHolder> {
private final List<CouponDTO> coupons;
private final OnCouponClickListener listener;
private boolean selectionMode = false;
private final Set<Long> selectedIds = new HashSet<>();
public interface OnCouponClickListener {
void onCouponClick(CouponDTO coupon);
void onSelectionChanged(int count);
}
public CouponAdapter(List<CouponDTO> coupons, OnCouponClickListener listener) {
this.coupons = coupons;
this.listener = listener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_coupon, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
CouponDTO coupon = coupons.get(position);
holder.tvCouponCode.setText(coupon.getCouponCode());
String discountText = "";
if ("PERCENT".equals(coupon.getDiscountType())) {
discountText = coupon.getDiscountValue().stripTrailingZeros().toPlainString() + "% OFF";
} else {
discountText = "$" + coupon.getDiscountValue().stripTrailingZeros().toPlainString() + " OFF";
}
holder.tvCouponDiscount.setText(discountText);
holder.tvCouponMinOrder.setText("Min order: $" + coupon.getMinOrderAmount().stripTrailingZeros().toPlainString());
if (coupon.getEndsAt() != null) {
holder.tvCouponExpiry.setText("Expires: " + coupon.getEndsAt().substring(0, 10));
holder.tvCouponExpiry.setVisibility(View.VISIBLE);
} else {
holder.tvCouponExpiry.setVisibility(View.GONE);
}
if (Boolean.TRUE.equals(coupon.getActive())) {
holder.tvCouponStatus.setText("ACTIVE");
holder.tvCouponStatus.setBackgroundTintList(ContextCompat.getColorStateList(holder.itemView.getContext(), R.color.primary_dark));
} else {
holder.tvCouponStatus.setText("INACTIVE");
holder.tvCouponStatus.setBackgroundTintList(ContextCompat.getColorStateList(holder.itemView.getContext(), R.color.accent_coral));
}
holder.cbSelectCoupon.setVisibility(selectionMode ? View.VISIBLE : View.GONE);
holder.cbSelectCoupon.setChecked(selectedIds.contains(coupon.getCouponId()));
holder.itemView.setOnClickListener(v -> {
if (selectionMode) {
toggleSelection(coupon.getCouponId());
} else {
listener.onCouponClick(coupon);
}
});
holder.itemView.setOnLongClickListener(v -> {
if (!selectionMode) {
setSelectionMode(true);
toggleSelection(coupon.getCouponId());
return true;
}
return false;
});
holder.cbSelectCoupon.setOnClickListener(v -> toggleSelection(coupon.getCouponId()));
}
private void toggleSelection(Long id) {
if (selectedIds.contains(id)) {
selectedIds.remove(id);
} else {
selectedIds.add(id);
}
notifyDataSetChanged();
listener.onSelectionChanged(selectedIds.size());
}
public void setSelectionMode(boolean selectionMode) {
this.selectionMode = selectionMode;
if (!selectionMode) selectedIds.clear();
notifyDataSetChanged();
listener.onSelectionChanged(selectedIds.size());
}
public Set<Long> getSelectedIds() {
return selectedIds;
}
@Override
public int getItemCount() {
return coupons.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvCouponCode, tvCouponDiscount, tvCouponMinOrder, tvCouponExpiry, tvCouponStatus;
CheckBox cbSelectCoupon;
public ViewHolder(@NonNull View itemView) {
super(itemView);
tvCouponCode = itemView.findViewById(R.id.tvCouponCode);
tvCouponDiscount = itemView.findViewById(R.id.tvCouponDiscount);
tvCouponMinOrder = itemView.findViewById(R.id.tvCouponMinOrder);
tvCouponExpiry = itemView.findViewById(R.id.tvCouponExpiry);
tvCouponStatus = itemView.findViewById(R.id.tvCouponStatus);
cbSelectCoupon = itemView.findViewById(R.id.cbSelectCoupon);
}
}
}

View File

@@ -7,14 +7,19 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.api.UserApi;
import com.example.petstoremobile.databinding.ItemEmployeeBinding; import com.example.petstoremobile.databinding.ItemEmployeeBinding;
import com.example.petstoremobile.dtos.EmployeeDTO; import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.utils.GlideUtils;
import java.util.List; import java.util.List;
public class EmployeeAdapter extends RecyclerView.Adapter<EmployeeAdapter.EmployeeViewHolder> { public class EmployeeAdapter extends RecyclerView.Adapter<EmployeeAdapter.EmployeeViewHolder> {
private List<EmployeeDTO> list; private List<EmployeeDTO> list;
private OnEmployeeClickListener listener; private OnEmployeeClickListener listener;
private String baseUrl;
private String token;
public interface OnEmployeeClickListener { public interface OnEmployeeClickListener {
void onEmployeeClick(int position); void onEmployeeClick(int position);
@@ -25,6 +30,14 @@ public class EmployeeAdapter extends RecyclerView.Adapter<EmployeeAdapter.Employ
this.listener = listener; this.listener = listener;
} }
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setToken(String token) {
this.token = token;
}
public static class EmployeeViewHolder extends RecyclerView.ViewHolder { public static class EmployeeViewHolder extends RecyclerView.ViewHolder {
private final ItemEmployeeBinding binding; private final ItemEmployeeBinding binding;
@@ -66,8 +79,13 @@ public class EmployeeAdapter extends RecyclerView.Adapter<EmployeeAdapter.Employ
binding.tvEmployeeStatus.setText(active ? "Active" : "Inactive"); binding.tvEmployeeStatus.setText(active ? "Active" : "Inactive");
binding.tvEmployeeStatus.setTextColor(active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336")); binding.tvEmployeeStatus.setTextColor(active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336"));
// Placeholder for profile image - matching Pet style // Profile image
binding.ivEmployeeProfile.setImageResource(R.drawable.placeholder); if (baseUrl != null && e.getId() != null) {
String imageUrl = baseUrl + String.format(UserApi.AVATAR_PATH, e.getId());
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), binding.ivEmployeeProfile, imageUrl, token, R.drawable.placeholder);
} else {
binding.ivEmployeeProfile.setImageResource(R.drawable.placeholder);
}
holder.itemView.setOnClickListener(v -> listener.onEmployeeClick(position)); holder.itemView.setOnClickListener(v -> listener.onEmployeeClick(position));
} }

View File

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

View File

@@ -0,0 +1,44 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.dtos.PageResponse;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface CouponApi {
@GET("api/v1/coupons")
Call<PageResponse<CouponDTO>> getAllCoupons(
@Query("page") int page,
@Query("size") int size,
@Query("active") Boolean active,
@Query("discountType") String discountType,
@Query("sort") String sort);
@GET("api/v1/coupons/{id}")
Call<CouponDTO> getCouponById(@Path("id") Long id);
@GET("api/v1/coupons/code/{code}")
Call<CouponDTO> getCouponByCode(@Path("code") String code);
@POST("api/v1/coupons")
Call<CouponDTO> createCoupon(@Body CouponDTO coupon);
@PUT("api/v1/coupons/{id}")
Call<CouponDTO> updateCoupon(@Path("id") Long id, @Body CouponDTO coupon);
@DELETE("api/v1/coupons/{id}")
Call<Void> deleteCoupon(@Path("id") Long id);
@DELETE("api/v1/coupons")
Call<Void> bulkDeleteCoupons(@Query("ids") List<Long> ids);
}

View File

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

View File

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

View File

@@ -1,143 +0,0 @@
package com.example.petstoremobile.api;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import com.example.petstoremobile.BuildConfig;
import com.example.petstoremobile.api.auth.AuthApi;
import com.example.petstoremobile.api.auth.AuthInterceptor;
import com.example.petstoremobile.api.auth.TokenManager;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.util.concurrent.TimeUnit;
//Retrofit client Used for API calls TODO: DELETE THIS FILE AFTER MERGE NOW THAT WE ARE USING HILT AND NETWORKMODULE
public class RetrofitClient {
private static final String TAG = "RetrofitClient";
public static final String BASE_URL = getBaseUrl();
// Helper function to determine BASE_URL based on whether we are testing on an emulator or a real device
private static String getBaseUrl() {
String url = isEmulator() ? BuildConfig.EMULATOR_BACKEND_URL : BuildConfig.DEVICE_BACKEND_URL;
Log.i(TAG, "Using backend URL: " + url + " (emulator=" + isEmulator() + ")");
return url;
}
private static boolean isEmulator() {
return Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| Build.HARDWARE.contains("goldfish")
|| Build.HARDWARE.contains("ranchu")
|| Build.PRODUCT.contains("sdk")
|| Build.PRODUCT.contains("sdk_gphone")
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"));
}
private static Retrofit retrofit = null;
public static Retrofit getClient(Context context) {
//create an http logging using an interceptor
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(interceptor)
.addInterceptor(new AuthInterceptor(new TokenManager(context)))
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
//build the retrofit object with all needed properties
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create()) //JSON converter
.client(client) //logging interceptor - OkHttpClient
.build();
return retrofit;
}
//associate the retrofit object with the API interface
public static PetApi getPetApi(Context context) {
return getClient(context).create(PetApi.class);
}
public static ServiceApi getServiceApi(Context context) {
return getClient(context).create(ServiceApi.class);
}
public static SupplierApi getSupplierApi(Context context) {
return getClient(context).create(SupplierApi.class);
}
public static AdoptionApi getAdoptionApi(Context context) {
return getClient(context).create(AdoptionApi.class);
}
public static AppointmentApi getAppointmentApi(Context context) {
return getClient(context).create(AppointmentApi.class);
}
public static ProductApi getProductApi(Context context) {
return getClient(context).create(ProductApi.class);
}
public static SaleApi getSaleApi(Context context) {
return getClient(context).create(SaleApi.class);
}
public static PurchaseOrderApi getPurchaseOrderApi(Context context) {
return getClient(context).create(PurchaseOrderApi.class);
}
public static ProductSupplierApi getProductSupplierApi(Context context) {
return getClient(context).create(ProductSupplierApi.class);
}
public static InventoryApi getInventoryApi(Context context) {
return getClient(context).create(InventoryApi.class);
}
public static AuthApi getAuthApi(Context context) {
return getClient(context).create(AuthApi.class);
}
public static ChatApi getChatApi(Context context) {
return getClient(context).create(ChatApi.class);
}
public static CustomerApi getCustomerApi(Context context) {
return getClient(context).create(CustomerApi.class);
}
public static MessageApi getMessageApi(Context context) {
return getClient(context).create(MessageApi.class);
}
public static StoreApi getStoreApi(Context context) {
return getClient(context).create(StoreApi.class);
}
public static CategoryApi getCategoryApi(Context context) {
return getClient(context).create(CategoryApi.class);
}
public static RefundApi getRefundApi(Context context) {
return getClient(context).create(RefundApi.class);
}
public static EmployeeApi getEmployeeApi(Context context) {
return getClient(context).create(EmployeeApi.class);
}
}

View File

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

View File

@@ -8,6 +8,8 @@ import retrofit2.http.GET;
import retrofit2.http.Query; import retrofit2.http.Query;
public interface UserApi { public interface UserApi {
String AVATAR_PATH = "api/v1/users/%d/avatar/file";
@GET("api/v1/users") @GET("api/v1/users")
Call<PageResponse<UserDTO>> getUsers(@Query("role") String role, @Query("page") int page, @Query("size") int size); Call<PageResponse<UserDTO>> getUsers(@Query("role") String role, @Query("page") int page, @Query("size") int size);
} }

View File

@@ -191,4 +191,10 @@ public class NetworkModule {
public static RefundApi provideRefundApi(Retrofit retrofit) { public static RefundApi provideRefundApi(Retrofit retrofit) {
return retrofit.create(RefundApi.class); return retrofit.create(RefundApi.class);
} }
@Provides
@Singleton
public static CouponApi provideCouponApi(Retrofit retrofit) {
return retrofit.create(CouponApi.class);
}
} }

View File

@@ -0,0 +1,52 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
public class CouponDTO {
private Long couponId;
private String couponCode;
private String discountType;
private BigDecimal discountValue;
private BigDecimal minOrderAmount;
private Boolean active;
private String startsAt;
private String endsAt;
private Integer usageLimit;
private String createdAt;
private String updatedAt;
public CouponDTO() {}
public Long getCouponId() { return couponId; }
public void setCouponId(Long couponId) { this.couponId = couponId; }
public String getCouponCode() { return couponCode; }
public void setCouponCode(String couponCode) { this.couponCode = couponCode; }
public String getDiscountType() { return discountType; }
public void setDiscountType(String discountType) { this.discountType = discountType; }
public BigDecimal getDiscountValue() { return discountValue; }
public void setDiscountValue(BigDecimal discountValue) { this.discountValue = discountValue; }
public BigDecimal getMinOrderAmount() { return minOrderAmount; }
public void setMinOrderAmount(BigDecimal minOrderAmount) { this.minOrderAmount = minOrderAmount; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
public String getStartsAt() { return startsAt; }
public void setStartsAt(String startsAt) { this.startsAt = startsAt; }
public String getEndsAt() { return endsAt; }
public void setEndsAt(String endsAt) { this.endsAt = endsAt; }
public Integer getUsageLimit() { return usageLimit; }
public void setUsageLimit(Integer usageLimit) { this.usageLimit = usageLimit; }
public String getCreatedAt() { return createdAt; }
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
public String getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; }
}

View File

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

View File

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

View File

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

View File

@@ -127,6 +127,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
binding.btnAttach.setOnClickListener(v -> selectAttachment()); binding.btnAttach.setOnClickListener(v -> selectAttachment());
binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment()); binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment());
binding.btnCloseChat.setOnClickListener(v -> closeChat());
setupDrawerToggles(); setupDrawerToggles();
setupRecyclerViews(); setupRecyclerViews();
@@ -356,6 +357,30 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
viewModel.loadMessageHistory(activeConversationId); 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() { private void sendMessage() {
if (activeConversationId == null) return; if (activeConversationId == null) return;
String text = binding.etMessage.getText().toString().trim(); 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) { private void setConversationActive(boolean active, String status) {
boolean isClosed = "CLOSED".equalsIgnoreCase(status); boolean isClosed = "CLOSED".equalsIgnoreCase(status);
UIUtils.setViewsEnabled(active && !isClosed, binding.btnSend, binding.etMessage, binding.btnAttach); UIUtils.setViewsEnabled(active && !isClosed, binding.btnSend, binding.etMessage, binding.btnAttach);
binding.btnCloseChat.setVisibility(active && !isClosed ? View.VISIBLE : View.GONE);
if (!active) { if (!active) {
activeConversationId = null; activeConversationId = null;

View File

@@ -43,15 +43,13 @@ public class ListFragment extends Fragment {
// Check user role and restrict access for STAFF // Check user role and restrict access for STAFF
String role = tokenManager.getRole(); String role = tokenManager.getRole();
if ("STAFF".equalsIgnoreCase(role)) { if ("STAFF".equalsIgnoreCase(role)) {
binding.drawerSuppliers.setVisibility(View.GONE); binding.sectionAdmin.setVisibility(View.GONE);
binding.drawerInventory.setVisibility(View.GONE); } else if ("ADMIN".equalsIgnoreCase(role)) {
} binding.sectionAdmin.setVisibility(View.VISIBLE);
// Only show for ADMIN
if ("ADMIN".equalsIgnoreCase(role)) {
binding.drawerStaff.setVisibility(View.VISIBLE); binding.drawerStaff.setVisibility(View.VISIBLE);
} else { } 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) //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.drawerSale.setOnClickListener(v -> navigateTo(R.id.nav_sale));
binding.drawerStaff.setOnClickListener(v -> navigateTo(R.id.nav_staff)); binding.drawerStaff.setOnClickListener(v -> navigateTo(R.id.nav_staff));
binding.drawerAnalytics.setOnClickListener(v -> navigateTo(R.id.nav_analytics)); binding.drawerAnalytics.setOnClickListener(v -> navigateTo(R.id.nav_analytics));
binding.drawerCoupons.setOnClickListener(v -> navigateTo(R.id.nav_coupon));
return binding.getRoot(); return binding.getRoot();
} }

View File

@@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import com.example.petstoremobile.databinding.FragmentAnalyticsBinding; import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AnalyticsViewModel; import com.example.petstoremobile.viewmodels.AnalyticsViewModel;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@@ -20,6 +21,10 @@ public class AnalyticsFragment extends Fragment {
private FragmentAnalyticsBinding binding; private FragmentAnalyticsBinding binding;
private AnalyticsViewModel viewModel; 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 @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
@@ -27,19 +32,114 @@ public class AnalyticsFragment extends Fragment {
binding = FragmentAnalyticsBinding.inflate(inflater, container, false); binding = FragmentAnalyticsBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class); viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class);
setupFilterPanel();
observeViewModel(); observeViewModel();
viewModel.loadAnalytics(); viewModel.loadAnalytics();
binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics()); binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics());
UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this); UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this);
return binding.getRoot(); 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() { private void observeViewModel() {
viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay); viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay);
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
if (loading) { if (loading) {
@@ -53,6 +153,15 @@ public class AnalyticsFragment extends Fragment {
viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> { viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> {
if (error != null) showError(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 @Override
@@ -61,10 +170,12 @@ public class AnalyticsFragment extends Fragment {
binding = null; binding = null;
} }
// Display
private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) { private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) {
if (data == null) return; if (data == null) return;
// Summary // Summary cards
binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP)); binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP));
binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions)); binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions));
binding.tvAvgTransaction.setText("$" + data.avgTransaction); binding.tvAvgTransaction.setText("$" + data.avgTransaction);
@@ -73,11 +184,12 @@ public class AnalyticsFragment extends Fragment {
// Top Revenue Products // Top Revenue Products
binding.llTopRevenue.removeAllViews(); binding.llTopRevenue.removeAllViews();
if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) { if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) {
BigDecimal maxRevenue = data.topRevenueProducts.get(0).getValue(); BigDecimal maxRev = data.topRevenueProducts.get(0).getValue();
if (maxRevenue.compareTo(BigDecimal.ZERO) == 0) maxRevenue = BigDecimal.ONE; if (maxRev.compareTo(BigDecimal.ZERO) == 0) maxRev = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.topRevenueProducts) { for (Map.Entry<String, BigDecimal> e : data.topRevenueProducts) {
addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), addBarRow(binding.llTopRevenue, e.getKey(),
e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35"); "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxRev.floatValue(), "#ff6b35");
} }
} else { } else {
addEmptyRow(binding.llTopRevenue, "No data"); addEmptyRow(binding.llTopRevenue, "No data");
@@ -99,15 +211,13 @@ public class AnalyticsFragment extends Fragment {
// Payment Methods // Payment Methods
binding.llPaymentMethods.removeAllViews(); binding.llPaymentMethods.removeAllViews();
if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) { if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) {
int maxPayment = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1); int maxPay = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1);
String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" }; String[] payColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
int ci = 0; int ci = 0;
for (Map.Entry<String, Integer> e : data.paymentMethodStats) { for (Map.Entry<String, Integer> e : data.paymentMethodStats) {
addBarRow(binding.llPaymentMethods, e.getKey(), addBarRow(binding.llPaymentMethods, e.getKey(),
e.getValue() + " transactions", e.getValue() + " transactions",
(float) e.getValue() / maxPayment, (float) e.getValue() / maxPay, payColors[ci++ % payColors.length]);
paymentColors[ci % paymentColors.length]);
ci++;
} }
} else { } else {
addEmptyRow(binding.llPaymentMethods, "No data"); addEmptyRow(binding.llPaymentMethods, "No data");
@@ -116,36 +226,37 @@ public class AnalyticsFragment extends Fragment {
// Employee Performance // Employee Performance
binding.llEmployeePerformance.removeAllViews(); binding.llEmployeePerformance.removeAllViews();
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) { 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; if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
maxEmp = data.employeePerformance.get(0).getValue();
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) { for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
addBarRow(binding.llEmployeePerformance, e.getKey(), addBarRow(binding.llEmployeePerformance, e.getKey(),
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxEmp.floatValue(), e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f");
"#1a759f");
} }
} else { } else {
addEmptyRow(binding.llEmployeePerformance, "No data"); addEmptyRow(binding.llEmployeePerformance, "No data");
} }
// Daily Revenue // Daily Revenue
binding.tvDailyRevenueTitle.setText(data.dailyRevenueTitle);
binding.llDailyRevenue.removeAllViews(); binding.llDailyRevenue.removeAllViews();
if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) { 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; if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE;
for (Map.Entry<String, BigDecimal> e : data.dailyRevenue) { for (Map.Entry<String, BigDecimal> e : data.dailyRevenue) {
String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey(); String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey();
addBarRow(binding.llDailyRevenue, label, addBarRow(binding.llDailyRevenue, label,
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
e.getValue().floatValue() / maxDaily.floatValue(), e.getValue().floatValue() / maxDaily.floatValue(), "#ff6b35");
"#ff6b35");
} }
} else {
addEmptyRow(binding.llDailyRevenue, "No data");
} }
} }
// Chart Helpers
private void addBarRow(LinearLayout parent, String label, String value, float ratio, String color) { private void addBarRow(LinearLayout parent, String label, String value, float ratio, String color) {
if (getContext() == null) return; if (getContext() == null) return;
LinearLayout row = new LinearLayout(getContext()); LinearLayout row = new LinearLayout(getContext());
@@ -156,8 +267,7 @@ public class AnalyticsFragment extends Fragment {
labelRow.setOrientation(LinearLayout.HORIZONTAL); labelRow.setOrientation(LinearLayout.HORIZONTAL);
TextView tvLabel = new TextView(getContext()); TextView tvLabel = new TextView(getContext());
tvLabel.setLayoutParams(new LinearLayout.LayoutParams( tvLabel.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
tvLabel.setText(label); tvLabel.setText(label);
tvLabel.setTextColor(Color.parseColor("#444441")); tvLabel.setTextColor(Color.parseColor("#444441"));
tvLabel.setTextSize(13f); tvLabel.setTextSize(13f);
@@ -172,22 +282,19 @@ public class AnalyticsFragment extends Fragment {
labelRow.addView(tvValue); labelRow.addView(tvValue);
LinearLayout barBg = new LinearLayout(getContext()); LinearLayout barBg = new LinearLayout(getContext());
LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 12);
LinearLayout.LayoutParams.MATCH_PARENT, 12);
bgParams.setMargins(0, 4, 0, 0); bgParams.setMargins(0, 4, 0, 0);
barBg.setLayoutParams(bgParams); barBg.setLayoutParams(bgParams);
barBg.setBackgroundColor(Color.parseColor("#EEEEEE")); barBg.setBackgroundColor(Color.parseColor("#EEEEEE"));
float safeRatio = Math.max(0f, Math.min(1f, ratio));
View barFill = new View(getContext()); View barFill = new View(getContext());
LinearLayout.LayoutParams fillParams = new LinearLayout.LayoutParams( barFill.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, safeRatio));
0, LinearLayout.LayoutParams.MATCH_PARENT, ratio);
barFill.setLayoutParams(fillParams);
barFill.setBackgroundColor(Color.parseColor(color)); barFill.setBackgroundColor(Color.parseColor(color));
barBg.addView(barFill); barBg.addView(barFill);
View spacer = new View(getContext()); View spacer = new View(getContext());
spacer.setLayoutParams(new LinearLayout.LayoutParams( spacer.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - safeRatio));
0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - ratio));
barBg.addView(spacer); barBg.addView(spacer);
row.addView(labelRow); row.addView(labelRow);
@@ -205,8 +312,7 @@ public class AnalyticsFragment extends Fragment {
} }
private void showError(String msg) { private void showError(String msg) {
if (getContext() == null || binding == null) if (getContext() == null || binding == null) return;
return;
binding.tvTotalRevenue.setText("Error"); binding.tvTotalRevenue.setText("Error");
binding.tvTotalTransactions.setText(""); binding.tvTotalTransactions.setText("");
binding.tvAvgTransaction.setText(""); binding.tvAvgTransaction.setText("");

View File

@@ -0,0 +1,145 @@
package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.CouponAdapter;
import com.example.petstoremobile.databinding.FragmentCouponBinding;
import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.CouponListViewModel;
import java.util.ArrayList;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class CouponFragment extends Fragment implements CouponAdapter.OnCouponClickListener {
private FragmentCouponBinding binding;
private CouponListViewModel viewModel;
private CouponAdapter adapter;
private final List<CouponDTO> couponList = new ArrayList<>();
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentCouponBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(CouponListViewModel.class);
setupRecyclerView();
setupSearch();
setupStatusFilter();
setupTypeFilter();
setupSwipeRefresh();
observeViewModel();
viewModel.loadCoupons(0, 100, null, null, null);
binding.fabAddCoupon.setOnClickListener(v -> openDetail(-1));
binding.btnBulkDeleteCoupons.setOnClickListener(v -> confirmBulkDelete());
UIUtils.setupHamburgerMenu(binding.btnHamburgerCoupon, this);
UIUtils.setupFilterToggle(binding.btnToggleFilterCoupon, binding.layoutFilterCoupon, binding.etSearchCoupon,
binding.spinnerTypeCoupon, binding.spinnerStatusCoupon);
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getCoupons().observe(getViewLifecycleOwner(), list -> {
couponList.clear();
couponList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshCoupon.setRefreshing(loading);
});
}
private void setupRecyclerView() {
adapter = new CouponAdapter(couponList, this);
binding.recyclerViewCoupon.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewCoupon.setAdapter(adapter);
}
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Active", "Inactive"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCoupon, statuses, this::applyFilters);
}
private void setupTypeFilter() {
String[] types = {"All Types", "FIXED", "PERCENT"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerTypeCoupon, types, this::applyFilters);
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchCoupon, this::applyFilters);
}
private void setupSwipeRefresh() {
binding.swipeRefreshCoupon.setOnRefreshListener(this::applyFilters);
}
private void applyFilters() {
String statusStr = binding.spinnerStatusCoupon.getSelectedItem() != null ?
binding.spinnerStatusCoupon.getSelectedItem().toString() : "All Statuses";
Boolean active = null;
if (statusStr.equals("Active")) active = true;
else if (statusStr.equals("Inactive")) active = false;
String typeStr = binding.spinnerTypeCoupon.getSelectedItem() != null ?
binding.spinnerTypeCoupon.getSelectedItem().toString() : "All Types";
String discountType = typeStr.equals("All Types") ? null : typeStr;
viewModel.loadCoupons(0, 100, active, discountType, null);
}
private void openDetail(long id) {
Bundle args = new Bundle();
args.putLong("couponId", id);
Navigation.findNavController(requireView()).navigate(R.id.couponDetailFragment, args);
}
@Override
public void onCouponClick(CouponDTO coupon) {
openDetail(coupon.getCouponId());
}
@Override
public void onSelectionChanged(int count) {
binding.btnBulkDeleteCoupons.setVisibility(count > 0 ? View.VISIBLE : View.GONE);
}
private void confirmBulkDelete() {
new AlertDialog.Builder(requireContext())
.setTitle("Confirm Bulk Delete")
.setMessage("Are you sure you want to delete the selected coupons?")
.setPositiveButton("Delete", (dialog, which) -> {
List<Long> ids = new ArrayList<>(adapter.getSelectedIds());
viewModel.bulkDeleteCoupons(ids).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
adapter.setSelectionMode(false);
applyFilters();
} else if (resource.status == Resource.Status.ERROR) {
UIUtils.showToast(requireContext(), resource.message);
}
});
})
.setNegativeButton("Cancel", null)
.show();
}
}

View File

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

View File

@@ -51,6 +51,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
setupSearch(); setupSearch();
setupStoreFilter(); setupStoreFilter();
setupPaymentMethodFilter(); setupPaymentMethodFilter();
setupRefundStatusFilter();
setupSwipeRefresh(); setupSwipeRefresh();
setupFilterToggle(); setupFilterToggle();
observeViewModel(); observeViewModel();
@@ -75,7 +76,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
viewModel.getStores().observe(getViewLifecycleOwner(), list -> { viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, 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 -> { viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
@@ -91,7 +92,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale, UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale,
binding.spinnerPaymentMethod, binding.spinnerStore); binding.spinnerPaymentMethod, binding.spinnerStore, binding.spinnerRefundStatus);
} }
private void setupStoreFilter() { private void setupStoreFilter() {
@@ -99,10 +100,15 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
} }
private void setupPaymentMethodFilter() { private void setupPaymentMethodFilter() {
String[] paymentMethods = {"Payments", "Cash", "Card"}; String[] paymentMethods = {"All Payments", "Cash", "Card"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerPaymentMethod, paymentMethods, () -> loadSales(true)); 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() { private void setupRecyclerView() {
adapter = new SaleAdapter(saleList, this); adapter = new SaleAdapter(saleList, this);
binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); 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(); 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 @Override
@@ -159,6 +170,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
Bundle args = new Bundle(); Bundle args = new Bundle();
if (sale.getSaleId() != null) { if (sale.getSaleId() != null) {
args.putLong("saleId", sale.getSaleId()); args.putLong("saleId", sale.getSaleId());
args.putBoolean("viewOnly", true);
} }
if (sale.getIsRefund() != null) { if (sale.getIsRefund() != null) {
args.putBoolean("isRefund", sale.getIsRefund()); args.putBoolean("isRefund", sale.getIsRefund());

View File

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

View File

@@ -10,13 +10,19 @@ import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.EmployeeAdapter; import com.example.petstoremobile.adapters.EmployeeAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentStaffBinding; import com.example.petstoremobile.databinding.FragmentStaffBinding;
import com.example.petstoremobile.dtos.EmployeeDTO; 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.utils.UIUtils;
import com.example.petstoremobile.viewmodels.StaffListViewModel; import com.example.petstoremobile.viewmodels.StaffListViewModel;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
import java.util.*; import java.util.*;
import javax.inject.Inject;
import javax.inject.Named;
@AndroidEntryPoint @AndroidEntryPoint
public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener { public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener {
@@ -25,6 +31,9 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
private List<EmployeeDTO> staffList = new ArrayList<>(); private List<EmployeeDTO> staffList = new ArrayList<>();
private EmployeeAdapter adapter; private EmployeeAdapter adapter;
@Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -33,15 +42,19 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
setupRecyclerView(); setupRecyclerView();
setupSearch(); setupSearch();
setupStatusFilter();
setupStoreFilter();
setupSwipeRefresh(); setupSwipeRefresh();
observeViewModel(); observeViewModel();
viewModel.loadStaff(); viewModel.loadStaff();
viewModel.loadStores();
binding.fabAddStaff.setOnClickListener(v -> openDetail(-1)); binding.fabAddStaff.setOnClickListener(v -> openDetail(-1));
UIUtils.setupHamburgerMenu(binding.btnHamburgerStaff, this); 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(); return binding.getRoot();
} }
@@ -53,6 +66,11 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
adapter.notifyDataSetChanged(); 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 -> { viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshStaff.setRefreshing(loading); binding.swipeRefreshStaff.setRefreshing(loading);
}); });
@@ -60,12 +78,37 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new EmployeeAdapter(staffList, this); adapter = new EmployeeAdapter(staffList, this);
adapter.setBaseUrl(baseUrl);
adapter.setToken(tokenManager.getToken());
binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewStaff.setAdapter(adapter); 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() { private void setupSearch() {
UIUtils.attachSearch(binding.etSearchStaff, () -> viewModel.filter(binding.etSearchStaff.getText().toString())); UIUtils.attachSearch(binding.etSearchStaff, this::applyFilters);
}
private void applyFilters() {
String query = binding.etSearchStaff.getText().toString().trim();
String status = binding.spinnerStatusStaff.getSelectedItem() != null ?
binding.spinnerStatusStaff.getSelectedItem().toString() : "All Statuses";
Long storeId = null;
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStoreStaff.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStoreStaff.getSelectedItemPosition() - 1).getStoreId();
}
viewModel.filter(query, storeId, status);
} }
private void setupSwipeRefresh() { private void setupSwipeRefresh() {
@@ -76,7 +119,7 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
Bundle args = new Bundle(); Bundle args = new Bundle();
if (position != -1) { if (position != -1) {
EmployeeDTO e = staffList.get(position); 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("username", e.getUsername() != null ? e.getUsername() : "");
args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : ""); args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : "");
args.putString("lastName", e.getLastName() != null ? e.getLastName() : ""); args.putString("lastName", e.getLastName() != null ? e.getLastName() : "");

View File

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

View File

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

View File

@@ -0,0 +1,175 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.app.DatePickerDialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import com.example.petstoremobile.R;
import com.example.petstoremobile.databinding.FragmentCouponDetailBinding;
import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.CouponDetailViewModel;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class CouponDetailFragment extends Fragment {
private FragmentCouponDetailBinding binding;
private CouponDetailViewModel viewModel;
private long couponId = -1;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentCouponDetailBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(CouponDetailViewModel.class);
if (getArguments() != null) {
couponId = getArguments().getLong("couponId", -1);
viewModel.setCouponId(couponId, couponId > 0);
}
setupDiscountTypeSpinner();
setupDatePicker(binding.etStartsAtDetail, null, () -> {
String selectedDate = binding.etStartsAtDetail.getText().toString();
if (selectedDate.isEmpty()) {
binding.etEndsAtDetail.setText("");
} else {
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
if (selectedDate.equals(today)) {
binding.cbActiveDetail.setChecked(true);
}
}
});
setupDatePicker(binding.etEndsAtDetail, binding.etStartsAtDetail, null);
if (couponId > 0) {
loadCouponDetails();
binding.btnDeleteCouponDetail.setVisibility(View.VISIBLE);
} else {
binding.tvTitleCouponDetail.setText("Add Coupon");
}
binding.btnBackCouponDetail.setOnClickListener(v -> Navigation.findNavController(v).popBackStack());
binding.btnSaveCouponDetail.setOnClickListener(v -> saveCoupon());
binding.btnDeleteCouponDetail.setOnClickListener(v -> confirmDelete());
return binding.getRoot();
}
private void setupDiscountTypeSpinner() {
String[] types = {"FIXED", "PERCENT"};
ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, types);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.spinnerDiscountTypeDetail.setAdapter(adapter);
}
private void setupDatePicker(android.widget.EditText editText, android.widget.EditText dependOn, Runnable onDateSet) {
editText.setFocusable(false);
editText.setClickable(true);
editText.setCursorVisible(false);
editText.setOnClickListener(v -> {
if (dependOn != null && dependOn.getText().toString().trim().isEmpty()) {
UIUtils.showToast(requireContext(), "Please select a start date first");
return;
}
UIUtils.showDatePicker(requireContext(), editText, onDateSet);
});
}
private void loadCouponDetails() {
viewModel.loadCoupon(couponId).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
CouponDTO coupon = resource.data;
binding.etCouponCodeDetail.setText(coupon.getCouponCode());
binding.spinnerDiscountTypeDetail.setSelection(coupon.getDiscountType().equals("FIXED") ? 0 : 1);
binding.etDiscountValueDetail.setText(coupon.getDiscountValue().stripTrailingZeros().toPlainString());
binding.etMinOrderAmountDetail.setText(coupon.getMinOrderAmount().stripTrailingZeros().toPlainString());
binding.etUsageLimitDetail.setText(coupon.getUsageLimit() != null ? String.valueOf(coupon.getUsageLimit()) : "");
binding.cbActiveDetail.setChecked(Boolean.TRUE.equals(coupon.getActive()));
if (coupon.getStartsAt() != null) binding.etStartsAtDetail.setText(coupon.getStartsAt().substring(0, 10));
if (coupon.getEndsAt() != null) binding.etEndsAtDetail.setText(coupon.getEndsAt().substring(0, 10));
}
});
}
private void saveCoupon() {
if (!InputValidator.isNotEmpty(binding.etCouponCodeDetail, "Coupon Code")) return;
if (!InputValidator.isGreaterThanZero(binding.etDiscountValueDetail, "Discount Value")) return;
if (!InputValidator.isPositiveDecimal(binding.etMinOrderAmountDetail, "Min Order Amount")) return;
String startsAt = binding.etStartsAtDetail.getText().toString().trim();
String endsAt = binding.etEndsAtDetail.getText().toString().trim();
if (!startsAt.isEmpty() && !endsAt.isEmpty()) {
if (startsAt.compareTo(endsAt) > 0) {
binding.etEndsAtDetail.setError("End date must be after start date");
UIUtils.showToast(requireContext(), "End date must be after start date");
return;
}
}
String usageLimitStr = binding.etUsageLimitDetail.getText().toString().trim();
if (!usageLimitStr.isEmpty()) {
if (!InputValidator.isPositiveInteger(binding.etUsageLimitDetail, "Usage Limit")) return;
}
CouponDTO dto = new CouponDTO();
dto.setCouponCode(binding.etCouponCodeDetail.getText().toString().trim().toUpperCase());
dto.setDiscountType(binding.spinnerDiscountTypeDetail.getSelectedItem().toString());
dto.setDiscountValue(new BigDecimal(binding.etDiscountValueDetail.getText().toString().trim()));
dto.setMinOrderAmount(new BigDecimal(binding.etMinOrderAmountDetail.getText().toString().trim()));
String usageLimit = binding.etUsageLimitDetail.getText().toString().trim();
dto.setUsageLimit(usageLimit.isEmpty() ? null : Integer.parseInt(usageLimit));
dto.setActive(binding.cbActiveDetail.isChecked());
dto.setStartsAt(startsAt.isEmpty() ? null : startsAt + "T00:00:00");
dto.setEndsAt(endsAt.isEmpty() ? null : endsAt + "T23:59:59");
viewModel.saveCoupon(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
Navigation.findNavController(requireView()).popBackStack();
} else if (resource.status == Resource.Status.ERROR) {
UIUtils.showToast(requireContext(), resource.message);
}
});
}
private void confirmDelete() {
new AlertDialog.Builder(requireContext())
.setTitle("Delete Coupon")
.setMessage("Are you sure you want to delete this coupon?")
.setPositiveButton("Delete", (dialog, which) -> {
viewModel.deleteCoupon().observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
Navigation.findNavController(requireView()).popBackStack();
} else if (resource.status == Resource.Status.ERROR) {
UIUtils.showToast(requireContext(), resource.message);
}
});
})
.setNegativeButton("Cancel", null)
.show();
}
}

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,11 @@ public class SaleDetailFragment extends Fragment {
observeViewModel(); observeViewModel();
handleArguments(); 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(); loadData();
setupAddItem(); setupAddItem();
} }
@@ -84,8 +88,9 @@ public class SaleDetailFragment extends Fragment {
binding.tvSaleMode.setText("Sale #" + saleId); binding.tvSaleMode.setText("Sale #" + saleId);
binding.tvSaleDetailId.setText("ID: " + saleId); binding.tvSaleDetailId.setText("ID: " + saleId);
if (!a.getBoolean("isRefund", false)) { boolean isRefund = a.getBoolean("isRefund", false);
binding.btnRefundSale.setVisibility(View.VISIBLE); if (isRefund) {
binding.btnRefundSale.setVisibility(View.GONE);
} }
if (viewOnly) { if (viewOnly) {
@@ -96,6 +101,16 @@ public class SaleDetailFragment extends Fragment {
binding.spinnerPaymentMethod); binding.spinnerPaymentMethod);
binding.llAddItemRow.setVisibility(View.GONE); binding.llAddItemRow.setVisibility(View.GONE);
binding.llExtraInfo.setVisibility(View.VISIBLE); 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(); loadSaleDetails();
@@ -157,6 +172,13 @@ public class SaleDetailFragment extends Fragment {
binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : ""); binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "");
binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0)); 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()); SpinnerUtils.setSelectionByValue(binding.spinnerPaymentMethod, sale.getPaymentMethod());

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
package com.example.petstoremobile.repositories;
import androidx.lifecycle.LiveData;
import com.example.petstoremobile.api.CouponApi;
import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.utils.Resource;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class CouponRepository extends BaseRepository {
private final CouponApi couponApi;
@Inject
public CouponRepository(CouponApi couponApi) {
super("CouponRepository");
this.couponApi = couponApi;
}
public LiveData<Resource<PageResponse<CouponDTO>>> getAllCoupons(int page, int size, Boolean active, String discountType, String sort) {
return executeCall(couponApi.getAllCoupons(page, size, active, discountType, sort));
}
public LiveData<Resource<CouponDTO>> getCouponById(Long id) {
return executeCall(couponApi.getCouponById(id));
}
public LiveData<Resource<CouponDTO>> createCoupon(CouponDTO coupon) {
return executeCall(couponApi.createCoupon(coupon));
}
public LiveData<Resource<CouponDTO>> updateCoupon(Long id, CouponDTO coupon) {
return executeCall(couponApi.updateCoupon(id, coupon));
}
public LiveData<Resource<Void>> deleteCoupon(Long id) {
return executeCall(couponApi.deleteCoupon(id));
}
public LiveData<Resource<Void>> bulkDeleteCoupons(List<Long> ids) {
return executeCall(couponApi.bulkDeleteCoupons(ids));
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ public class ErrorUtils {
try { try {
if (response.errorBody() != null) { if (response.errorBody() != null) {
String errorJson = response.errorBody().string(); String errorJson = response.errorBody().string();
Log.e(TAG, "Full Error JSON: " + errorJson);
ErrorResponse errorResponse = gson.fromJson(errorJson, ErrorResponse.class); ErrorResponse errorResponse = gson.fromJson(errorJson, ErrorResponse.class);
if (errorResponse != null && errorResponse.getMessage() != null) { if (errorResponse != null && errorResponse.getMessage() != null) {
return errorResponse.getMessage(); return errorResponse.getMessage();

View File

@@ -11,13 +11,31 @@ import java.io.InputStream;
public class FileUtils { public class FileUtils {
public static File getFileFromUri(Context context, Uri uri) { public static File getFileFromUri(Context context, Uri uri) {
try { 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); String fileName = getFileName(context, uri);
if (fileName == null) fileName = "upload_" + System.currentTimeMillis(); if (fileName == null) fileName = "upload_" + System.currentTimeMillis();
InputStream inputStream = context.getContentResolver().openInputStream(uri); InputStream inputStream = context.getContentResolver().openInputStream(uri);
if (inputStream == null) return null;
File tempFile = new File(context.getCacheDir(), fileName); File tempFile = new File(context.getCacheDir(), fileName);
FileOutputStream outputStream = new FileOutputStream(tempFile); FileOutputStream outputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[1024]; byte[] buffer = new byte[4096];
int length; int length;
while ((length = inputStream.read(buffer)) > 0) { while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length); outputStream.write(buffer, 0, length);
@@ -47,4 +65,4 @@ public class FileUtils {
} }
return result; return result;
} }
} }

View File

@@ -129,9 +129,16 @@ public class ImagePickerHelper {
* Prepares a temporary file and launches the camera app. * Prepares a temporary file and launches the camera app.
*/ */
private void launchCamera() { private void launchCamera() {
File photoFile = new File(fragment.requireContext().getCacheDir(), tempFileName); try {
photoUri = FileProvider.getUriForFile(fragment.requireContext(), fragment.requireContext().getPackageName() + ".fileprovider", photoFile); File photoFile = new File(fragment.requireContext().getCacheDir(), tempFileName);
cameraLauncher.launch(photoUri); 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) .setNegativeButton("Cancel", null)
.show(); .show();
} }
} }

View File

@@ -35,6 +35,24 @@ public class InputValidator {
} }
} }
// Checks if the value is a positive decimal number greater than 0
public static boolean isGreaterThanZero(EditText field, String fieldName) {
String value = field.getText().toString().trim();
try {
double num = Double.parseDouble(value);
if (num <= 0) {
field.setError(fieldName + " must be greater than 0");
field.requestFocus();
return false;
}
return true;
} catch (NumberFormatException e) {
field.setError(fieldName + " must be a number");
field.requestFocus();
return false;
}
}
// Checks if the value is a positive decimal number // Checks if the value is a positive decimal number
public static boolean isPositiveDecimal(EditText field, String fieldName) { public static boolean isPositiveDecimal(EditText field, String fieldName) {
String value = field.getText().toString().trim(); String value = field.getText().toString().trim();

View File

@@ -134,6 +134,10 @@ public class UIUtils {
}, },
c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.YEAR), c.get(Calendar.MONTH),
c.get(Calendar.DAY_OF_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.getDatePicker().setMinDate(System.currentTimeMillis() - 1000);
d.show(); d.show();
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.repositories.CouponRepository;
import com.example.petstoremobile.utils.Resource;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class CouponDetailViewModel extends ViewModel {
private final CouponRepository repository;
private long couponId = -1;
private boolean isEditing = false;
@Inject
public CouponDetailViewModel(CouponRepository repository) {
this.repository = repository;
}
public LiveData<Resource<CouponDTO>> loadCoupon(long id) {
return repository.getCouponById(id);
}
public void setCouponId(long id, boolean isEditing) {
this.couponId = id;
this.isEditing = isEditing;
}
public long getCouponId() {
return couponId;
}
public boolean isEditing() {
return isEditing;
}
public LiveData<Resource<CouponDTO>> saveCoupon(CouponDTO dto) {
if (isEditing && couponId > 0) {
return repository.updateCoupon(couponId, dto);
} else {
return repository.createCoupon(dto);
}
}
public LiveData<Resource<Void>> deleteCoupon() {
return repository.deleteCoupon(couponId);
}
}

View File

@@ -0,0 +1,50 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.repositories.CouponRepository;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class CouponListViewModel extends ViewModel {
private final CouponRepository repository;
private final MutableLiveData<List<CouponDTO>> coupons = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
public CouponListViewModel(CouponRepository repository) {
this.repository = repository;
}
public LiveData<List<CouponDTO>> getCoupons() { return coupons; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadCoupons(int page, int size, Boolean active, String discountType, String sort) {
isLoading.setValue(true);
repository.getAllCoupons(page, size, active, discountType, sort).observeForever(resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
coupons.setValue(resource.data.getContent());
isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false);
}
}
});
}
public LiveData<Resource<Void>> bulkDeleteCoupons(List<Long> ids) {
return repository.bulkDeleteCoupons(ids);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -42,7 +42,7 @@ public class SaleListViewModel extends ViewModel {
public LiveData<Boolean> getIsLoading() { return isLoading; } public LiveData<Boolean> getIsLoading() { return isLoading; }
public boolean isLastPage() { return isLastPage; } 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 (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
if (reset) { if (reset) {
@@ -51,7 +51,7 @@ public class SaleListViewModel extends ViewModel {
} }
isLoading.setValue(true); 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 != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<SaleDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue()); List<SaleDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue());

View File

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

View File

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

View File

@@ -5,7 +5,9 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.EmployeeDTO; import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.repositories.EmployeeRepository; import com.example.petstoremobile.repositories.EmployeeRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList; import java.util.ArrayList;
@@ -18,18 +20,24 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel @HiltViewModel
public class StaffListViewModel extends ViewModel { public class StaffListViewModel extends ViewModel {
private final EmployeeRepository repository; private final EmployeeRepository repository;
private final StoreRepository storeRepository;
private final MutableLiveData<List<EmployeeDTO>> employees = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<EmployeeDTO>> employees = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<EmployeeDTO>> filteredEmployees = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<EmployeeDTO>> filteredEmployees = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false); private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private String lastQuery = ""; private String lastQuery = "";
private Long lastStoreId = null;
private String lastStatus = "All Statuses";
@Inject @Inject
public StaffListViewModel(EmployeeRepository repository) { public StaffListViewModel(EmployeeRepository repository, StoreRepository storeRepository) {
this.repository = repository; this.repository = repository;
this.storeRepository = storeRepository;
} }
public LiveData<List<EmployeeDTO>> getFilteredEmployees() { return filteredEmployees; } public LiveData<List<EmployeeDTO>> getFilteredEmployees() { return filteredEmployees; }
public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<Boolean> getIsLoading() { return isLoading; } public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadStaff() { public void loadStaff() {
@@ -38,7 +46,7 @@ public class StaffListViewModel extends ViewModel {
if (resource != null) { if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
employees.setValue(resource.data.getContent()); employees.setValue(resource.data.getContent());
filter(lastQuery); filter(lastQuery, lastStoreId, lastStatus);
isLoading.setValue(false); isLoading.setValue(false);
} else if (resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
isLoading.setValue(false); 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.lastQuery = query;
this.lastStoreId = storeId;
this.lastStatus = status;
List<EmployeeDTO> all = employees.getValue(); List<EmployeeDTO> all = employees.getValue();
if (all == null) return; if (all == null) return;
if (query.isEmpty()) { List<EmployeeDTO> filtered = new ArrayList<>();
filteredEmployees.setValue(new ArrayList<>(all)); String lowerQuery = query.toLowerCase();
} else {
List<EmployeeDTO> filtered = new ArrayList<>(); for (EmployeeDTO e : all) {
String lower = query.toLowerCase(); // Search Query Filter
for (EmployeeDTO e : all) { boolean matchesQuery = query.isEmpty() ||
if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower)) (e.getFullName() != null && e.getFullName().toLowerCase().contains(lowerQuery)) ||
|| (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower)) (e.getUsername() != null && e.getUsername().toLowerCase().contains(lowerQuery)) ||
|| (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower)) (e.getEmail() != null && e.getEmail().toLowerCase().contains(lowerQuery)) ||
|| (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) { (e.getPhone() != null && e.getPhone().toLowerCase().contains(lowerQuery));
filtered.add(e);
} // Store Filter
boolean matchesStore = storeId == null || (e.getPrimaryStoreId() != null && e.getPrimaryStoreId().equals(storeId));
// Status Filter
boolean matchesStatus = status.equals("All Statuses") ||
(status.equals("Active") && Boolean.TRUE.equals(e.getActive())) ||
(status.equals("Inactive") && Boolean.FALSE.equals(e.getActive()));
if (matchesQuery && matchesStore && matchesStatus) {
filtered.add(e);
} }
filteredEmployees.setValue(filtered);
} }
filteredEmployees.setValue(filtered);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_grey">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/headerCoupon"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageButton
android:id="@+id/btnHamburgerCoupon"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/baseline_menu_36"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Open menu"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Coupons"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginStart="8dp"/>
<ImageButton
android:id="@+id/btnBulkDeleteCoupons"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_delete"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="@color/white"
android:contentDescription="Bulk Delete Coupons"
android:visibility="gone"/>
<ImageButton
android:id="@+id/btnToggleFilterCoupon"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_search"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="@color/white"
android:contentDescription="Toggle filter"/>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutFilterCoupon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:visibility="gone"
android:background="@color/primary_dark"
android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@drawable/bg_search_bar"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@android:drawable/ic_menu_search"
android:alpha="0.6"/>
<EditText
android:id="@+id/etSearchCoupon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="Search coupons..."
android:inputType="text"
android:background="@android:color/transparent"
android:textColor="@color/text_dark"
android:textColorHint="#99000000"
android:textSize="14sp"
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp"
android:gravity="center_vertical">
<Spinner
android:id="@+id/spinnerTypeCoupon"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:layout_marginEnd="4dp"/>
<Spinner
android:id="@+id/spinnerStatusCoupon"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:layout_marginStart="4dp"/>
</LinearLayout>
</LinearLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshCoupon"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewCoupon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAddCoupon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:backgroundTint="@color/accent_coral"
android:contentDescription="Add Coupon"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@color/white"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,258 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_grey">
<!-- Top Bar -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvTitleCouponDetail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Coupon Detail"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeleteCouponDetail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"
android:visibility="gone"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<!-- Coupon Code -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Coupon Code"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCouponCodeDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter coupon code"
android:inputType="textCapCharacters"
android:layout_marginBottom="16dp"/>
<!-- Discount Type -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Discount Type"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerDiscountTypeDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Discount Value -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Discount Value"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etDiscountValueDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="0.00"
android:inputType="numberDecimal"
android:layout_marginBottom="16dp"/>
<!-- Min Order Amount -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Min Order Amount"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etMinOrderAmountDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="0.00"
android:inputType="numberDecimal"
android:layout_marginBottom="16dp"/>
<!-- Usage Limit -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Usage Limit"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etUsageLimitDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="No limit"
android:inputType="number"
android:layout_marginBottom="16dp"/>
<!-- Dates -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginEnd="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Starts At"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etStartsAtDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="YYYY-MM-DD"
android:focusable="false"
android:clickable="true"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ends At"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etEndsAtDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="YYYY-MM-DD"
android:focusable="false"
android:clickable="true"/>
</LinearLayout>
</LinearLayout>
<!-- Active -->
<CheckBox
android:id="@+id/cbActiveDetail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Active"
android:textColor="@color/text_dark"
android:textSize="14sp"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- Bottom Action Bar -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnBackCouponDetail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSaveCouponDetail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

View File

@@ -34,9 +34,10 @@
android:layout_width="260dp" android:layout_width="260dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" android:layout_gravity="start"
android:orientation="vertical" android:background="@color/primary_dark"
android:background="@color/primary_dark"> android:orientation="vertical">
<!-- Header: Logo and Store Name (Static) -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -69,221 +70,286 @@
</LinearLayout> </LinearLayout>
<TextView <!-- Scrollable Menu Items -->
android:layout_width="wrap_content" <ScrollView
android:layout_height="wrap_content"
android:text="MANAGE"
android:textColor="@color/text_light"
android:textSize="11sp"
android:letterSpacing="0.15"
android:paddingStart="16dp"
android:paddingTop="24dp"
android:paddingBottom="8dp"/>
<LinearLayout
android:id="@+id/drawerPets"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="0dp"
android:orientation="horizontal" android:layout_weight="1"
android:gravity="center_vertical" android:fillViewport="true">
android:paddingStart="16dp"
android:paddingEnd="16dp" <LinearLayout
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Pets" android:orientation="vertical"
android:textColor="@color/white" android:paddingBottom="24dp">
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout <!-- BUSINESS SECTION -->
android:id="@+id/drawerServices" <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="48dp" android:layout_height="wrap_content"
android:orientation="horizontal" android:text="BUSINESS"
android:gravity="center_vertical" android:textColor="@color/text_light"
android:paddingStart="16dp" android:textSize="11sp"
android:paddingEnd="16dp" android:letterSpacing="0.15"
android:background="?attr/selectableItemBackground"> android:paddingStart="16dp"
<TextView android:paddingTop="24dp"
android:layout_width="match_parent" android:paddingBottom="8dp"/>
android:layout_height="wrap_content"
android:text="Services"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/drawerSuppliers" android:id="@+id/drawerAnalytics"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground"> android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Suppliers" android:text="Analytics"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="15sp"/> android:textSize="15sp"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/drawerAppointments" android:id="@+id/drawerSale"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground"> android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Appointments" android:text="Sales"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="15sp"/> android:textSize="15sp"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/drawerAdoptions" android:id="@+id/drawerAppointments"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground"> android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Adoptions" android:text="Appointments"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="15sp"/> android:textSize="15sp"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/drawerInventory" android:id="@+id/drawerServices"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground"> android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Inventory" android:text="Services"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="15sp"/> android:textSize="15sp"/>
</LinearLayout> </LinearLayout>
<LinearLayout <!-- STORE SECTION -->
android:id="@+id/drawerProducts" <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="48dp" android:layout_height="wrap_content"
android:orientation="horizontal" android:text="STORE"
android:gravity="center_vertical" android:textColor="@color/text_light"
android:paddingStart="16dp" android:textSize="11sp"
android:paddingEnd="16dp" android:letterSpacing="0.15"
android:background="?attr/selectableItemBackground"> android:paddingStart="16dp"
<TextView android:paddingTop="24dp"
android:layout_width="match_parent" android:paddingBottom="8dp"/>
android:layout_height="wrap_content"
android:text="Products"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/drawerSale" android:id="@+id/drawerPets"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground"> android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Sale" android:text="Pets"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="15sp"/> android:textSize="15sp"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/drawerAnalytics" android:id="@+id/drawerAdoptions"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground"> android:background="?attr/selectableItemBackground">
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Analytics" android:text="Adoptions"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="15sp"/> android:textSize="15sp"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/drawerStaff" android:id="@+id/drawerProducts"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground">
android:visibility="gone"> <TextView
<TextView android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="Products"
android:text="Staff Accounts" android:textColor="@color/white"
android:textColor="@color/white" android:textSize="15sp"/>
android:textSize="15sp"/> </LinearLayout>
</LinearLayout>
<LinearLayout <!-- ADMIN SECTION -->
android:id="@+id/drawerPurchaseOrderView" <LinearLayout
android:layout_width="match_parent" android:id="@+id/sectionAdmin"
android:layout_height="48dp" android:layout_width="match_parent"
android:orientation="horizontal" android:layout_height="wrap_content"
android:gravity="center_vertical" android:orientation="vertical">
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="PurchaseOrder"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout <TextView
android:id="@+id/drawerProductSupplier" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="48dp" android:text="ADMIN"
android:orientation="horizontal" android:textColor="@color/text_light"
android:gravity="center_vertical" android:textSize="11sp"
android:paddingStart="16dp" android:letterSpacing="0.15"
android:paddingEnd="16dp" android:paddingStart="16dp"
android:background="?attr/selectableItemBackground"> android:paddingTop="24dp"
<TextView android:paddingBottom="8dp"/>
android:layout_width="match_parent"
android:layout_height="wrap_content" <LinearLayout
android:text="ProductSupplier" android:id="@+id/drawerInventory"
android:textColor="@color/white" android:layout_width="match_parent"
android:textSize="15sp"/> android:layout_height="48dp"
</LinearLayout> android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Inventory"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerSuppliers"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Suppliers"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerCoupons"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Coupons"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerPurchaseOrderView"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Purchase Orders"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerProductSupplier"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Product Suppliers"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/drawerStaff"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Staff Accounts"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout> </LinearLayout>

View File

@@ -117,8 +117,25 @@
android:background="@drawable/bg_spinner" android:background="@drawable/bg_spinner"
android:paddingStart="12dp" android:paddingStart="12dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"/> android:layout_marginStart="4dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp"
android:gravity="center_vertical">
<Spinner
android:id="@+id/spinnerRefundStatus"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:layout_marginEnd="4dp"/>
<Button <Button
android:id="@+id/btnOpenRefund" android:id="@+id/btnOpenRefund"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
package com.petshop.backend.dto.common;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class CouponResponse {
private Long couponId;
private String couponCode;
private String discountType;
private BigDecimal discountValue;
private BigDecimal minOrderAmount;
private Boolean active;
private LocalDateTime startsAt;
private LocalDateTime endsAt;
private Integer usageLimit;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public CouponResponse(Long couponId, String couponCode, String discountType, BigDecimal discountValue, BigDecimal minOrderAmount, Boolean active, LocalDateTime startsAt, LocalDateTime endsAt, Integer usageLimit, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.couponId = couponId;
this.couponCode = couponCode;
this.discountType = discountType;
this.discountValue = discountValue;
this.minOrderAmount = minOrderAmount;
this.active = active;
this.startsAt = startsAt;
this.endsAt = endsAt;
this.usageLimit = usageLimit;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public Long getCouponId() { return couponId; }
public void setCouponId(Long couponId) { this.couponId = couponId; }
public String getCouponCode() { return couponCode; }
public void setCouponCode(String couponCode) { this.couponCode = couponCode; }
public String getDiscountType() { return discountType; }
public void setDiscountType(String discountType) { this.discountType = discountType; }
public BigDecimal getDiscountValue() { return discountValue; }
public void setDiscountValue(BigDecimal discountValue) { this.discountValue = discountValue; }
public BigDecimal getMinOrderAmount() { return minOrderAmount; }
public void setMinOrderAmount(BigDecimal minOrderAmount) { this.minOrderAmount = minOrderAmount; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
public LocalDateTime getStartsAt() { return startsAt; }
public void setStartsAt(LocalDateTime startsAt) { this.startsAt = startsAt; }
public LocalDateTime getEndsAt() { return endsAt; }
public void setEndsAt(LocalDateTime endsAt) { this.endsAt = endsAt; }
public Integer getUsageLimit() { return usageLimit; }
public void setUsageLimit(Integer usageLimit) { this.usageLimit = usageLimit; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

View File

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

View File

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

View File

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

View File

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

View File

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