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 @@ + + + + + + + + + + + + + + + + + + + +