diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index cef8c8c2..2a5bfc14 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -34,6 +34,9 @@
android:name=".activities.HomeActivity"
android:windowSoftInputMode="adjustResize"
android:exported="false" />
+
diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java
new file mode 100644
index 00000000..385444ed
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java
@@ -0,0 +1,46 @@
+package com.example.petstoremobile.activities;
+
+import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.activity.EdgeToEdge;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import com.example.petstoremobile.databinding.ActivityForgotPasswordBinding;
+import com.example.petstoremobile.utils.InputValidator;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+@AndroidEntryPoint
+public class ForgotPasswordActivity extends AppCompatActivity {
+
+ private ActivityForgotPasswordBinding binding;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ EdgeToEdge.enable(this);
+ super.onCreate(savedInstanceState);
+ binding = ActivityForgotPasswordBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ ViewCompat.setOnApplyWindowInsetsListener(binding.forgotPasswordRoot, (v, insets) -> {
+ Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+ return insets;
+ });
+
+ binding.btnSubmit.setOnClickListener(v -> {
+ if (InputValidator.isValidEmail(binding.etEmail)) {
+ String email = binding.etEmail.getText().toString().trim();
+ // TODO: Implement password reset logic here
+ Toast.makeText(this, "If this email is linked, a reset email will be sent.", Toast.LENGTH_LONG).show();
+ finish();
+ }
+ });
+
+ binding.btnBackToLogin.setOnClickListener(v -> finish());
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java
index f8c89342..25969757 100644
--- a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java
+++ b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java
@@ -89,6 +89,11 @@ public class MainActivity extends AppCompatActivity {
performLogin(username, password);
});
+
+ // Set click listener for forgot password link
+ binding.tvForgotPassword.setOnClickListener(v -> {
+ startActivity(new Intent(this, ForgotPasswordActivity.class));
+ });
}
/**
@@ -133,6 +138,7 @@ public class MainActivity extends AppCompatActivity {
if (resource != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
tokenManager.saveUserId(resource.data.getId());
+ tokenManager.savePrimaryStoreId(resource.data.getStoreId());
}
Toast.makeText(this, "Login successful", Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, HomeActivity.class));
diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/CustomerAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/CustomerAdapter.java
new file mode 100644
index 00000000..30b08b23
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/adapters/CustomerAdapter.java
@@ -0,0 +1,80 @@
+package com.example.petstoremobile.adapters;
+
+import android.graphics.Color;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.api.UserApi;
+import com.example.petstoremobile.databinding.ItemCustomerBinding;
+import com.example.petstoremobile.dtos.CustomerDTO;
+import com.example.petstoremobile.utils.GlideUtils;
+
+import java.util.List;
+
+public class CustomerAdapter extends RecyclerView.Adapter {
+
+ private List list;
+ private OnCustomerClickListener listener;
+ private String baseUrl;
+ private String token;
+
+ public interface OnCustomerClickListener {
+ void onCustomerClick(int position);
+ }
+
+ public CustomerAdapter(List list, OnCustomerClickListener listener) {
+ this.list = list;
+ this.listener = listener;
+ }
+
+ public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
+ public void setToken(String token) { this.token = token; }
+
+ public static class CustomerViewHolder extends RecyclerView.ViewHolder {
+ final ItemCustomerBinding binding;
+
+ public CustomerViewHolder(@NonNull ItemCustomerBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+ }
+
+ @NonNull
+ @Override
+ public CustomerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ ItemCustomerBinding binding = ItemCustomerBinding.inflate(
+ LayoutInflater.from(parent.getContext()), parent, false);
+ return new CustomerViewHolder(binding);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull CustomerViewHolder holder, int position) {
+ CustomerDTO c = list.get(position);
+ ItemCustomerBinding b = holder.binding;
+
+ b.tvCustomerFullName.setText(c.getFullName() != null ? c.getFullName() : "");
+ b.tvCustomerUsername.setText("@" + (c.getUsername() != null ? c.getUsername() : ""));
+ b.tvCustomerEmail.setText(c.getEmail() != null ? c.getEmail() : "");
+
+ int points = c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0;
+ b.tvCustomerLoyalty.setText(points + " pts");
+
+ boolean active = Boolean.TRUE.equals(c.getActive());
+ b.tvCustomerStatus.setText(active ? "Active" : "Inactive");
+ b.tvCustomerStatus.setTextColor(active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336"));
+
+ if (baseUrl != null && c.getCustomerId() != null) {
+ String imageUrl = baseUrl + String.format(UserApi.AVATAR_PATH, c.getCustomerId());
+ GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), b.ivCustomerProfile, imageUrl, token, R.drawable.placeholder);
+ } else {
+ b.ivCustomerProfile.setImageResource(R.drawable.placeholder);
+ }
+
+ holder.itemView.setOnClickListener(v -> listener.onCustomerClick(position));
+ }
+
+ @Override
+ public int getItemCount() { return list.size(); }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java b/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java
index ec397909..6e1de73d 100644
--- a/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java
+++ b/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java
@@ -24,7 +24,8 @@ public interface AdoptionApi {
@Query("status") String status,
@Query("storeId") Long storeId,
@Query("date") String date,
- @Query("employeeId") Long employeeId);
+ @Query("employeeId") Long employeeId,
+ @Query("sort") String sort);
@GET("api/v1/adoptions/{id}")
Call getAdoptionById(@Path("id") Long id);
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java
index 5b8a37a7..5bc88643 100644
--- a/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java
+++ b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java
@@ -24,7 +24,8 @@ public interface AppointmentApi {
@Query("status") String status,
@Query("storeId") Long storeId,
@Query("date") String date,
- @Query("employeeId") Long employeeId);
+ @Query("employeeId") Long employeeId,
+ @Query("sort") String sort);
@GET("api/v1/appointments/{id}")
Call getAppointmentById(@Path("id") Long id);
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java
index 855ba5fa..55c51682 100644
--- a/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java
+++ b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java
@@ -7,11 +7,14 @@ 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;
-//api calls to get customers
public interface CustomerApi {
@GET("api/v1/customers")
@@ -20,6 +23,15 @@ public interface CustomerApi {
@GET("api/v1/customers/{customerId}")
Call getCustomerById(@Path("customerId") Long customerId);
+ @PUT("api/v1/customers/{customerId}")
+ Call updateCustomer(@Path("customerId") Long customerId, @Body CustomerDTO customer);
+
+ @DELETE("api/v1/customers/{customerId}")
+ Call deleteCustomer(@Path("customerId") Long customerId);
+
+ @POST("api/v1/auth/register")
+ Call registerCustomer(@Body CustomerDTO customer);
+
@GET("api/v1/dropdowns/customers")
Call> getCustomerDropdowns();
-}
\ No newline at end of file
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java
index d13a5ed4..97423002 100644
--- a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java
+++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java
@@ -47,6 +47,9 @@ public interface PetApi {
@GET("api/v1/dropdowns/pets")
Call> getPetDropdowns();
+ @GET("api/v1/dropdowns/pet-species")
+ Call> getPetSpeciesDropdowns();
+
// Get pet by id
@GET("api/v1/pets/{id}")
Call getPetById(@Path("id") Long id);
diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java
index aa9ab363..dc6096de 100644
--- a/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java
+++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java
@@ -15,6 +15,7 @@ public class TokenManager {
private static final String ROLE_KEY = "role";
private static final String PREFS_NAME = "auth_prefs";
private static final String USER_ID_KEY = "user_id";
+ private static final String PRIMARY_STORE_ID_KEY = "primary_store_id";
private SharedPreferences prefs;
@@ -54,6 +55,19 @@ public class TokenManager {
prefs.edit().putLong(USER_ID_KEY, userId).apply();
}
+ public void savePrimaryStoreId(Long storeId) {
+ if (storeId != null) {
+ prefs.edit().putLong(PRIMARY_STORE_ID_KEY, storeId).apply();
+ } else {
+ prefs.edit().remove(PRIMARY_STORE_ID_KEY).apply();
+ }
+ }
+
+ public Long getPrimaryStoreId() {
+ long id = prefs.getLong(PRIMARY_STORE_ID_KEY, -1L);
+ return id == -1L ? null : id;
+ }
+
//Check if logged in
public boolean isLoggedIn() {
return getToken() != null;
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java
index 21376a63..52d96a1e 100644
--- a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java
@@ -5,61 +5,72 @@ import com.google.gson.annotations.SerializedName;
public class CustomerDTO {
@SerializedName("id")
private Long customerId;
+ private String username;
private String firstName;
private String lastName;
+ private String fullName;
private String email;
+ private String phone;
+ private Boolean active;
+ private Integer loyaltyPoints;
+ private Long primaryStoreId;
private String createdAt;
private String updatedAt;
+ private String password;
- public Long getCustomerId() {
- return customerId;
- }
+ public CustomerDTO() {}
- public void setCustomerId(Long customerId) {
- this.customerId = customerId;
- }
-
- public String getFirstName() {
- return firstName;
- }
-
- public void setFirstName(String firstName) {
+ public CustomerDTO(String username, String password, String firstName, String lastName,
+ String email, String phone) {
+ this.username = username;
+ this.password = password;
this.firstName = firstName;
- }
-
- public String getLastName() {
- return lastName;
- }
-
- public void setLastName(String lastName) {
this.lastName = lastName;
- }
-
- public String getEmail() {
- return email;
- }
-
- public void setEmail(String email) {
this.email = email;
+ this.phone = phone;
}
+ public Long getCustomerId() { return customerId; }
+ public void setCustomerId(Long customerId) { this.customerId = customerId; }
+
+ public String getUsername() { return username; }
+ public void setUsername(String username) { this.username = username; }
+
+ public String getFirstName() { return firstName; }
+ public void setFirstName(String firstName) { this.firstName = firstName; }
+
+ public String getLastName() { return lastName; }
+ public void setLastName(String lastName) { this.lastName = lastName; }
+
public String getFullName() {
- return firstName + " " + lastName;
+ if (fullName != null) return fullName;
+ String f = firstName != null ? firstName : "";
+ String l = lastName != null ? lastName : "";
+ return (f + " " + l).trim();
}
+ public void setFullName(String fullName) { this.fullName = fullName; }
- public String getCreatedAt() {
- return createdAt;
- }
+ public String getEmail() { return email; }
+ public void setEmail(String email) { this.email = email; }
- public void setCreatedAt(String createdAt) {
- this.createdAt = createdAt;
- }
+ public String getPhone() { return phone; }
+ public void setPhone(String phone) { this.phone = phone; }
- public String getUpdatedAt() {
- return updatedAt;
- }
+ public Boolean getActive() { return active; }
+ public void setActive(Boolean active) { this.active = active; }
- public void setUpdatedAt(String updatedAt) {
- this.updatedAt = updatedAt;
- }
+ public Integer getLoyaltyPoints() { return loyaltyPoints; }
+ public void setLoyaltyPoints(Integer loyaltyPoints) { this.loyaltyPoints = loyaltyPoints; }
+
+ public Long getPrimaryStoreId() { return primaryStoreId; }
+ public void setPrimaryStoreId(Long primaryStoreId) { this.primaryStoreId = primaryStoreId; }
+
+ public String getCreatedAt() { return createdAt; }
+ public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
+
+ public String getUpdatedAt() { return updatedAt; }
+ public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; }
+
+ public String getPassword() { return password; }
+ public void setPassword(String password) { this.password = password; }
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java
index edb2a132..f309978f 100644
--- a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java
+++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java
@@ -102,6 +102,10 @@ public class SaleDTO {
return couponId;
}
+ public void setCouponId(Long couponId) {
+ this.couponId = couponId;
+ }
+
public Integer getPointsEarned() {
return pointsEarned;
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java
index e7180292..0aab247b 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java
@@ -89,6 +89,7 @@ public class ListFragment extends Fragment {
binding.drawerPurchaseOrderView.setOnClickListener(v -> navigateTo(R.id.nav_purchase_order));
binding.drawerSale.setOnClickListener(v -> navigateTo(R.id.nav_sale));
binding.drawerStaff.setOnClickListener(v -> navigateTo(R.id.nav_staff));
+ binding.drawerCustomers.setOnClickListener(v -> navigateTo(R.id.nav_customer));
binding.drawerAnalytics.setOnClickListener(v -> navigateTo(R.id.nav_analytics));
binding.drawerCoupons.setOnClickListener(v -> navigateTo(R.id.nav_coupon));
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java
index b31243c2..7f8fc039 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java
@@ -160,7 +160,8 @@ public class ProfileFragment extends Fragment {
android.content.Intent serviceIntent = new android.content.Intent(requireContext(), ChatNotificationService.class);
requireContext().stopService(serviceIntent);
- tokenManager.clearLoginData(); // clear the token for next login
+ // clear the token for next login
+ tokenManager.clearLoginData();
//get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen
android.content.Intent intent = new android.content.Intent(getActivity(), MainActivity.class);
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP | android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java
index 2d687c6e..cfc05233 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java
@@ -17,6 +17,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.AdoptionAdapter;
+import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentAdoptionBinding;
import com.example.petstoremobile.dtos.AdoptionDTO;
import com.example.petstoremobile.dtos.StoreDTO;
@@ -38,6 +39,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import javax.inject.Inject;
+
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
@@ -52,6 +55,8 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
private boolean isMonthMode = false;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
+ @Inject TokenManager tokenManager;
+
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -117,7 +122,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
public void onResume() {
super.onResume();
loadAdoptions();
- viewModel.loadStores();
+ if (!isStaff()) viewModel.loadStores();
}
private void toggleCalendarMode() {
@@ -128,8 +133,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
}
private void setupFilterToggle() {
- UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption,
- binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption);
+ if (isStaff()) {
+ UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption,
+ binding.etSearchAdoption, binding.spinnerStatusAdoption);
+ binding.spinnerStoreAdoption.setVisibility(View.GONE);
+ } else {
+ UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption,
+ binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption);
+ }
+ }
+
+ private boolean isStaff() {
+ return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void setupCalendar() {
@@ -195,10 +210,15 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
String query = binding.etSearchAdoption.getText().toString().trim();
String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses";
- Long storeId = null;
- List stores = viewModel.getStores().getValue();
- if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
- storeId = stores.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId();
+ Long storeId;
+ if (isStaff()) {
+ storeId = tokenManager.getPrimaryStoreId();
+ } else {
+ storeId = null;
+ List stores = viewModel.getStores().getValue();
+ if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
+ storeId = stores.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId();
+ }
}
String selectedDateString = null;
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java
index 8610790e..037c7ce9 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java
@@ -17,6 +17,7 @@ import android.view.ViewGroup;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.AppointmentAdapter;
+import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentAppointmentBinding;
import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.StoreDTO;
@@ -39,6 +40,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import javax.inject.Inject;
+
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
@@ -52,6 +55,8 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private AuthViewModel authViewModel;
private BulkDeleteHandler bulkDeleteHandler;
+ @Inject TokenManager tokenManager;
+
private CalendarDay selectedCalendarDay;
private boolean isMonthMode = false;
private Long currentUserId = null;
@@ -126,7 +131,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
public void onResume() {
super.onResume();
loadAppointmentData();
- viewModel.loadStores();
+ if (!isStaff()) viewModel.loadStores();
}
private void toggleCalendarMode() {
@@ -151,8 +156,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
}
private void setupFilterToggle() {
- UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment,
- binding.spinnerStatus, binding.spinnerStore);
+ if (isStaff()) {
+ UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment, binding.spinnerStatus);
+ binding.spinnerStore.setVisibility(View.GONE);
+ } else {
+ UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment,
+ binding.spinnerStatus, binding.spinnerStore);
+ }
}
private void setupCalendar() {
@@ -227,14 +237,23 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
}
}
+ private boolean isStaff() {
+ return "STAFF".equalsIgnoreCase(tokenManager.getRole());
+ }
+
private void loadAppointmentData() {
String query = binding.etSearchAppointment.getText().toString().trim();
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
-
- Long storeId = null;
- List stores = viewModel.getStores().getValue();
- if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
- storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
+
+ Long storeId;
+ if (isStaff()) {
+ storeId = tokenManager.getPrimaryStoreId();
+ } else {
+ storeId = null;
+ List stores = viewModel.getStores().getValue();
+ if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
+ storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
+ }
}
String selectedDateString = null;
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java
new file mode 100644
index 00000000..69fb967c
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java
@@ -0,0 +1,125 @@
+package com.example.petstoremobile.fragments.listfragments;
+
+import android.os.Bundle;
+import android.view.*;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.fragment.NavHostFragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import com.example.petstoremobile.R;
+import com.example.petstoremobile.adapters.CustomerAdapter;
+import com.example.petstoremobile.api.auth.TokenManager;
+import com.example.petstoremobile.databinding.FragmentCustomerBinding;
+import com.example.petstoremobile.dtos.CustomerDTO;
+import com.example.petstoremobile.utils.SpinnerUtils;
+import com.example.petstoremobile.utils.UIUtils;
+import com.example.petstoremobile.viewmodels.CustomerListViewModel;
+import dagger.hilt.android.AndroidEntryPoint;
+import java.util.*;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+@AndroidEntryPoint
+public class CustomerFragment extends Fragment implements CustomerAdapter.OnCustomerClickListener {
+
+ private FragmentCustomerBinding binding;
+ private CustomerListViewModel viewModel;
+ private List customerList = new ArrayList<>();
+ private CustomerAdapter adapter;
+
+ @Inject @Named("baseUrl") String baseUrl;
+ @Inject TokenManager tokenManager;
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ binding = FragmentCustomerBinding.inflate(inflater, container, false);
+ viewModel = new ViewModelProvider(this).get(CustomerListViewModel.class);
+
+ setupRecyclerView();
+ setupSearch();
+ setupStatusFilter();
+ setupSwipeRefresh();
+ observeViewModel();
+
+ viewModel.loadCustomers();
+
+ binding.fabAddCustomer.setOnClickListener(v -> openDetail(-1));
+
+ UIUtils.setupHamburgerMenu(binding.btnHamburgerCustomer, this);
+ UIUtils.setupFilterToggle(binding.btnToggleFilterCustomer, binding.layoutFilterCustomer,
+ binding.etSearchCustomer, binding.spinnerStatusCustomer);
+
+ return binding.getRoot();
+ }
+
+ private void observeViewModel() {
+ viewModel.getFilteredCustomers().observe(getViewLifecycleOwner(), list -> {
+ customerList.clear();
+ customerList.addAll(list);
+ adapter.notifyDataSetChanged();
+ });
+
+ viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
+ binding.swipeRefreshCustomer.setRefreshing(loading);
+ });
+ }
+
+ private void setupRecyclerView() {
+ adapter = new CustomerAdapter(customerList, this);
+ adapter.setBaseUrl(baseUrl);
+ adapter.setToken(tokenManager.getToken());
+ binding.recyclerViewCustomer.setLayoutManager(new LinearLayoutManager(getContext()));
+ binding.recyclerViewCustomer.setAdapter(adapter);
+ }
+
+ private void setupStatusFilter() {
+ String[] statuses = {"All Statuses", "Active", "Inactive"};
+ SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCustomer, statuses, this::applyFilters);
+ }
+
+ private void setupSearch() {
+ UIUtils.attachSearch(binding.etSearchCustomer, this::applyFilters);
+ }
+
+ private void applyFilters() {
+ String query = binding.etSearchCustomer.getText().toString().trim();
+ String status = binding.spinnerStatusCustomer.getSelectedItem() != null ?
+ binding.spinnerStatusCustomer.getSelectedItem().toString() : "All Statuses";
+ viewModel.filter(query, status);
+ }
+
+ private void setupSwipeRefresh() {
+ binding.swipeRefreshCustomer.setOnRefreshListener(viewModel::loadCustomers);
+ }
+
+ private void openDetail(int position) {
+ Bundle args = new Bundle();
+ if (position != -1) {
+ CustomerDTO c = customerList.get(position);
+ args.putLong("customerId", c.getCustomerId() != null ? c.getCustomerId() : -1);
+ args.putString("username", c.getUsername() != null ? c.getUsername() : "");
+ args.putString("firstName", c.getFirstName() != null ? c.getFirstName() : "");
+ args.putString("lastName", c.getLastName() != null ? c.getLastName() : "");
+ args.putString("email", c.getEmail() != null ? c.getEmail() : "");
+ args.putString("phone", c.getPhone() != null ? c.getPhone() : "");
+ args.putBoolean("active", Boolean.TRUE.equals(c.getActive()));
+ args.putInt("loyaltyPoints", c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0);
+ args.putBoolean("isEditing", true);
+ }
+ NavHostFragment.findNavController(this).navigate(R.id.nav_customer_detail, args);
+ }
+
+ @Override
+ public void onCustomerClick(int position) {
+ openDetail(position);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ binding = null;
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java
index 6cefedb8..4e2c2337 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java
@@ -16,6 +16,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.InventoryAdapter;
+import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentInventoryBinding;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.StoreDTO;
@@ -27,6 +28,8 @@ import com.example.petstoremobile.utils.SpinnerUtils;
import java.util.ArrayList;
import java.util.List;
+import javax.inject.Inject;
+
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
@@ -38,6 +41,8 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
private InventoryListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
+ @Inject TokenManager tokenManager;
+
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -99,7 +104,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
@Override
public void onResume() {
super.onResume();
- viewModel.loadStores();
+ if (!isStaff()) viewModel.loadStores();
}
@Override
@@ -109,7 +114,16 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
}
private void setupFilterToggle() {
- UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore);
+ if (isStaff()) {
+ UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory);
+ binding.spinnerStore.setVisibility(View.GONE);
+ } else {
+ UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore);
+ }
+ }
+
+ private boolean isStaff() {
+ return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void setupSearch() {
@@ -150,10 +164,15 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : "";
if (query.isEmpty()) query = null;
- Long storeId = null;
- List stores = viewModel.getStores().getValue();
- if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
- storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
+ Long storeId;
+ if (isStaff()) {
+ storeId = tokenManager.getPrimaryStoreId();
+ } else {
+ storeId = null;
+ List stores = viewModel.getStores().getValue();
+ if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
+ storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
+ }
}
viewModel.loadInventory(reset, query, storeId);
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java
index bc1b9a6f..a9d153e4 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java
@@ -88,6 +88,11 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshPet.setRefreshing(loading);
});
+
+ viewModel.getSpeciesOptions().observe(getViewLifecycleOwner(), options -> {
+ String[] arr = options.toArray(new String[0]);
+ SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, arr, this::loadPetData);
+ });
}
private void setupBulkDelete() {
@@ -107,12 +112,23 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
public void onResume() {
super.onResume();
loadPetData();
- viewModel.loadStores();
+ viewModel.loadSpecies();
+ if (!isStaff()) viewModel.loadStores();
}
private void setupFilterToggle() {
- UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet,
- binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore);
+ if (isStaff()) {
+ UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet,
+ binding.spinnerStatus, binding.spinnerSpecies);
+ binding.spinnerStore.setVisibility(View.GONE);
+ } else {
+ UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet,
+ binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore);
+ }
+ }
+
+ private boolean isStaff() {
+ return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void setupSearch() {
@@ -125,8 +141,8 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
}
private void setupSpeciesFilter() {
- String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"};
- SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, species, this::loadPetData);
+ String[] initial = {"All Species"};
+ SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, initial, this::loadPetData);
}
private void setupStoreFilter() {
@@ -141,11 +157,16 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
String query = binding.etSearchPet.getText().toString().trim();
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species";
-
- Long storeId = null;
- List stores = viewModel.getStores().getValue();
- if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
- storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
+
+ Long storeId;
+ if (isStaff()) {
+ storeId = tokenManager.getPrimaryStoreId();
+ } else {
+ storeId = null;
+ List stores = viewModel.getStores().getValue();
+ if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
+ storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
+ }
}
viewModel.loadPets(query, status, species, storeId);
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java
index 64dc0ea5..10b9ee3c 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java
@@ -14,6 +14,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.PurchaseOrderAdapter;
+import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding;
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.dtos.StoreDTO;
@@ -24,6 +25,8 @@ import com.example.petstoremobile.viewmodels.PurchaseOrderListViewModel;
import java.util.ArrayList;
import java.util.List;
+import javax.inject.Inject;
+
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
@@ -35,6 +38,8 @@ public class PurchaseOrderFragment extends Fragment
private PurchaseOrderAdapter adapter;
private PurchaseOrderListViewModel viewModel;
+ @Inject TokenManager tokenManager;
+
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -79,11 +84,20 @@ public class PurchaseOrderFragment extends Fragment
public void onResume() {
super.onResume();
loadData();
- viewModel.loadStores();
+ if (!isStaff()) viewModel.loadStores();
}
private void setupFilterToggle() {
- UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO, binding.spinnerStore);
+ if (isStaff()) {
+ UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO);
+ binding.spinnerStore.setVisibility(View.GONE);
+ } else {
+ UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO, binding.spinnerStore);
+ }
+ }
+
+ private boolean isStaff() {
+ return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void setupSearch() {
@@ -108,10 +122,15 @@ public class PurchaseOrderFragment extends Fragment
String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : "";
if (query.isEmpty()) query = null;
- Long storeId = null;
- List stores = viewModel.getStores().getValue();
- if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
- storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
+ Long storeId;
+ if (isStaff()) {
+ storeId = tokenManager.getPrimaryStoreId();
+ } else {
+ storeId = null;
+ List stores = viewModel.getStores().getValue();
+ if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
+ storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
+ }
}
viewModel.loadPurchaseOrders(query, storeId);
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java
index b3fd9546..787e99a7 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java
@@ -15,6 +15,7 @@ import android.widget.Toast;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.SaleAdapter;
+import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentSaleBinding;
import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.dtos.StoreDTO;
@@ -25,6 +26,8 @@ import com.example.petstoremobile.viewmodels.SaleListViewModel;
import java.util.ArrayList;
import java.util.List;
+import javax.inject.Inject;
+
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
@@ -35,6 +38,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
private SaleAdapter adapter;
private SaleListViewModel viewModel;
+ @Inject TokenManager tokenManager;
+
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -87,12 +92,22 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
@Override
public void onResume() {
super.onResume();
- viewModel.loadStores();
+ if (!isStaff()) viewModel.loadStores();
}
private void setupFilterToggle() {
- UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale,
- binding.spinnerPaymentMethod, binding.spinnerStore, binding.spinnerRefundStatus);
+ if (isStaff()) {
+ UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale,
+ binding.spinnerPaymentMethod, binding.spinnerRefundStatus);
+ binding.spinnerStore.setVisibility(View.GONE);
+ } else {
+ UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale,
+ binding.spinnerPaymentMethod, binding.spinnerStore, binding.spinnerRefundStatus);
+ }
+ }
+
+ private boolean isStaff() {
+ return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void setupStoreFilter() {
@@ -149,10 +164,15 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
paymentMethod = (String) binding.spinnerPaymentMethod.getSelectedItem();
}
- Long storeId = null;
- List stores = viewModel.getStores().getValue();
- if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
- storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
+ Long storeId;
+ if (isStaff()) {
+ storeId = tokenManager.getPrimaryStoreId();
+ } else {
+ storeId = null;
+ List stores = viewModel.getStores().getValue();
+ if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
+ storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
+ }
}
Boolean isRefund = null;
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java
index 1e109213..18fab4ec 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java
@@ -17,11 +17,14 @@ import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
+import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.viewmodels.AdoptionDetailViewModel;
import java.math.BigDecimal;
import java.util.*;
+import javax.inject.Inject;
+
import dagger.hilt.android.AndroidEntryPoint;
/**
@@ -34,6 +37,8 @@ public class AdoptionDetailFragment extends Fragment {
private AdoptionDetailViewModel viewModel;
private boolean isUpdatingUI = false;
+ @Inject TokenManager tokenManager;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -82,7 +87,7 @@ public class AdoptionDetailFragment extends Fragment {
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> {
AdoptionDetailViewModel.ViewState state = viewModel.getViewState().getValue();
- Long storeId = state != null ? state.selectedStoreId : null;
+ Long storeId = isStaff() ? tokenManager.getPrimaryStoreId() : (state != null ? state.selectedStoreId : null);
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, list,
DropdownDTO::getLabel, "-- Select Store --", storeId, DropdownDTO::getId);
});
@@ -113,6 +118,7 @@ public class AdoptionDetailFragment extends Fragment {
binding.spinnerAdoptionCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ if (isUpdatingUI) return;
viewModel.onCustomerSelected(position);
}
@Override
@@ -122,6 +128,7 @@ public class AdoptionDetailFragment extends Fragment {
binding.spinnerAdoptionStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ if (isUpdatingUI) return;
viewModel.onStoreSelected(position);
}
@Override
@@ -210,9 +217,15 @@ public class AdoptionDetailFragment extends Fragment {
if (employees != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee,
employees, DropdownDTO::getLabel, "-- Select Staff --", state.selectedEmployeeId, DropdownDTO::getId);
+ if (isStaff()) UIUtils.setViewsEnabled(false, binding.spinnerAdoptionStore);
+
isUpdatingUI = false;
}
+ private boolean isStaff() {
+ return "STAFF".equalsIgnoreCase(tokenManager.getRole());
+ }
+
private void saveAdoption() {
if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionCustomer, "Customer")) return;
if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionPet, "Pet")) return;
@@ -225,9 +238,16 @@ public class AdoptionDetailFragment extends Fragment {
try { fee = new BigDecimal(feeStr); } catch (NumberFormatException ignored) {}
}
- DropdownDTO customer = viewModel.getCustomerList().getValue().get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1);
- DropdownDTO pet = viewModel.getPetList().getValue().get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1);
- DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1);
+ List customerListVal = viewModel.getCustomerList().getValue();
+ List petListVal = viewModel.getPetList().getValue();
+ List storeListVal = viewModel.getStoreList().getValue();
+ if (customerListVal == null || petListVal == null || storeListVal == null) {
+ Toast.makeText(getContext(), "Data not loaded yet, please try again", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ DropdownDTO customer = customerListVal.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1);
+ DropdownDTO pet = petListVal.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1);
+ DropdownDTO store = storeListVal.get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1);
Long employeeId = null;
if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0 && viewModel.getEmployeeList().getValue() != null) {
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java
index 6c524d75..05b83622 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java
@@ -23,10 +23,13 @@ import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
+import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.viewmodels.AppointmentDetailViewModel;
import java.util.List;
+import javax.inject.Inject;
+
import dagger.hilt.android.AndroidEntryPoint;
/**
@@ -43,6 +46,8 @@ public class AppointmentDetailFragment extends Fragment {
private AppointmentDetailViewModel viewModel;
private boolean isUpdatingUI = false;
+ @Inject TokenManager tokenManager;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -89,6 +94,7 @@ public class AppointmentDetailFragment extends Fragment {
binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ if (isUpdatingUI) return;
viewModel.onCustomerSelected(position);
}
@Override
@@ -98,6 +104,7 @@ public class AppointmentDetailFragment extends Fragment {
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
@@ -129,7 +136,7 @@ public class AppointmentDetailFragment extends Fragment {
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue();
- Long id = state != null ? state.selectedStoreId : null;
+ Long id = isStaff() ? tokenManager.getPrimaryStoreId() : (state != null ? state.selectedStoreId : null);
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, list, DropdownDTO::getLabel, "-- Select Store --", id, DropdownDTO::getId);
});
@@ -201,9 +208,15 @@ public class AppointmentDetailFragment extends Fragment {
if (staff != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff,
staff, DropdownDTO::getLabel, "-- Select Staff --", state.selectedStaffId, DropdownDTO::getId);
+ if (isStaff()) binding.spinnerStore.setEnabled(false);
+
isUpdatingUI = false;
}
+ private boolean isStaff() {
+ return "STAFF".equalsIgnoreCase(tokenManager.getRole());
+ }
+
private void notifyDateTimeStatusChange() {
if (isUpdatingUI) return;
@@ -248,7 +261,12 @@ public class AppointmentDetailFragment extends Fragment {
String date = binding.etAppointmentDate.getText().toString().trim();
String time = buildTimeString();
- String status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase();
+ Object selectedStatus = binding.spinnerAppointmentStatus.getSelectedItem();
+ if (selectedStatus == null) {
+ Toast.makeText(getContext(), "Please select a status", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ String status = selectedStatus.toString().toUpperCase();
if (!viewModel.isValidFutureBooking(status, date, time)) {
DialogUtils.showInfoDialog(requireContext(), "Invalid Time", "Booked appointments must be in the future.");
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java
index 7646358f..ff172bdc 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java
@@ -16,6 +16,7 @@ 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.DateTimeUtils;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils;
@@ -95,6 +96,9 @@ public class CouponDetailFragment extends Fragment {
}
private void loadCouponDetails() {
+ binding.tvCouponId.setText(DateTimeUtils.formatId(couponId));
+ binding.tvCouponId.setVisibility(View.VISIBLE);
+
viewModel.loadCoupon(couponId).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
CouponDTO coupon = resource.data;
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java
new file mode 100644
index 00000000..f7a8c5a3
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java
@@ -0,0 +1,175 @@
+package com.example.petstoremobile.fragments.listfragments.detailfragments;
+
+import android.os.Bundle;
+import android.view.*;
+import android.widget.*;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.fragment.NavHostFragment;
+import com.example.petstoremobile.databinding.FragmentCustomerDetailBinding;
+import com.example.petstoremobile.dtos.CustomerDTO;
+import com.example.petstoremobile.utils.DialogUtils;
+import com.example.petstoremobile.utils.InputValidator;
+import com.example.petstoremobile.utils.SpinnerUtils;
+import com.example.petstoremobile.utils.UIUtils;
+import com.example.petstoremobile.viewmodels.CustomerDetailViewModel;
+import com.example.petstoremobile.utils.Resource;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+@AndroidEntryPoint
+public class CustomerDetailFragment extends Fragment {
+
+ private FragmentCustomerDetailBinding binding;
+ private CustomerDetailViewModel viewModel;
+
+ private final String[] STATUSES = {"Active", "Inactive"};
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ binding = FragmentCustomerDetailBinding.inflate(inflater, container, false);
+ viewModel = new ViewModelProvider(this).get(CustomerDetailViewModel.class);
+
+ setupSpinners();
+ handleArguments();
+
+ binding.btnCustomerBack.setOnClickListener(v -> navigateBack());
+ binding.btnSaveCustomer.setOnClickListener(v -> save());
+ binding.btnDeleteCustomer.setOnClickListener(v -> confirmDelete());
+
+ UIUtils.formatPhoneInput(binding.etCustomerPhone);
+
+ return binding.getRoot();
+ }
+
+ private void setupSpinners() {
+ SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerCustomerStatus, STATUSES);
+ }
+
+ private void handleArguments() {
+ Bundle a = getArguments();
+ if (a != null && a.getBoolean("isEditing", false)) {
+ long customerId = a.getLong("customerId", -1);
+ viewModel.setCustomerId(customerId, true);
+
+ binding.tvCustomerMode.setText("Edit Customer");
+ binding.tvCustomerId.setText("ID: " + customerId);
+ binding.tvCustomerId.setVisibility(View.VISIBLE);
+ binding.btnDeleteCustomer.setVisibility(View.VISIBLE);
+
+ // Hide password field in edit mode
+ binding.tvCustomerPasswordLabel.setVisibility(View.GONE);
+ binding.etCustomerPassword.setVisibility(View.GONE);
+
+ // Show loyalty points
+ binding.tvLoyaltyPointsLabel.setVisibility(View.VISIBLE);
+ binding.tvCustomerLoyaltyPoints.setVisibility(View.VISIBLE);
+
+ loadCustomerData(customerId);
+ } else {
+ viewModel.setCustomerId(-1, false);
+ binding.tvCustomerMode.setText("Add Customer");
+ binding.btnDeleteCustomer.setVisibility(View.GONE);
+ binding.tvCustomerId.setVisibility(View.GONE);
+ binding.tvLoyaltyPointsLabel.setVisibility(View.GONE);
+ binding.tvCustomerLoyaltyPoints.setVisibility(View.GONE);
+ }
+ }
+
+ private void loadCustomerData(long id) {
+ viewModel.loadCustomer(id).observe(getViewLifecycleOwner(), resource -> {
+ if (resource != null) {
+ setLoading(resource.status == Resource.Status.LOADING);
+ if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
+ CustomerDTO c = resource.data;
+ binding.etCustomerUsername.setText(c.getUsername() != null ? c.getUsername() : "");
+ binding.etCustomerFirstName.setText(c.getFirstName() != null ? c.getFirstName() : "");
+ binding.etCustomerLastName.setText(c.getLastName() != null ? c.getLastName() : "");
+ binding.etCustomerEmail.setText(c.getEmail() != null ? c.getEmail() : "");
+ binding.etCustomerPhone.setText(c.getPhone() != null ? c.getPhone() : "");
+ binding.spinnerCustomerStatus.setSelection(Boolean.TRUE.equals(c.getActive()) ? 0 : 1);
+ int pts = c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0;
+ binding.tvCustomerLoyaltyPoints.setText(String.valueOf(pts));
+ }
+ }
+ });
+ }
+
+ private void setLoading(boolean loading) {
+ if (binding != null && binding.progressBar != null) {
+ binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ private void save() {
+ if (!InputValidator.isNotEmpty(binding.etCustomerUsername, "Username")) return;
+
+ if (!viewModel.isEditing()) {
+ if (!InputValidator.isNotEmpty(binding.etCustomerPassword, "Password")) return;
+ String pass = binding.etCustomerPassword.getText().toString();
+ if (pass.length() < 6) {
+ binding.etCustomerPassword.setError("At least 6 characters");
+ binding.etCustomerPassword.requestFocus();
+ return;
+ }
+ }
+
+ if (!InputValidator.isNotEmpty(binding.etCustomerFirstName, "First Name")) return;
+ if (!InputValidator.isNotEmpty(binding.etCustomerLastName, "Last Name")) return;
+ if (!InputValidator.isValidEmail(binding.etCustomerEmail)) return;
+ if (!InputValidator.isValidPhone(binding.etCustomerPhone)) return;
+
+ String username = binding.etCustomerUsername.getText().toString().trim();
+ String password = viewModel.isEditing() ? null : binding.etCustomerPassword.getText().toString().trim();
+ String firstName = binding.etCustomerFirstName.getText().toString().trim();
+ String lastName = binding.etCustomerLastName.getText().toString().trim();
+ String email = binding.etCustomerEmail.getText().toString().trim();
+ String phone = binding.etCustomerPhone.getText().toString().trim();
+ boolean active = binding.spinnerCustomerStatus.getSelectedItemPosition() == 0;
+
+ CustomerDTO dto = new CustomerDTO(username, password, firstName, lastName, email, phone);
+ dto.setFullName(firstName + " " + lastName);
+ dto.setActive(active);
+
+ viewModel.saveCustomer(dto).observe(getViewLifecycleOwner(), resource -> {
+ if (resource != null) {
+ setLoading(resource.status == Resource.Status.LOADING);
+ if (resource.status == Resource.Status.SUCCESS) {
+ Toast.makeText(getContext(),
+ viewModel.isEditing() ? "Updated successfully" : "Customer created",
+ Toast.LENGTH_SHORT).show();
+ navigateBack();
+ } else if (resource.status == Resource.Status.ERROR) {
+ Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show();
+ }
+ }
+ });
+ }
+
+ private void confirmDelete() {
+ DialogUtils.showDeleteConfirmDialog(requireContext(), "Customer", () ->
+ viewModel.deleteCustomer().observe(getViewLifecycleOwner(), resource -> {
+ if (resource != null) {
+ setLoading(resource.status == Resource.Status.LOADING);
+ if (resource.status == Resource.Status.SUCCESS) {
+ navigateBack();
+ } else if (resource.status == Resource.Status.ERROR) {
+ Toast.makeText(getContext(), "Delete failed: " + resource.message,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ }));
+ }
+
+ private void navigateBack() {
+ NavHostFragment.findNavController(this).popBackStack();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ binding = null;
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java
index d56ba6a6..8b28cd1d 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java
@@ -4,6 +4,7 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
@@ -17,6 +18,7 @@ import android.widget.TextView;
import android.widget.Toast;
import com.example.petstoremobile.R;
+import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentPetDetailBinding;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PetDTO;
@@ -29,8 +31,11 @@ import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.PetDetailViewModel;
+import java.util.List;
import java.util.Locale;
+import javax.inject.Inject;
+
import dagger.hilt.android.AndroidEntryPoint;
/**
@@ -43,6 +48,8 @@ public class PetDetailFragment extends Fragment {
private PetDetailViewModel viewModel;
private boolean isUpdatingUI = false;
+ @Inject TokenManager tokenManager;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -81,8 +88,19 @@ public class PetDetailFragment extends Fragment {
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> {
PetDetailViewModel.ViewState state = viewModel.getViewState().getValue();
- Long selectedStoreId = state != null ? state.selectedStoreId : null;
+ boolean staffCreating = isStaff() && (state == null || !state.isEditing);
+ boolean storeEnabled = state == null || state.isStoreEnabled;
+ Long selectedStoreId = (staffCreating && storeEnabled) ? tokenManager.getPrimaryStoreId() : (state != null ? state.selectedStoreId : null);
updateStoreSpinnerSelection(selectedStoreId);
+ if (staffCreating && storeEnabled) UIUtils.setViewsEnabled(false, binding.spinnerStore);
+ });
+
+ viewModel.getSpeciesList().observe(getViewLifecycleOwner(), list -> {
+ PetDetailViewModel.ViewState state = viewModel.getViewState().getValue();
+ String selectedSpecies = state != null ? state.selectedSpecies : null;
+ SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPetSpecies, list,
+ DropdownDTO::getLabel, "-- Select Species --", null, DropdownDTO::getId);
+ SpinnerUtils.setSelectionByValue(binding.spinnerPetSpecies, selectedSpecies);
});
}
@@ -100,13 +118,16 @@ public class PetDetailFragment extends Fragment {
private void savePet() {
if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return;
- if (!InputValidator.isNotEmpty(binding.etPetSpecies, "Species")) return;
+ if (!InputValidator.isSpinnerSelected(binding.spinnerPetSpecies, "Species")) return;
if (!InputValidator.isNotEmpty(binding.etPetBreed, "Breed")) return;
if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return;
if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return;
String name = binding.etPetName.getText().toString().trim();
- String species = binding.etPetSpecies.getText().toString().trim();
+ List speciesOptions = viewModel.getSpeciesList().getValue();
+ String species = (speciesOptions != null && binding.spinnerPetSpecies.getSelectedItemPosition() > 0)
+ ? speciesOptions.get(binding.spinnerPetSpecies.getSelectedItemPosition() - 1).getLabel()
+ : "";
String breed = binding.etPetBreed.getText().toString().trim();
int age = Integer.parseInt(binding.etPetAge.getText().toString().trim());
double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim());
@@ -143,6 +164,20 @@ public class PetDetailFragment extends Fragment {
petDTO.setCustomerId(customerId);
petDTO.setStoreId(storeId);
+ boolean ownerChanged = !java.util.Objects.equals(viewModel.getOriginalCustomerId(), customerId);
+ if (!isStaff() && viewModel.isEditing() && viewModel.isOriginallyOwnedOrAdopted() && ownerChanged) {
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Change Owner")
+ .setMessage("Are you sure you want to change the owner of this pet?")
+ .setPositiveButton("Yes", (d, w) -> performSave(petDTO, name))
+ .setNegativeButton("Cancel", null)
+ .show();
+ } else {
+ performSave(petDTO, name);
+ }
+ }
+
+ private void performSave(PetDTO petDTO, String name) {
viewModel.savePet(petDTO).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
@@ -202,7 +237,6 @@ public class PetDetailFragment extends Fragment {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
PetDTO p = resource.data;
binding.etPetName.setText(p.getPetName());
- binding.etPetSpecies.setText(p.getPetSpecies());
binding.etPetBreed.setText(p.getPetBreed());
binding.etPetAge.setText(String.valueOf(p.getPetAge()));
if (p.getPetPrice() != null) {
@@ -241,6 +275,11 @@ public class PetDetailFragment extends Fragment {
private void setupSpinner() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, new String[]{});
+ SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPetSpecies, p -> {
+ if (isUpdatingUI) return;
+ viewModel.onSpeciesSelected(p);
+ });
+
binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView> parent, View view, int position, long id) {
@@ -290,7 +329,7 @@ public class PetDetailFragment extends Fragment {
binding.btnDeletePet.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE);
binding.btnSavePet.setText(state.saveButtonText);
- UIUtils.setViewsEnabled(state.isSpeciesEnabled, binding.etPetSpecies);
+ UIUtils.setViewsEnabled(state.isSpeciesEnabled, binding.spinnerPetSpecies);
UIUtils.setViewsEnabled(state.isBreedEnabled, binding.etPetBreed);
UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer);
UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore);
@@ -301,6 +340,13 @@ public class PetDetailFragment extends Fragment {
updateCustomerSpinnerSelection(state.selectedCustomerId);
updateStoreSpinnerSelection(state.selectedStoreId);
+ List species = viewModel.getSpeciesList().getValue();
+ if (species != null) {
+ SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPetSpecies, species,
+ DropdownDTO::getLabel, "-- Select Species --", null, DropdownDTO::getId);
+ SpinnerUtils.setSelectionByValue(binding.spinnerPetSpecies, state.selectedSpecies);
+ }
+
if (!state.isCustomerEnabled && binding.spinnerCustomer.getSelectedItemPosition() != 0) {
binding.spinnerCustomer.setSelection(0);
}
@@ -308,9 +354,22 @@ public class PetDetailFragment extends Fragment {
binding.spinnerStore.setSelection(0);
}
+ if (isStaff() && !state.isEditing && state.isStoreEnabled) {
+ updateStoreSpinnerSelection(tokenManager.getPrimaryStoreId());
+ UIUtils.setViewsEnabled(false, binding.spinnerStore);
+ }
+
+ if (isStaff() && state.isEditing && viewModel.isOriginallyOwnedOrAdopted()) {
+ UIUtils.setViewsEnabled(false, binding.spinnerCustomer, binding.spinnerPetStatus, binding.spinnerStore);
+ }
+
isUpdatingUI = false;
}
+ private boolean isStaff() {
+ return "STAFF".equalsIgnoreCase(tokenManager.getRole());
+ }
+
private void clearSpinnerError(Spinner spinner) {
View selectedView = spinner.getSelectedView();
if (selectedView instanceof TextView) {
diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java
index 1c216ba3..f8752a16 100644
--- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java
+++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java
@@ -17,6 +17,10 @@ import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils;
+import com.example.petstoremobile.api.auth.TokenManager;
+
+import javax.inject.Inject;
+
import dagger.hilt.android.AndroidEntryPoint;
import java.math.BigDecimal;
import java.util.*;
@@ -27,6 +31,8 @@ public class SaleDetailFragment extends Fragment {
private FragmentSaleDetailBinding binding;
private SaleDetailViewModel viewModel;
+ @Inject TokenManager tokenManager;
+
private final String[] PAYMENT_METHODS = { "Cash", "Card"};
@Override
@@ -42,11 +48,13 @@ public class SaleDetailFragment extends Fragment {
if (viewModel.isViewOnly()) {
binding.llAddItemRow.setVisibility(View.GONE);
+ binding.llCouponInput.setVisibility(View.GONE);
binding.btnSaveSale.setVisibility(View.GONE);
UIUtils.setViewsEnabled(false, binding.spinnerSaleStore, binding.spinnerSaleCustomer, binding.spinnerPaymentMethod);
} else {
loadData();
setupAddItem();
+ setupCoupon();
}
binding.btnSaleBack.setOnClickListener(v -> navigateBack());
@@ -56,10 +64,18 @@ public class SaleDetailFragment extends Fragment {
return binding.getRoot();
}
+ private boolean isStaff() {
+ return "STAFF".equalsIgnoreCase(tokenManager.getRole());
+ }
+
private void observeViewModel() {
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> {
+ Long selectedStoreId = isStaff() ? tokenManager.getPrimaryStoreId() : -1L;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, list,
- DropdownDTO::getLabel, "-- Select Store --", -1L, DropdownDTO::getId);
+ DropdownDTO::getLabel, "-- Select Store --", selectedStoreId, DropdownDTO::getId);
+ if (isStaff()) {
+ binding.spinnerSaleStore.setEnabled(false);
+ }
});
viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> {
@@ -185,13 +201,77 @@ public class SaleDetailFragment extends Fragment {
if (sale.getItems() != null) {
binding.llSaleItems.removeAllViews();
for (SaleDTO.SaleItemDTO item : sale.getItems()) {
- addItemRow(item.getProductName(), Math.abs(item.getQuantity()), item.getUnitPrice());
+ addItemRow(item.getProductName(), Math.abs(item.getQuantity()), item.getUnitPrice(), null);
}
}
}
});
}
+ private void setupCoupon() {
+ binding.btnApplyCoupon.setOnClickListener(v -> {
+ String code = binding.etCouponCode.getText().toString().trim();
+ if (code.isEmpty()) {
+ Toast.makeText(getContext(), "Enter a coupon code", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ setLoading(true);
+ viewModel.lookupCoupon(code).observe(getViewLifecycleOwner(), resource -> {
+ if (resource == null) return;
+ setLoading(resource.status == Resource.Status.LOADING);
+ if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
+ CouponDTO coupon = resource.data;
+ if (Boolean.FALSE.equals(coupon.getActive())) {
+ showCouponError("This coupon is no longer active.");
+ return;
+ }
+ if (coupon.getMinOrderAmount() != null &&
+ viewModel.calculateSubtotal().compareTo(coupon.getMinOrderAmount()) < 0) {
+ showCouponError("Minimum order of $" + coupon.getMinOrderAmount() + " required.");
+ return;
+ }
+ viewModel.setAppliedCoupon(coupon);
+ applyAppliedCouponUI(coupon);
+ updateTotal();
+ } else if (resource.status == Resource.Status.ERROR) {
+ showCouponError("Invalid coupon code.");
+ }
+ });
+ });
+
+ binding.btnClearCoupon.setOnClickListener(v -> {
+ viewModel.clearCoupon();
+ binding.etCouponCode.setText("");
+ binding.tvCouponInfo.setVisibility(View.GONE);
+ binding.btnClearCoupon.setVisibility(View.GONE);
+ binding.btnApplyCoupon.setVisibility(View.VISIBLE);
+ binding.etCouponCode.setEnabled(true);
+ binding.llCouponDiscount.setVisibility(View.GONE);
+ updateTotal();
+ });
+ }
+
+ private void applyAppliedCouponUI(CouponDTO coupon) {
+ String info;
+ if ("PERCENTAGE".equalsIgnoreCase(coupon.getDiscountType())) {
+ info = coupon.getDiscountValue().stripTrailingZeros().toPlainString() + "% off applied";
+ } else {
+ info = "$" + coupon.getDiscountValue() + " off applied";
+ }
+ binding.tvCouponInfo.setText(info);
+ binding.tvCouponInfo.setTextColor(0xFF4CAF50);
+ binding.tvCouponInfo.setVisibility(View.VISIBLE);
+ binding.btnClearCoupon.setVisibility(View.VISIBLE);
+ binding.btnApplyCoupon.setVisibility(View.GONE);
+ binding.etCouponCode.setEnabled(false);
+ }
+
+ private void showCouponError(String message) {
+ binding.tvCouponInfo.setText(message);
+ binding.tvCouponInfo.setTextColor(0xFFE53935);
+ binding.tvCouponInfo.setVisibility(View.VISIBLE);
+ }
+
private void setupAddItem() {
binding.btnAddItem.setOnClickListener(v -> {
if (!InputValidator.isSpinnerSelected(binding.spinnerSaleProduct, "Product")) return;
@@ -228,15 +308,16 @@ public class SaleDetailFragment extends Fragment {
break;
}
}
- addItemRow(name, item.getQuantity(), price);
+ addItemRow(name, item.getQuantity(), price, item.getProdId());
}
}
- private void addItemRow(String name, int qty, BigDecimal price) {
+ private void addItemRow(String name, int qty, BigDecimal price, Long prodId) {
if (getContext() == null) return;
LinearLayout row = new LinearLayout(getContext());
row.setOrientation(LinearLayout.HORIZONTAL);
row.setPadding(0, 8, 0, 8);
+ row.setGravity(android.view.Gravity.CENTER_VERTICAL);
TextView tvName = new TextView(getContext());
tvName.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f));
@@ -253,12 +334,34 @@ public class SaleDetailFragment extends Fragment {
row.addView(tvName);
row.addView(tvQty);
row.addView(tvPrice);
+
+ if (prodId != null) {
+ Button btnRemove = new Button(getContext());
+ btnRemove.setLayoutParams(new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
+ btnRemove.setText("✕");
+ btnRemove.setTextSize(12f);
+ btnRemove.setBackgroundTintList(android.content.res.ColorStateList.valueOf(0xFFE53935));
+ btnRemove.setTextColor(0xFFFFFFFF);
+ btnRemove.setPadding(16, 4, 16, 4);
+ btnRemove.setOnClickListener(v -> viewModel.removeFromCart(prodId));
+ row.addView(btnRemove);
+ }
+
binding.llSaleItems.addView(row);
}
private void updateTotal() {
- BigDecimal total = viewModel.calculateSubtotal();
- binding.tvSaleSubtotal.setText("$" + total);
+ BigDecimal subtotal = viewModel.calculateSubtotal();
+ BigDecimal discount = viewModel.calculateDiscount();
+ BigDecimal total = subtotal.subtract(discount);
+ binding.tvSaleSubtotal.setText("$" + subtotal);
+ if (discount.compareTo(BigDecimal.ZERO) > 0) {
+ binding.llCouponDiscount.setVisibility(View.VISIBLE);
+ binding.tvSaleCouponDiscount.setText("-$" + discount);
+ } else {
+ binding.llCouponDiscount.setVisibility(View.GONE);
+ }
binding.tvSaleDetailTotal.setText("Total: $" + total);
}
@@ -279,6 +382,7 @@ public class SaleDetailFragment extends Fragment {
}
SaleDTO dto = new SaleDTO(store.getId(), payment, viewModel.getCartItems().getValue(), false, null, customerId);
+ dto.setCouponId(viewModel.getAppliedCouponId());
viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource != null) {
diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java
index f09f85b4..8758357a 100644
--- a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java
+++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java
@@ -24,8 +24,8 @@ public class AdoptionRepository extends BaseRepository {
/**
* Retrieves a paginated list of all adoptions from the API.
*/
- public LiveData>> getAllAdoptions(int page, int size, String query, String status, Long storeId, String date, Long employeeId) {
- return executeCall(adoptionApi.getAllAdoptions(page, size, query, status, storeId, date, employeeId));
+ public LiveData>> getAllAdoptions(int page, int size, String query, String status, Long storeId, String date, Long employeeId, String sort) {
+ return executeCall(adoptionApi.getAllAdoptions(page, size, query, status, storeId, date, employeeId, sort));
}
/**
diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java
index 85083a25..6c2c4cf8 100644
--- a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java
+++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java
@@ -24,8 +24,8 @@ public class AppointmentRepository extends BaseRepository {
/**
* Retrieves a paginated list of all appointments from the API with filtering.
*/
- public LiveData>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date, Long employeeId) {
- return executeCall(appointmentApi.getAllAppointments(page, size, query, status, storeId, date, employeeId));
+ public LiveData>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date, Long employeeId, String sort) {
+ return executeCall(appointmentApi.getAllAppointments(page, size, query, status, storeId, date, employeeId, sort));
}
/**
diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java
index 338043be..f97d1ca0 100644
--- a/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java
+++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java
@@ -30,6 +30,10 @@ public class CouponRepository extends BaseRepository {
return executeCall(couponApi.getCouponById(id));
}
+ public LiveData> getCouponByCode(String code) {
+ return executeCall(couponApi.getCouponByCode(code));
+ }
+
public LiveData> createCoupon(CouponDTO coupon) {
return executeCall(couponApi.createCoupon(coupon));
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java
index 9fa9d2e6..65c9a79d 100644
--- a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java
+++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java
@@ -23,24 +23,27 @@ public class CustomerRepository extends BaseRepository {
this.customerApi = customerApi;
}
- /**
- * Retrieves a paginated list of all customers from the API.
- */
public LiveData>> getAllCustomers(int page, int size) {
return executeCall(customerApi.getAllCustomers(page, size));
}
- /**
- * Retrieves a specific customer by their ID.
- */
public LiveData> getCustomerById(Long id) {
return executeCall(customerApi.getCustomerById(id));
}
- /**
- * Retrieves a list of customer dropdowns from the API.
- */
+ public LiveData> createCustomer(CustomerDTO dto) {
+ return executeCall(customerApi.registerCustomer(dto));
+ }
+
+ public LiveData> updateCustomer(Long id, CustomerDTO dto) {
+ return executeCall(customerApi.updateCustomer(id, dto));
+ }
+
+ public LiveData> deleteCustomer(Long id) {
+ return executeCall(customerApi.deleteCustomer(id));
+ }
+
public LiveData>> getCustomerDropdowns() {
return executeCall(customerApi.getCustomerDropdowns());
}
-}
\ No newline at end of file
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java
index b1eda73f..a186cc7e 100644
--- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java
+++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java
@@ -54,6 +54,10 @@ public class PetRepository extends BaseRepository {
return executeCall(petApi.getPetDropdowns());
}
+ public LiveData>> getPetSpeciesDropdowns() {
+ return executeCall(petApi.getPetSpeciesDropdowns());
+ }
+
/**
* Retrieves available pets for a specific store.
*/
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java
index c15766f9..06b96595 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java
@@ -13,6 +13,8 @@ import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -71,18 +73,17 @@ public class AdoptionDetailViewModel extends ViewModel {
state.saveButtonText = isEditing ? "Save" : "Add";
state.isAdoptionIdVisible = isEditing;
state.isDeleteVisible = isEditing;
- state.isFeeEnabled = false; // fee is always read-only
+ state.isFeeEnabled = false;
if (!isEditing) {
state.isCustomerEnabled = true;
state.isStoreEnabled = true;
- state.isPetEnabled = false; // until customer selected
- state.isEmployeeEnabled = false; // until store selected
+ state.isPetEnabled = false;
+ state.isEmployeeEnabled = false;
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;
@@ -95,12 +96,12 @@ public class AdoptionDetailViewModel extends ViewModel {
public void loadInitialFormData(boolean isEditing) {
// Pets are loaded dynamically based on store selection; no pre-load needed.
- customerRepository.getCustomerDropdowns().observeForever(r -> {
+ observeOnce(customerRepository.getCustomerDropdowns(), r -> {
if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
customerList.setValue(r.data);
}
});
- storeRepository.getStoreDropdowns().observeForever(r -> {
+ observeOnce(storeRepository.getStoreDropdowns(), r -> {
if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
storeList.setValue(r.data);
}
@@ -155,7 +156,7 @@ public class AdoptionDetailViewModel extends ViewModel {
}
private void loadAvailablePetsByStore(Long storeId) {
- petRepository.getAvailablePetsByStore(storeId).observeForever(r -> {
+ observeOnce(petRepository.getAvailablePetsByStore(storeId), r -> {
if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
List dropdowns = new ArrayList<>();
for (com.example.petstoremobile.dtos.PetDTO pet : r.data.getContent()) {
@@ -167,7 +168,7 @@ public class AdoptionDetailViewModel extends ViewModel {
}
private void loadPetPrice(Long petId) {
- petRepository.getPetById(petId).observeForever(r -> {
+ observeOnce(petRepository.getPetById(petId), 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
@@ -185,7 +186,7 @@ public class AdoptionDetailViewModel extends ViewModel {
}
private void loadEmployeesForStore(Long storeId) {
- storeRepository.getStoreEmployees(storeId).observeForever(r -> {
+ observeOnce(storeRepository.getStoreEmployees(storeId), r -> {
if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
employeeList.setValue(r.data);
}
@@ -217,6 +218,10 @@ public class AdoptionDetailViewModel extends ViewModel {
s.isEmployeeEnabled = true;
s.isDateEnabled = true;
s.isStatusEnabled = true;
+ } else {
+ // Date cleared: disable everything except the date field so user can pick again
+ setAllEditableFieldsEnabled(s, false);
+ s.isDateEnabled = true;
}
});
}
@@ -240,7 +245,7 @@ public class AdoptionDetailViewModel extends ViewModel {
public LiveData> loadAdoption() {
MutableLiveData> result = new MutableLiveData<>();
- adoptionRepository.getAdoptionById(adoptionId).observeForever(resource -> {
+ observeOnce(adoptionRepository.getAdoptionById(adoptionId), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
AdoptionDTO a = resource.data;
String formattedStatus = DateTimeUtils.formatStatusFromBackend(
@@ -248,10 +253,10 @@ public class AdoptionDetailViewModel extends ViewModel {
String adoptionDate = a.getAdoptionDate() != null ? a.getAdoptionDate() : "";
updateViewState(state -> {
- state.selectedPetId = a.getPetId() != null ? a.getPetId() : -1;
- state.selectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1;
- state.selectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1;
- state.selectedEmployeeId = a.getEmployeeId() != null ? a.getEmployeeId() : -1;
+ state.selectedPetId = a.getPetId();
+ state.selectedCustomerId = a.getCustomerId();
+ state.selectedStoreId = a.getSourceStoreId();
+ state.selectedEmployeeId = a.getEmployeeId();
state.adoptionDate = adoptionDate;
state.adoptionFee = a.getAdoptionFee() != null ? a.getAdoptionFee().toPlainString() : "";
state.selectedStatus = formattedStatus;
@@ -321,6 +326,21 @@ public class AdoptionDetailViewModel extends ViewModel {
void run(T target);
}
+ /**
+ * Observes a LiveData once, removing the observer after the first response.
+ * */
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public static class ViewState {
public boolean isEditing = false;
public boolean isAdoptionIdVisible = false;
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java
index 7c0b72b8..ec3ecfab 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java
@@ -11,6 +11,8 @@ import com.example.petstoremobile.repositories.AdoptionRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -53,7 +55,7 @@ public class AdoptionListViewModel extends ViewModel {
if ("All Statuses".equals(status)) status = null;
isLoading.setValue(true);
- adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, date, employeeId).observeForever(resource -> {
+ observeOnce(adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, date, employeeId, "adoptionId,desc"), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue());
@@ -70,13 +72,25 @@ public class AdoptionListViewModel extends ViewModel {
}
public void loadStores() {
- storeRepository.getAllStores(0, 100).observeForever(resource -> {
+ observeOnce(storeRepository.getAllStores(0, 100), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public LiveData> bulkDeleteAdoptions(List ids) {
return adoptionRepository.bulkDeleteAdoptions(new BulkDeleteRequest(ids));
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java
index 8d43d995..3e5082ec 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java
@@ -8,6 +8,8 @@ import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.repositories.SaleRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
@@ -49,7 +51,7 @@ public class AnalyticsViewModel extends ViewModel {
public void loadAnalytics() {
isLoading.setValue(true);
errorMessage.setValue(null);
- saleRepository.getAllSales(0, 2000, null, null, null, null, "saleDate,desc").observeForever(resource -> {
+ observeOnce(saleRepository.getAllSales(0, 2000, null, null, null, null, "saleDate,desc"), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
cachedSales = resource.data.getContent();
@@ -247,6 +249,18 @@ public class AnalyticsViewModel extends ViewModel {
return "Daily Revenue (" + s + " – " + e + ")";
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public static class FilterState {
public String startDate = "";
public String endDate = "";
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java
index 18c20e24..b229c3a0 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java
@@ -15,6 +15,8 @@ import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -42,6 +44,7 @@ public class AppointmentDetailViewModel extends ViewModel {
private long appointmentId = -1;
private boolean isOriginallyCancel = false;
+ private boolean isOriginallyCompletedOrMissed = false;
private Long currentCustomerId;
private Long currentStoreId;
private Long currentPetId;
@@ -71,14 +74,14 @@ public class AppointmentDetailViewModel extends ViewModel {
* Loads initial dropdown data for customers, stores, and services.
*/
public void loadInitialFormData() {
- customerRepository.getCustomerDropdowns().observeForever(r -> {
- if (r.status == Resource.Status.SUCCESS) customers.setValue(r.data);
+ observeOnce(customerRepository.getCustomerDropdowns(), r -> {
+ if (r != null && r.status == Resource.Status.SUCCESS) customers.setValue(r.data);
});
- storeRepository.getStoreDropdowns().observeForever(r -> {
- if (r.status == Resource.Status.SUCCESS) stores.setValue(r.data);
+ observeOnce(storeRepository.getStoreDropdowns(), r -> {
+ if (r != null && r.status == Resource.Status.SUCCESS) stores.setValue(r.data);
});
- serviceRepository.getAllServices(0, 200, null, "serviceName").observeForever(r -> {
- if (r.status == Resource.Status.SUCCESS && r.data != null) services.setValue(r.data.getContent());
+ observeOnce(serviceRepository.getAllServices(0, 200, null, "serviceName"), r -> {
+ if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) services.setValue(r.data.getContent());
});
}
@@ -206,8 +209,8 @@ public class AppointmentDetailViewModel extends ViewModel {
* Loads the list of pets for a specific customer.
*/
private void loadPetsForCustomer(Long customerId) {
- petRepository.getCustomerPets(customerId).observeForever(r -> {
- if (r.status == Resource.Status.SUCCESS) customerPets.setValue(r.data);
+ observeOnce(petRepository.getCustomerPets(customerId), r -> {
+ if (r != null && r.status == Resource.Status.SUCCESS) customerPets.setValue(r.data);
});
}
@@ -215,8 +218,8 @@ public class AppointmentDetailViewModel extends ViewModel {
* Loads the list of employees for a specific store.
*/
private void loadEmployeesForStore(Long storeId) {
- storeRepository.getStoreEmployees(storeId).observeForever(r -> {
- if (r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data);
+ observeOnce(storeRepository.getStoreEmployees(storeId), r -> {
+ if (r != null && r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data);
});
}
@@ -227,10 +230,12 @@ public class AppointmentDetailViewModel extends ViewModel {
*/
public LiveData> loadAppointment() {
MutableLiveData> result = new MutableLiveData<>();
- repository.getAppointmentById(appointmentId).observeForever(resource -> {
- if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
+ observeOnce(repository.getAppointmentById(appointmentId), resource -> {
+ if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
AppointmentDTO a = resource.data;
isOriginallyCancel = "CANCELLED".equalsIgnoreCase(a.getAppointmentStatus());
+ isOriginallyCompletedOrMissed = "COMPLETED".equalsIgnoreCase(a.getAppointmentStatus())
+ || "MISSED".equalsIgnoreCase(a.getAppointmentStatus());
currentCustomerId = a.getCustomerId();
currentStoreId = a.getStoreId();
currentPetId = a.getPetId();
@@ -282,21 +287,18 @@ public class AppointmentDetailViewModel extends ViewModel {
public void onDateOrTimeChanged(String date, String time, String currentStatus) {
updateViewState(s -> {
s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus);
- // Keep selectedStatus if still valid; prefer explicit currentStatus from UI if valid
java.util.List available = java.util.Arrays.asList(s.availableStatuses);
if (!currentStatus.isEmpty() && available.contains(currentStatus)) {
s.selectedStatus = currentStatus;
} else if (!available.contains(s.selectedStatus) && s.availableStatuses.length > 0) {
s.selectedStatus = s.availableStatuses[0];
}
- boolean isPast = DateTimeUtils.isDateTimeInPast(date, time);
-
if (isOriginallyCancel) {
s.isPast = true;
setAllFieldsEnabled(s, false);
s.isStatusEnabled = false;
s.isSaveVisible = false;
- } else if (isPast) {
+ } else if (isOriginallyCompletedOrMissed) {
s.isPast = true;
setAllFieldsEnabled(s, false);
s.isStatusEnabled = true;
@@ -322,7 +324,7 @@ public class AppointmentDetailViewModel extends ViewModel {
if (!isEditing) return new String[]{"Booked"};
if (date == null || date.isEmpty()) return new String[]{};
if (isOriginallyCancel) return new String[]{"Cancelled"};
- if (DateTimeUtils.isDateTimeInPast(date, currentTime)) return new String[]{"Completed", "Missed"};
+ if (isOriginallyCompletedOrMissed) return new String[]{"Completed", "Missed"};
return new String[]{"Booked", "Cancelled"};
}
@@ -355,8 +357,8 @@ public class AppointmentDetailViewModel extends ViewModel {
s.isCustomerEnabled = true;
s.isStoreEnabled = true;
s.isServiceEnabled = true;
- s.isPetEnabled = false; // until customer selected
- s.isStaffEnabled = false; // until store selected
+ s.isPetEnabled = false;
+ s.isStaffEnabled = false;
s.availableStatuses = new String[]{"Booked"};
s.selectedStatus = "Booked";
}
@@ -385,6 +387,19 @@ public class AppointmentDetailViewModel extends ViewModel {
void run(T t);
}
+ /** Observes a LiveData once, removing the observer after the first non-loading response. */
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
/**
* A Class to show the states of Appointment Detail Fragment.
*/
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java
index 8bdaf699..88997684 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java
@@ -11,6 +11,8 @@ import com.example.petstoremobile.repositories.AppointmentRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -39,7 +41,7 @@ public class AppointmentListViewModel extends ViewModel {
public void loadAppointments(String query, String status, Long storeId, String date, Long employeeId) {
isLoading.setValue(true);
- appointmentRepository.getAllAppointments(0, 500, query, status, storeId, date, employeeId).observeForever(resource -> {
+ observeOnce(appointmentRepository.getAllAppointments(0, 500, query, status, storeId, date, employeeId, "appointmentId,desc"), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
appointments.setValue(resource.data.getContent());
@@ -52,13 +54,25 @@ public class AppointmentListViewModel extends ViewModel {
}
public void loadStores() {
- storeRepository.getAllStores(0, 100).observeForever(resource -> {
+ observeOnce(storeRepository.getAllStores(0, 100), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public LiveData> bulkDeleteAppointments(List ids) {
return appointmentRepository.bulkDeleteAppointments(new BulkDeleteRequest(ids));
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java
index ad27ae6c..e1ea12f8 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java
@@ -9,6 +9,8 @@ import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.repositories.CouponRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -32,7 +34,7 @@ public class CouponListViewModel extends ViewModel {
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 -> {
+ observeOnce(repository.getAllCoupons(page, size, active, discountType, sort), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
coupons.setValue(resource.data.getContent());
@@ -44,6 +46,18 @@ public class CouponListViewModel extends ViewModel {
});
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public LiveData> bulkDeleteCoupons(List ids) {
return repository.bulkDeleteCoupons(ids);
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java
new file mode 100644
index 00000000..e7864b66
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java
@@ -0,0 +1,49 @@
+package com.example.petstoremobile.viewmodels;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.example.petstoremobile.dtos.CustomerDTO;
+import com.example.petstoremobile.repositories.CustomerRepository;
+import com.example.petstoremobile.utils.Resource;
+
+import javax.inject.Inject;
+
+import dagger.hilt.android.lifecycle.HiltViewModel;
+
+@HiltViewModel
+public class CustomerDetailViewModel extends ViewModel {
+ private final CustomerRepository repository;
+
+ private long customerId = -1;
+ private boolean isEditing = false;
+
+ @Inject
+ public CustomerDetailViewModel(CustomerRepository repository) {
+ this.repository = repository;
+ }
+
+ public void setCustomerId(long id, boolean isEditing) {
+ this.customerId = id;
+ this.isEditing = isEditing;
+ }
+
+ public long getCustomerId() { return customerId; }
+ public boolean isEditing() { return isEditing; }
+
+ public LiveData> loadCustomer(long id) {
+ return repository.getCustomerById(id);
+ }
+
+ public LiveData> saveCustomer(CustomerDTO dto) {
+ if (isEditing && customerId > 0) {
+ return repository.updateCustomer(customerId, dto);
+ } else {
+ return repository.createCustomer(dto);
+ }
+ }
+
+ public LiveData> deleteCustomer() {
+ return repository.deleteCustomer(customerId);
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java
new file mode 100644
index 00000000..1d7c18ba
--- /dev/null
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java
@@ -0,0 +1,93 @@
+package com.example.petstoremobile.viewmodels;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.example.petstoremobile.dtos.CustomerDTO;
+import com.example.petstoremobile.repositories.CustomerRepository;
+import com.example.petstoremobile.utils.Resource;
+
+import androidx.lifecycle.Observer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import dagger.hilt.android.lifecycle.HiltViewModel;
+
+@HiltViewModel
+public class CustomerListViewModel extends ViewModel {
+ private final CustomerRepository repository;
+
+ private final MutableLiveData> customers = new MutableLiveData<>(new ArrayList<>());
+ private final MutableLiveData> filteredCustomers = new MutableLiveData<>(new ArrayList<>());
+ private final MutableLiveData isLoading = new MutableLiveData<>(false);
+
+ private String lastQuery = "";
+ private String lastStatus = "All Statuses";
+
+ @Inject
+ public CustomerListViewModel(CustomerRepository repository) {
+ this.repository = repository;
+ }
+
+ public LiveData> getFilteredCustomers() { return filteredCustomers; }
+ public LiveData getIsLoading() { return isLoading; }
+
+ public void loadCustomers() {
+ isLoading.setValue(true);
+ observeOnce(repository.getAllCustomers(0, 200), resource -> {
+ if (resource != null) {
+ if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
+ customers.setValue(resource.data.getContent());
+ filter(lastQuery, lastStatus);
+ isLoading.setValue(false);
+ } else if (resource.status == Resource.Status.ERROR) {
+ isLoading.setValue(false);
+ }
+ }
+ });
+ }
+
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
+ public void filter(String query, String status) {
+ this.lastQuery = query;
+ this.lastStatus = status;
+
+ List all = customers.getValue();
+ if (all == null) return;
+
+ List filtered = new ArrayList<>();
+ String lowerQuery = query.toLowerCase();
+
+ for (CustomerDTO c : all) {
+ boolean matchesQuery = query.isEmpty() ||
+ (c.getFullName() != null && c.getFullName().toLowerCase().contains(lowerQuery)) ||
+ (c.getUsername() != null && c.getUsername().toLowerCase().contains(lowerQuery)) ||
+ (c.getEmail() != null && c.getEmail().toLowerCase().contains(lowerQuery)) ||
+ (c.getPhone() != null && c.getPhone().toLowerCase().contains(lowerQuery));
+
+ boolean matchesStatus = status.equals("All Statuses") ||
+ (status.equals("Active") && Boolean.TRUE.equals(c.getActive())) ||
+ (status.equals("Inactive") && Boolean.FALSE.equals(c.getActive()));
+
+ if (matchesQuery && matchesStatus) {
+ filtered.add(c);
+ }
+ }
+ filteredCustomers.setValue(filtered);
+ }
+}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java
index c91f8337..66387983 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java
@@ -11,6 +11,8 @@ import com.example.petstoremobile.repositories.InventoryRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -51,7 +53,7 @@ public class InventoryListViewModel extends ViewModel {
}
isLoading.setValue(true);
- inventoryRepository.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observeForever(resource -> {
+ observeOnce(inventoryRepository.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName"), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List currentList = reset ? new ArrayList<>() : new ArrayList<>(inventory.getValue());
@@ -68,13 +70,25 @@ public class InventoryListViewModel extends ViewModel {
}
public void loadStores() {
- storeRepository.getAllStores(0, 100).observeForever(resource -> {
+ observeOnce(storeRepository.getAllStores(0, 100), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public LiveData> bulkDeleteInventory(List ids) {
return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids));
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java
index 44f2680a..2f302b42 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java
@@ -11,6 +11,8 @@ import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -30,12 +32,16 @@ public class PetDetailViewModel extends ViewModel {
private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>());
+ private final MutableLiveData> speciesList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData isLoading = new MutableLiveData<>(false);
private final MutableLiveData viewState = new MutableLiveData<>(new ViewState());
private long petId = -1;
private Long selectedCustomerId = null;
private Long selectedStoreId = null;
+ private String selectedSpecies = null;
+ private boolean isOriginallyOwnedOrAdopted = false;
+ private Long originalCustomerId = null;
@Inject
public PetDetailViewModel(PetRepository petRepository, CustomerRepository customerRepository, StoreRepository storeRepository) {
@@ -45,17 +51,23 @@ public class PetDetailViewModel extends ViewModel {
}
public void loadInitialFormData() {
- customerRepository.getCustomerDropdowns().observeForever(resource -> {
+ observeOnce(customerRepository.getCustomerDropdowns(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
customerList.setValue(resource.data);
}
});
- storeRepository.getStoreDropdowns().observeForever(resource -> {
+ observeOnce(storeRepository.getStoreDropdowns(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList.setValue(resource.data);
}
});
+
+ observeOnce(petRepository.getPetSpeciesDropdowns(), resource -> {
+ if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
+ speciesList.setValue(resource.data);
+ }
+ });
}
public void setPetId(long id) {
@@ -72,6 +84,14 @@ public class PetDetailViewModel extends ViewModel {
return current != null && current.isEditing;
}
+ public boolean isOriginallyOwnedOrAdopted() {
+ return isOriginallyOwnedOrAdopted;
+ }
+
+ public Long getOriginalCustomerId() {
+ return originalCustomerId;
+ }
+
public LiveData getViewState() {
return viewState;
}
@@ -87,6 +107,16 @@ public class PetDetailViewModel extends ViewModel {
updateViewState(state -> state.selectedCustomerId = selectedCustomerId);
}
+ public void onSpeciesSelected(int position) {
+ List list = speciesList.getValue();
+ if (position > 0 && list != null && position <= list.size()) {
+ selectedSpecies = list.get(position - 1).getLabel();
+ } else {
+ selectedSpecies = null;
+ }
+ updateViewState(state -> state.selectedSpecies = selectedSpecies);
+ }
+
public void onStoreSelected(int position) {
List list = storeList.getValue();
if (position > 0 && list != null && position <= list.size()) {
@@ -123,8 +153,10 @@ public class PetDetailViewModel extends ViewModel {
if (!isEditing) {
selectedCustomerId = null;
selectedStoreId = null;
+ selectedSpecies = null;
state.selectedCustomerId = null;
state.selectedStoreId = null;
+ state.selectedSpecies = null;
state.selectedStatus = STATUS_AVAILABLE;
state.isCustomerEnabled = false;
state.isStoreEnabled = true;
@@ -134,15 +166,20 @@ public class PetDetailViewModel extends ViewModel {
public LiveData> loadPet() {
MutableLiveData> result = new MutableLiveData<>();
- petRepository.getPetById(petId).observeForever(resource -> {
+ observeOnce(petRepository.getPetById(petId), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
PetDTO pet = resource.data;
selectedCustomerId = pet.getCustomerId();
selectedStoreId = pet.getStoreId();
+ selectedSpecies = pet.getPetSpecies();
+ isOriginallyOwnedOrAdopted = STATUS_OWNED.equalsIgnoreCase(pet.getPetStatus())
+ || STATUS_ADOPTED.equalsIgnoreCase(pet.getPetStatus());
+ originalCustomerId = pet.getCustomerId();
updateViewState(state -> {
state.selectedCustomerId = selectedCustomerId;
state.selectedStoreId = selectedStoreId;
+ state.selectedSpecies = selectedSpecies;
state.selectedStatus = normalizeStatus(pet.getPetStatus());
applyStatusRules(state, false);
});
@@ -174,6 +211,10 @@ public class PetDetailViewModel extends ViewModel {
return storeList;
}
+ public LiveData> getSpeciesList() {
+ return speciesList;
+ }
+
public LiveData getIsLoading() {
return isLoading;
}
@@ -227,6 +268,17 @@ public class PetDetailViewModel extends ViewModel {
void run(T target);
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
public static class ViewState {
public boolean isEditing = false;
@@ -240,6 +292,7 @@ public class PetDetailViewModel extends ViewModel {
public String saveButtonText = "Add";
public String[] availableStatuses = new String[]{STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED};
public String selectedStatus = STATUS_AVAILABLE;
+ public String selectedSpecies = null;
public Long selectedCustomerId = null;
public Long selectedStoreId = null;
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java
index 8a567450..af6770f8 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java
@@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
+import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.dtos.StoreDTO;
@@ -12,6 +13,8 @@ import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -26,6 +29,7 @@ public class PetListViewModel extends ViewModel {
private final MutableLiveData> pets = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>());
+ private final MutableLiveData> speciesOptions = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData isLoading = new MutableLiveData<>(false);
@Inject
@@ -36,6 +40,7 @@ public class PetListViewModel extends ViewModel {
public LiveData> getPets() { return pets; }
public LiveData> getStores() { return stores; }
+ public LiveData> getSpeciesOptions() { return speciesOptions; }
public LiveData getIsLoading() { return isLoading; }
public void loadPets(String query, String status, String species, Long storeId) {
@@ -43,7 +48,7 @@ public class PetListViewModel extends ViewModel {
if ("All Species".equals(species)) species = null;
isLoading.setValue(true);
- petRepository.getAllPets(0, 100, query, status, species, storeId, null, "petName").observeForever(resource -> {
+ observeOnce(petRepository.getAllPets(0, 100, query, status, species, storeId, null, "petName"), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
pets.setValue(resource.data.getContent());
@@ -55,14 +60,39 @@ public class PetListViewModel extends ViewModel {
});
}
+ public void loadSpecies() {
+ observeOnce(petRepository.getPetSpeciesDropdowns(), resource -> {
+ if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
+ List labels = new ArrayList<>();
+ labels.add("All Species");
+ for (DropdownDTO dto : resource.data) {
+ labels.add(dto.getLabel());
+ }
+ speciesOptions.setValue(labels);
+ }
+ });
+ }
+
public void loadStores() {
- storeRepository.getAllStores(0, 100).observeForever(resource -> {
+ observeOnce(storeRepository.getAllStores(0, 100), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public LiveData> bulkDeletePets(List ids) {
return petRepository.bulkDeletePets(new BulkDeleteRequest(ids));
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java
index 6d89ab6f..503bf8b6 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java
@@ -10,6 +10,8 @@ import com.example.petstoremobile.repositories.CategoryRepository;
import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -38,7 +40,7 @@ public class ProductListViewModel extends ViewModel {
public void loadProducts(String query, Long categoryId) {
isLoading.setValue(true);
- productRepository.getAllProducts(query, categoryId, 0, 100, "prodName").observeForever(resource -> {
+ observeOnce(productRepository.getAllProducts(query, categoryId, 0, 100, "prodName"), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
products.setValue(resource.data.getContent());
@@ -51,10 +53,22 @@ public class ProductListViewModel extends ViewModel {
}
public void loadCategories() {
- productRepository.getCategoryDropdowns().observeForever(resource -> {
+ observeOnce(productRepository.getCategoryDropdowns(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
categories.setValue(resource.data);
}
});
}
+
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java
index cad846c4..25f7b8a1 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java
@@ -13,6 +13,8 @@ import com.example.petstoremobile.repositories.ProductSupplierRepository;
import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -45,7 +47,7 @@ public class ProductSupplierListViewModel extends ViewModel {
public void loadProductSuppliers(String query, Long productId, Long supplierId) {
isLoading.setValue(true);
- psRepository.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observeForever(resource -> {
+ observeOnce(psRepository.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName"), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productSuppliers.setValue(resource.data.getContent());
@@ -58,19 +60,31 @@ public class ProductSupplierListViewModel extends ViewModel {
}
public void loadFilterData() {
- productRepository.getAllProducts(null, null, 0, 100, "prodName").observeForever(resource -> {
+ observeOnce(productRepository.getAllProducts(null, null, 0, 100, "prodName"), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
products.setValue(resource.data.getContent());
}
});
- supplierRepository.getAllSuppliers(0, 100, null, "supCompany").observeForever(resource -> {
+ observeOnce(supplierRepository.getAllSuppliers(0, 100, null, "supCompany"), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
suppliers.setValue(resource.data.getContent());
}
});
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public LiveData> bulkDeleteProductSuppliers(List ids) {
return psRepository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids));
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java
index 438f4198..296e4792 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java
@@ -10,6 +10,8 @@ import com.example.petstoremobile.repositories.PurchaseOrderRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -38,7 +40,7 @@ public class PurchaseOrderListViewModel extends ViewModel {
public void loadPurchaseOrders(String query, Long storeId) {
isLoading.setValue(true);
- purchaseOrderRepository.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observeForever(resource -> {
+ observeOnce(purchaseOrderRepository.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc"), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
purchaseOrders.setValue(resource.data.getContent());
@@ -51,10 +53,22 @@ public class PurchaseOrderListViewModel extends ViewModel {
}
public void loadStores() {
- storeRepository.getAllStores(0, 100).observeForever(resource -> {
+ observeOnce(storeRepository.getAllStores(0, 100), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
+
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java
index 201fae9e..658264c9 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java
@@ -4,9 +4,11 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
+import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.SaleDTO;
+import com.example.petstoremobile.repositories.CouponRepository;
import com.example.petstoremobile.repositories.CustomerRepository;
import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.repositories.SaleRepository;
@@ -27,6 +29,7 @@ public class SaleDetailViewModel extends ViewModel {
private final StoreRepository storeRepository;
private final CustomerRepository customerRepository;
private final ProductRepository productRepository;
+ private final CouponRepository couponRepository;
private long saleId = -1;
private boolean viewOnly = false;
@@ -35,15 +38,18 @@ public class SaleDetailViewModel extends ViewModel {
private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData> cartItems = new MutableLiveData<>(new ArrayList<>());
+ private final MutableLiveData appliedCoupon = new MutableLiveData<>(null);
private final MutableLiveData isLoading = new MutableLiveData<>(false);
@Inject
public SaleDetailViewModel(SaleRepository saleRepository, StoreRepository storeRepository,
- CustomerRepository customerRepository, ProductRepository productRepository) {
+ CustomerRepository customerRepository, ProductRepository productRepository,
+ CouponRepository couponRepository) {
this.saleRepository = saleRepository;
this.storeRepository = storeRepository;
this.customerRepository = customerRepository;
this.productRepository = productRepository;
+ this.couponRepository = couponRepository;
}
public void setSaleId(long id, boolean viewOnly) {
@@ -89,8 +95,46 @@ public class SaleDetailViewModel extends ViewModel {
cartItems.setValue(currentCart);
}
+ public void removeFromCart(Long prodId) {
+ List currentCart = new ArrayList<>(cartItems.getValue());
+ currentCart.removeIf(item -> item.getProdId().equals(prodId));
+ cartItems.setValue(currentCart);
+ }
+
public LiveData> getCartItems() { return cartItems; }
+ public LiveData> lookupCoupon(String code) {
+ return couponRepository.getCouponByCode(code);
+ }
+
+ public void setAppliedCoupon(CouponDTO coupon) {
+ appliedCoupon.setValue(coupon);
+ }
+
+ public void clearCoupon() {
+ appliedCoupon.setValue(null);
+ }
+
+ public LiveData getAppliedCoupon() {
+ return appliedCoupon;
+ }
+
+ public Long getAppliedCouponId() {
+ CouponDTO coupon = appliedCoupon.getValue();
+ return coupon != null ? coupon.getCouponId() : null;
+ }
+
+ public BigDecimal calculateDiscount() {
+ CouponDTO coupon = appliedCoupon.getValue();
+ if (coupon == null || coupon.getDiscountValue() == null) return BigDecimal.ZERO;
+ BigDecimal subtotal = calculateSubtotal();
+ if ("PERCENTAGE".equalsIgnoreCase(coupon.getDiscountType())) {
+ return subtotal.multiply(coupon.getDiscountValue()).divide(BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP);
+ } else {
+ return coupon.getDiscountValue().min(subtotal);
+ }
+ }
+
public BigDecimal calculateSubtotal() {
BigDecimal total = BigDecimal.ZERO;
List items = cartItems.getValue();
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java
index 297d987f..7df7269a 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java
@@ -11,6 +11,8 @@ import com.example.petstoremobile.repositories.SaleRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -51,7 +53,7 @@ public class SaleListViewModel extends ViewModel {
}
isLoading.setValue(true);
- saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, isRefund, "saleDate,desc").observeForever(resource -> {
+ observeOnce(saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, isRefund, "saleDate,desc"), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue());
@@ -68,10 +70,22 @@ public class SaleListViewModel extends ViewModel {
}
public void loadStores() {
- storeRepository.getAllStores(0, 100).observeForever(resource -> {
+ observeOnce(storeRepository.getAllStores(0, 100), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
+
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java
index aa465a08..625abb92 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java
@@ -8,6 +8,8 @@ import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.repositories.ServiceRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@@ -55,7 +57,7 @@ public class ServiceDetailViewModel extends ViewModel {
public LiveData> loadService() {
MutableLiveData> result = new MutableLiveData<>();
- repository.getServiceById(serviceId).observeForever(resource -> {
+ observeOnce(repository.getServiceById(serviceId), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
ServiceDTO service = resource.data;
updateViewState(state -> {
@@ -106,6 +108,18 @@ public class ServiceDetailViewModel extends ViewModel {
void run(T target);
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public static class ViewState {
public boolean isEditing = false;
public boolean isDeleteVisible = false;
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java
index d0fa121b..bbdbe270 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java
@@ -10,6 +10,8 @@ import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.repositories.ServiceRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -46,7 +48,7 @@ public class ServiceListViewModel extends ViewModel {
}
isLoading.setValue(true);
- repository.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observeForever(resource -> {
+ observeOnce(repository.getAllServices(currentPage, PAGE_SIZE, query, "serviceName"), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List currentList = reset ? new ArrayList<>() : new ArrayList<>(services.getValue());
@@ -62,6 +64,18 @@ public class ServiceListViewModel extends ViewModel {
});
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public LiveData> bulkDeleteServices(List ids) {
return repository.bulkDeleteServices(new BulkDeleteRequest(ids));
}
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java
index d448d853..023d1aaf 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java
@@ -10,6 +10,8 @@ import com.example.petstoremobile.repositories.EmployeeRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -42,7 +44,7 @@ public class StaffListViewModel extends ViewModel {
public void loadStaff() {
isLoading.setValue(true);
- repository.getAllEmployees(0, 100).observeForever(resource -> {
+ observeOnce(repository.getAllEmployees(0, 100), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
employees.setValue(resource.data.getContent());
@@ -56,13 +58,25 @@ public class StaffListViewModel extends ViewModel {
}
public void loadStores() {
- storeRepository.getAllStores(0, 100).observeForever(resource -> {
+ observeOnce(storeRepository.getAllStores(0, 100), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent());
}
});
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public void filter(String query, Long storeId, String status) {
this.lastQuery = query;
this.lastStoreId = storeId;
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java
index 88078102..54f83ef9 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java
@@ -8,6 +8,8 @@ import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
@@ -55,7 +57,7 @@ public class SupplierDetailViewModel extends ViewModel {
public LiveData> loadSupplier() {
MutableLiveData> result = new MutableLiveData<>();
- repository.getSupplierById(supId).observeForever(resource -> {
+ observeOnce(repository.getSupplierById(supId), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
SupplierDTO s = resource.data;
updateViewState(state -> {
@@ -99,6 +101,18 @@ public class SupplierDetailViewModel extends ViewModel {
void run(T target);
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public static class ViewState {
public boolean isEditing = false;
public boolean isDeleteVisible = false;
diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java
index 072ad3bd..462030ea 100644
--- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java
+++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java
@@ -9,6 +9,8 @@ import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource;
+import androidx.lifecycle.Observer;
+
import java.util.ArrayList;
import java.util.List;
@@ -33,7 +35,7 @@ public class SupplierListViewModel extends ViewModel {
public void loadSuppliers(String query) {
isLoading.setValue(true);
- repository.getAllSuppliers(0, 100, query, "supCompany").observeForever(resource -> {
+ observeOnce(repository.getAllSuppliers(0, 100, query, "supCompany"), resource -> {
if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
suppliers.setValue(resource.data.getContent());
@@ -45,6 +47,18 @@ public class SupplierListViewModel extends ViewModel {
});
}
+ private void observeOnce(LiveData> liveData, Observer> handler) {
+ liveData.observeForever(new Observer>() {
+ @Override
+ public void onChanged(Resource resource) {
+ if (resource == null || resource.status != Resource.Status.LOADING) {
+ liveData.removeObserver(this);
+ }
+ handler.onChanged(resource);
+ }
+ });
+ }
+
public LiveData> bulkDeleteSuppliers(List ids) {
return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids));
}
diff --git a/android/app/src/main/res/layout/activity_forgot_password.xml b/android/app/src/main/res/layout/activity_forgot_password.xml
new file mode 100644
index 00000000..b63ae318
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_forgot_password.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml
index 35692e3c..4a2d8c88 100644
--- a/android/app/src/main/res/layout/activity_main.xml
+++ b/android/app/src/main/res/layout/activity_main.xml
@@ -109,6 +109,18 @@
android:textColor="@color/white"
android:textSize="16sp"/>
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/fragment_coupon_detail.xml b/android/app/src/main/res/layout/fragment_coupon_detail.xml
index 64bf1f8a..29a45ae4 100644
--- a/android/app/src/main/res/layout/fragment_coupon_detail.xml
+++ b/android/app/src/main/res/layout/fragment_coupon_detail.xml
@@ -60,6 +60,17 @@
android:padding="16dp"
android:layout_marginBottom="16dp">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/fragment_customer_detail.xml b/android/app/src/main/res/layout/fragment_customer_detail.xml
new file mode 100644
index 00000000..a2c63030
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_customer_detail.xml
@@ -0,0 +1,252 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/fragment_list.xml b/android/app/src/main/res/layout/fragment_list.xml
index ece43489..090a6a09 100644
--- a/android/app/src/main/res/layout/fragment_list.xml
+++ b/android/app/src/main/res/layout/fragment_list.xml
@@ -146,6 +146,23 @@
android:textSize="15sp"/>
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/fragment_pet_detail.xml b/android/app/src/main/res/layout/fragment_pet_detail.xml
index 4e7ac201..e9018362 100644
--- a/android/app/src/main/res/layout/fragment_pet_detail.xml
+++ b/android/app/src/main/res/layout/fragment_pet_detail.xml
@@ -95,14 +95,11 @@
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
-
+ android:layout_marginBottom="16dp"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/navigation/list_nav_graph.xml b/android/app/src/main/res/navigation/list_nav_graph.xml
index 47d62352..70c651e1 100644
--- a/android/app/src/main/res/navigation/list_nav_graph.xml
+++ b/android/app/src/main/res/navigation/list_nav_graph.xml
@@ -151,6 +151,18 @@
android:label="Staff Details"
tools:layout="@layout/fragment_staff_detail" />
+
+
+
+
new ResourceNotFoundException("Coupon not found with id: " + request.getCouponId()));
+ validateCoupon(coupon);
sale.setCoupon(coupon);
}
@@ -85,8 +89,9 @@ public class SaleService {
sale.setCart(cart);
}
+ User customer = null;
if (request.getCustomerId() != null) {
- User customer = userRepository.findById(request.getCustomerId())
+ customer = userRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
sale.setCustomer(customer);
}
@@ -97,7 +102,7 @@ public class SaleService {
sale.setOriginalSale(originalSale);
}
- BigDecimal totalAmount = BigDecimal.ZERO;
+ BigDecimal subtotalAmount = BigDecimal.ZERO;
List saleItems = new ArrayList<>();
if (sale.getIsRefund() && sale.getOriginalSale() != null) {
@@ -145,9 +150,11 @@ public class SaleService {
saleItem.setUnitPrice(unitPrice);
saleItems.add(saleItem);
- totalAmount = totalAmount.add(itemTotal);
+ subtotalAmount = subtotalAmount.add(itemTotal);
}
- totalAmount = totalAmount.negate();
+ subtotalAmount = subtotalAmount.negate();
+ sale.setSubtotalAmount(subtotalAmount);
+ sale.setTotalAmount(subtotalAmount);
} else {
for (var itemRequest : request.getItems()) {
Product product = productRepository.findById(itemRequest.getProdId())
@@ -174,17 +181,77 @@ public class SaleService {
saleItem.setUnitPrice(unitPrice);
saleItems.add(saleItem);
- totalAmount = totalAmount.add(itemTotal);
+ subtotalAmount = subtotalAmount.add(itemTotal);
+ }
+ sale.setSubtotalAmount(subtotalAmount);
+
+ BigDecimal couponDiscount = calculateCouponDiscount(sale.getCoupon(), subtotalAmount);
+ sale.setCouponDiscountAmount(couponDiscount);
+
+ BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount));
+ sale.setEmployeeDiscountAmount(employeeDiscount);
+
+ BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount);
+ sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO));
+
+ sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue());
+ if (customer != null) {
+ customer.setLoyaltyPoints(customer.getLoyaltyPoints() + sale.getPointsEarned());
+ userRepository.save(customer);
}
}
- sale.setTotalAmount(totalAmount);
sale.setItems(saleItems);
Sale savedSale = saleRepository.save(sale);
return mapToResponse(savedSale);
}
+ private void validateCoupon(Coupon coupon) {
+ if (!Boolean.TRUE.equals(coupon.getActive())) {
+ throw new BusinessException("Coupon is not active");
+ }
+ LocalDateTime now = LocalDateTime.now();
+ if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) {
+ throw new BusinessException("Coupon has not started yet");
+ }
+ if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) {
+ throw new BusinessException("Coupon has expired");
+ }
+ }
+
+ private BigDecimal calculateCouponDiscount(Coupon coupon, BigDecimal subtotal) {
+ if (coupon == null || subtotal.compareTo(BigDecimal.ZERO) <= 0) {
+ return BigDecimal.ZERO;
+ }
+
+ if (coupon.getMinOrderAmount() != null && subtotal.compareTo(coupon.getMinOrderAmount()) < 0) {
+ return BigDecimal.ZERO;
+ }
+
+ BigDecimal discount = BigDecimal.ZERO;
+ String type = coupon.getDiscountType().trim().toUpperCase();
+ if ("PERCENTAGE".equals(type) || "PERCENT".equals(type)) {
+ discount = subtotal.multiply(coupon.getDiscountValue().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP));
+ } else if ("FIXED".equals(type)) {
+ discount = coupon.getDiscountValue();
+ }
+
+ return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) {
+ if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) {
+ return BigDecimal.ZERO;
+ }
+
+ if (customer.getRole() == User.Role.STAFF || customer.getRole() == User.Role.ADMIN) {
+ return remainingAmount.multiply(EMPLOYEE_DISCOUNT_PERCENT).setScale(2, RoundingMode.HALF_UP);
+ }
+
+ return BigDecimal.ZERO;
+ }
+
private SaleResponse mapToResponse(Sale sale) {
SaleResponse response = new SaleResponse();
response.setSaleId(sale.getSaleId());
@@ -197,6 +264,11 @@ public class SaleService {
response.setStoreName(sale.getStore().getStoreName());
}
+ if (sale.getCustomer() != null) {
+ response.setCustomerId(sale.getCustomer().getId());
+ response.setCustomerName(sale.getCustomer().getFirstName() + " " + sale.getCustomer().getLastName());
+ }
+
response.setTotalAmount(sale.getTotalAmount());
response.setSubtotalAmount(sale.getSubtotalAmount());
response.setCouponDiscountAmount(sale.getCouponDiscountAmount());