Merge branch 'AttachmentsToChat'

This commit is contained in:
Alex
2026-04-12 01:09:25 -06:00
64 changed files with 2303 additions and 193 deletions

View File

@@ -34,6 +34,9 @@
android:name=".activities.HomeActivity" android:name=".activities.HomeActivity"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:exported="false" /> android:exported="false" />
<activity
android:name=".activities.ForgotPasswordActivity"
android:exported="false" />
<activity <activity
android:name=".activities.MainActivity" android:name=".activities.MainActivity"
android:exported="true"> android:exported="true">

View File

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

View File

@@ -89,6 +89,11 @@ public class MainActivity extends AppCompatActivity {
performLogin(username, password); 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 != null && resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
tokenManager.saveUserId(resource.data.getId()); tokenManager.saveUserId(resource.data.getId());
tokenManager.savePrimaryStoreId(resource.data.getStoreId());
} }
Toast.makeText(this, "Login successful", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Login successful", Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, HomeActivity.class)); startActivity(new Intent(this, HomeActivity.class));

View File

@@ -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<CustomerAdapter.CustomerViewHolder> {
private List<CustomerDTO> list;
private OnCustomerClickListener listener;
private String baseUrl;
private String token;
public interface OnCustomerClickListener {
void onCustomerClick(int position);
}
public CustomerAdapter(List<CustomerDTO> 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(); }
}

View File

@@ -24,7 +24,8 @@ public interface AdoptionApi {
@Query("status") String status, @Query("status") String status,
@Query("storeId") Long storeId, @Query("storeId") Long storeId,
@Query("date") String date, @Query("date") String date,
@Query("employeeId") Long employeeId); @Query("employeeId") Long employeeId,
@Query("sort") String sort);
@GET("api/v1/adoptions/{id}") @GET("api/v1/adoptions/{id}")
Call<AdoptionDTO> getAdoptionById(@Path("id") Long id); Call<AdoptionDTO> getAdoptionById(@Path("id") Long id);

View File

@@ -24,7 +24,8 @@ public interface AppointmentApi {
@Query("status") String status, @Query("status") String status,
@Query("storeId") Long storeId, @Query("storeId") Long storeId,
@Query("date") String date, @Query("date") String date,
@Query("employeeId") Long employeeId); @Query("employeeId") Long employeeId,
@Query("sort") String sort);
@GET("api/v1/appointments/{id}") @GET("api/v1/appointments/{id}")
Call<AppointmentDTO> getAppointmentById(@Path("id") Long id); Call<AppointmentDTO> getAppointmentById(@Path("id") Long id);

View File

@@ -7,11 +7,14 @@ import com.example.petstoremobile.dtos.PageResponse;
import java.util.List; import java.util.List;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET; import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path; import retrofit2.http.Path;
import retrofit2.http.Query; import retrofit2.http.Query;
//api calls to get customers
public interface CustomerApi { public interface CustomerApi {
@GET("api/v1/customers") @GET("api/v1/customers")
@@ -20,6 +23,15 @@ public interface CustomerApi {
@GET("api/v1/customers/{customerId}") @GET("api/v1/customers/{customerId}")
Call<CustomerDTO> getCustomerById(@Path("customerId") Long customerId); Call<CustomerDTO> getCustomerById(@Path("customerId") Long customerId);
@PUT("api/v1/customers/{customerId}")
Call<CustomerDTO> updateCustomer(@Path("customerId") Long customerId, @Body CustomerDTO customer);
@DELETE("api/v1/customers/{customerId}")
Call<Void> deleteCustomer(@Path("customerId") Long customerId);
@POST("api/v1/auth/register")
Call<CustomerDTO> registerCustomer(@Body CustomerDTO customer);
@GET("api/v1/dropdowns/customers") @GET("api/v1/dropdowns/customers")
Call<List<DropdownDTO>> getCustomerDropdowns(); Call<List<DropdownDTO>> getCustomerDropdowns();
} }

View File

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

View File

@@ -15,6 +15,7 @@ public class TokenManager {
private static final String ROLE_KEY = "role"; private static final String ROLE_KEY = "role";
private static final String PREFS_NAME = "auth_prefs"; private static final String PREFS_NAME = "auth_prefs";
private static final String USER_ID_KEY = "user_id"; private static final String USER_ID_KEY = "user_id";
private static final String PRIMARY_STORE_ID_KEY = "primary_store_id";
private SharedPreferences prefs; private SharedPreferences prefs;
@@ -54,6 +55,19 @@ public class TokenManager {
prefs.edit().putLong(USER_ID_KEY, userId).apply(); 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 //Check if logged in
public boolean isLoggedIn() { public boolean isLoggedIn() {
return getToken() != null; return getToken() != null;

View File

@@ -5,61 +5,72 @@ import com.google.gson.annotations.SerializedName;
public class CustomerDTO { public class CustomerDTO {
@SerializedName("id") @SerializedName("id")
private Long customerId; private Long customerId;
private String username;
private String firstName; private String firstName;
private String lastName; private String lastName;
private String fullName;
private String email; private String email;
private String phone;
private Boolean active;
private Integer loyaltyPoints;
private Long primaryStoreId;
private String createdAt; private String createdAt;
private String updatedAt; private String updatedAt;
private String password;
public Long getCustomerId() { public CustomerDTO() {}
return customerId;
}
public void setCustomerId(Long customerId) { public CustomerDTO(String username, String password, String firstName, String lastName,
this.customerId = customerId; String email, String phone) {
} this.username = username;
this.password = password;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName; this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName; this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = 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() { 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() { public String getEmail() { return email; }
return createdAt; public void setEmail(String email) { this.email = email; }
}
public void setCreatedAt(String createdAt) { public String getPhone() { return phone; }
this.createdAt = createdAt; public void setPhone(String phone) { this.phone = phone; }
}
public String getUpdatedAt() { public Boolean getActive() { return active; }
return updatedAt; public void setActive(Boolean active) { this.active = active; }
}
public void setUpdatedAt(String updatedAt) { public Integer getLoyaltyPoints() { return loyaltyPoints; }
this.updatedAt = updatedAt; 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; }
} }

View File

@@ -102,6 +102,10 @@ public class SaleDTO {
return couponId; return couponId;
} }
public void setCouponId(Long couponId) {
this.couponId = couponId;
}
public Integer getPointsEarned() { public Integer getPointsEarned() {
return pointsEarned; return pointsEarned;
} }

View File

@@ -89,6 +89,7 @@ public class ListFragment extends Fragment {
binding.drawerPurchaseOrderView.setOnClickListener(v -> navigateTo(R.id.nav_purchase_order)); binding.drawerPurchaseOrderView.setOnClickListener(v -> navigateTo(R.id.nav_purchase_order));
binding.drawerSale.setOnClickListener(v -> navigateTo(R.id.nav_sale)); binding.drawerSale.setOnClickListener(v -> navigateTo(R.id.nav_sale));
binding.drawerStaff.setOnClickListener(v -> navigateTo(R.id.nav_staff)); binding.drawerStaff.setOnClickListener(v -> navigateTo(R.id.nav_staff));
binding.drawerCustomers.setOnClickListener(v -> navigateTo(R.id.nav_customer));
binding.drawerAnalytics.setOnClickListener(v -> navigateTo(R.id.nav_analytics)); binding.drawerAnalytics.setOnClickListener(v -> navigateTo(R.id.nav_analytics));
binding.drawerCoupons.setOnClickListener(v -> navigateTo(R.id.nav_coupon)); binding.drawerCoupons.setOnClickListener(v -> navigateTo(R.id.nav_coupon));

View File

@@ -160,7 +160,8 @@ public class ProfileFragment extends Fragment {
android.content.Intent serviceIntent = new android.content.Intent(requireContext(), ChatNotificationService.class); android.content.Intent serviceIntent = new android.content.Intent(requireContext(), ChatNotificationService.class);
requireContext().stopService(serviceIntent); 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 //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); 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); intent.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP | android.content.Intent.FLAG_ACTIVITY_NEW_TASK);

View File

@@ -17,6 +17,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.AdoptionAdapter; import com.example.petstoremobile.adapters.AdoptionAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentAdoptionBinding; import com.example.petstoremobile.databinding.FragmentAdoptionBinding;
import com.example.petstoremobile.dtos.AdoptionDTO; import com.example.petstoremobile.dtos.AdoptionDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
@@ -38,6 +39,8 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
@@ -52,6 +55,8 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
private boolean isMonthMode = false; private boolean isMonthMode = false;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
@Inject TokenManager tokenManager;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -117,7 +122,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
loadAdoptions(); loadAdoptions();
viewModel.loadStores(); if (!isStaff()) viewModel.loadStores();
} }
private void toggleCalendarMode() { private void toggleCalendarMode() {
@@ -128,8 +133,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
} }
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption, if (isStaff()) {
binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption); 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() { private void setupCalendar() {
@@ -195,10 +210,15 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
String query = binding.etSearchAdoption.getText().toString().trim(); String query = binding.etSearchAdoption.getText().toString().trim();
String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses"; String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses";
Long storeId = null; Long storeId;
List<StoreDTO> stores = viewModel.getStores().getValue(); if (isStaff()) {
if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { storeId = tokenManager.getPrimaryStoreId();
storeId = stores.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId(); } else {
storeId = null;
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId();
}
} }
String selectedDateString = null; String selectedDateString = null;

View File

@@ -17,6 +17,7 @@ import android.view.ViewGroup;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.AppointmentAdapter; import com.example.petstoremobile.adapters.AppointmentAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentAppointmentBinding; import com.example.petstoremobile.databinding.FragmentAppointmentBinding;
import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
@@ -39,6 +40,8 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
@@ -52,6 +55,8 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private AuthViewModel authViewModel; private AuthViewModel authViewModel;
private BulkDeleteHandler bulkDeleteHandler; private BulkDeleteHandler bulkDeleteHandler;
@Inject TokenManager tokenManager;
private CalendarDay selectedCalendarDay; private CalendarDay selectedCalendarDay;
private boolean isMonthMode = false; private boolean isMonthMode = false;
private Long currentUserId = null; private Long currentUserId = null;
@@ -126,7 +131,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
loadAppointmentData(); loadAppointmentData();
viewModel.loadStores(); if (!isStaff()) viewModel.loadStores();
} }
private void toggleCalendarMode() { private void toggleCalendarMode() {
@@ -151,8 +156,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
} }
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment, if (isStaff()) {
binding.spinnerStatus, binding.spinnerStore); 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() { 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() { private void loadAppointmentData() {
String query = binding.etSearchAppointment.getText().toString().trim(); String query = binding.etSearchAppointment.getText().toString().trim();
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
Long storeId = null; Long storeId;
List<StoreDTO> stores = viewModel.getStores().getValue(); if (isStaff()) {
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { storeId = tokenManager.getPrimaryStoreId();
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } else {
storeId = null;
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
} }
String selectedDateString = null; String selectedDateString = null;

View File

@@ -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<CustomerDTO> 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;
}
}

View File

@@ -16,6 +16,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.InventoryAdapter; import com.example.petstoremobile.adapters.InventoryAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentInventoryBinding; import com.example.petstoremobile.databinding.FragmentInventoryBinding;
import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
@@ -27,6 +28,8 @@ import com.example.petstoremobile.utils.SpinnerUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
@@ -38,6 +41,8 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
private InventoryListViewModel viewModel; private InventoryListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler; private BulkDeleteHandler bulkDeleteHandler;
@Inject TokenManager tokenManager;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -99,7 +104,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
viewModel.loadStores(); if (!isStaff()) viewModel.loadStores();
} }
@Override @Override
@@ -109,7 +114,16 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
} }
private void setupFilterToggle() { 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() { 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() : ""; String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : "";
if (query.isEmpty()) query = null; if (query.isEmpty()) query = null;
Long storeId = null; Long storeId;
List<StoreDTO> stores = viewModel.getStores().getValue(); if (isStaff()) {
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { storeId = tokenManager.getPrimaryStoreId();
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } else {
storeId = null;
List<StoreDTO> 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); viewModel.loadInventory(reset, query, storeId);

View File

@@ -88,6 +88,11 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshPet.setRefreshing(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() { private void setupBulkDelete() {
@@ -107,12 +112,23 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
loadPetData(); loadPetData();
viewModel.loadStores(); viewModel.loadSpecies();
if (!isStaff()) viewModel.loadStores();
} }
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet, if (isStaff()) {
binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore); 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() { private void setupSearch() {
@@ -125,8 +141,8 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
} }
private void setupSpeciesFilter() { private void setupSpeciesFilter() {
String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"}; String[] initial = {"All Species"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, species, this::loadPetData); SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, initial, this::loadPetData);
} }
private void setupStoreFilter() { private void setupStoreFilter() {
@@ -142,10 +158,15 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species"; String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species";
Long storeId = null; Long storeId;
List<StoreDTO> stores = viewModel.getStores().getValue(); if (isStaff()) {
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { storeId = tokenManager.getPrimaryStoreId();
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } else {
storeId = null;
List<StoreDTO> 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); viewModel.loadPets(query, status, species, storeId);

View File

@@ -14,6 +14,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.PurchaseOrderAdapter; import com.example.petstoremobile.adapters.PurchaseOrderAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding; import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding;
import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.dtos.PurchaseOrderDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
@@ -24,6 +25,8 @@ import com.example.petstoremobile.viewmodels.PurchaseOrderListViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
@@ -35,6 +38,8 @@ public class PurchaseOrderFragment extends Fragment
private PurchaseOrderAdapter adapter; private PurchaseOrderAdapter adapter;
private PurchaseOrderListViewModel viewModel; private PurchaseOrderListViewModel viewModel;
@Inject TokenManager tokenManager;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -79,11 +84,20 @@ public class PurchaseOrderFragment extends Fragment
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
loadData(); loadData();
viewModel.loadStores(); if (!isStaff()) viewModel.loadStores();
} }
private void setupFilterToggle() { 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() { private void setupSearch() {
@@ -108,10 +122,15 @@ public class PurchaseOrderFragment extends Fragment
String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : ""; String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : "";
if (query.isEmpty()) query = null; if (query.isEmpty()) query = null;
Long storeId = null; Long storeId;
List<StoreDTO> stores = viewModel.getStores().getValue(); if (isStaff()) {
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { storeId = tokenManager.getPrimaryStoreId();
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } else {
storeId = null;
List<StoreDTO> 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); viewModel.loadPurchaseOrders(query, storeId);

View File

@@ -15,6 +15,7 @@ import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.SaleAdapter; import com.example.petstoremobile.adapters.SaleAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentSaleBinding; import com.example.petstoremobile.databinding.FragmentSaleBinding;
import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.dtos.StoreDTO;
@@ -25,6 +26,8 @@ import com.example.petstoremobile.viewmodels.SaleListViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
@@ -35,6 +38,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
private SaleAdapter adapter; private SaleAdapter adapter;
private SaleListViewModel viewModel; private SaleListViewModel viewModel;
@Inject TokenManager tokenManager;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -87,12 +92,22 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
viewModel.loadStores(); if (!isStaff()) viewModel.loadStores();
} }
private void setupFilterToggle() { private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale, if (isStaff()) {
binding.spinnerPaymentMethod, binding.spinnerStore, binding.spinnerRefundStatus); 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() { private void setupStoreFilter() {
@@ -149,10 +164,15 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
paymentMethod = (String) binding.spinnerPaymentMethod.getSelectedItem(); paymentMethod = (String) binding.spinnerPaymentMethod.getSelectedItem();
} }
Long storeId = null; Long storeId;
List<StoreDTO> stores = viewModel.getStores().getValue(); if (isStaff()) {
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { storeId = tokenManager.getPrimaryStoreId();
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } else {
storeId = null;
List<StoreDTO> stores = viewModel.getStores().getValue();
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
} }
Boolean isRefund = null; Boolean isRefund = null;

View File

@@ -17,11 +17,14 @@ import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.viewmodels.AdoptionDetailViewModel; import com.example.petstoremobile.viewmodels.AdoptionDetailViewModel;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
/** /**
@@ -34,6 +37,8 @@ public class AdoptionDetailFragment extends Fragment {
private AdoptionDetailViewModel viewModel; private AdoptionDetailViewModel viewModel;
private boolean isUpdatingUI = false; private boolean isUpdatingUI = false;
@Inject TokenManager tokenManager;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -82,7 +87,7 @@ public class AdoptionDetailFragment extends Fragment {
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> { viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> {
AdoptionDetailViewModel.ViewState state = viewModel.getViewState().getValue(); 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, SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, list,
DropdownDTO::getLabel, "-- Select Store --", storeId, DropdownDTO::getId); DropdownDTO::getLabel, "-- Select Store --", storeId, DropdownDTO::getId);
}); });
@@ -113,6 +118,7 @@ public class AdoptionDetailFragment extends Fragment {
binding.spinnerAdoptionCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { binding.spinnerAdoptionCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (isUpdatingUI) return;
viewModel.onCustomerSelected(position); viewModel.onCustomerSelected(position);
} }
@Override @Override
@@ -122,6 +128,7 @@ public class AdoptionDetailFragment extends Fragment {
binding.spinnerAdoptionStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { binding.spinnerAdoptionStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (isUpdatingUI) return;
viewModel.onStoreSelected(position); viewModel.onStoreSelected(position);
} }
@Override @Override
@@ -210,9 +217,15 @@ public class AdoptionDetailFragment extends Fragment {
if (employees != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, if (employees != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee,
employees, DropdownDTO::getLabel, "-- Select Staff --", state.selectedEmployeeId, DropdownDTO::getId); employees, DropdownDTO::getLabel, "-- Select Staff --", state.selectedEmployeeId, DropdownDTO::getId);
if (isStaff()) UIUtils.setViewsEnabled(false, binding.spinnerAdoptionStore);
isUpdatingUI = false; isUpdatingUI = false;
} }
private boolean isStaff() {
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void saveAdoption() { private void saveAdoption() {
if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionCustomer, "Customer")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionCustomer, "Customer")) return;
if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionPet, "Pet")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionPet, "Pet")) return;
@@ -225,9 +238,16 @@ public class AdoptionDetailFragment extends Fragment {
try { fee = new BigDecimal(feeStr); } catch (NumberFormatException ignored) {} try { fee = new BigDecimal(feeStr); } catch (NumberFormatException ignored) {}
} }
DropdownDTO customer = viewModel.getCustomerList().getValue().get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); List<DropdownDTO> customerListVal = viewModel.getCustomerList().getValue();
DropdownDTO pet = viewModel.getPetList().getValue().get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); List<DropdownDTO> petListVal = viewModel.getPetList().getValue();
DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); List<DropdownDTO> 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; Long employeeId = null;
if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0 && viewModel.getEmployeeList().getValue() != null) { if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0 && viewModel.getEmployeeList().getValue() != null) {

View File

@@ -23,10 +23,13 @@ import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.viewmodels.AppointmentDetailViewModel; import com.example.petstoremobile.viewmodels.AppointmentDetailViewModel;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
/** /**
@@ -43,6 +46,8 @@ public class AppointmentDetailFragment extends Fragment {
private AppointmentDetailViewModel viewModel; private AppointmentDetailViewModel viewModel;
private boolean isUpdatingUI = false; private boolean isUpdatingUI = false;
@Inject TokenManager tokenManager;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -89,6 +94,7 @@ public class AppointmentDetailFragment extends Fragment {
binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (isUpdatingUI) return;
viewModel.onCustomerSelected(position); viewModel.onCustomerSelected(position);
} }
@Override @Override
@@ -98,6 +104,7 @@ public class AppointmentDetailFragment extends Fragment {
binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (isUpdatingUI) return;
viewModel.onStoreSelected(position); viewModel.onStoreSelected(position);
} }
@Override @Override
@@ -129,7 +136,7 @@ public class AppointmentDetailFragment extends Fragment {
viewModel.getStores().observe(getViewLifecycleOwner(), list -> { viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue(); 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); 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, if (staff != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff,
staff, DropdownDTO::getLabel, "-- Select Staff --", state.selectedStaffId, DropdownDTO::getId); staff, DropdownDTO::getLabel, "-- Select Staff --", state.selectedStaffId, DropdownDTO::getId);
if (isStaff()) binding.spinnerStore.setEnabled(false);
isUpdatingUI = false; isUpdatingUI = false;
} }
private boolean isStaff() {
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void notifyDateTimeStatusChange() { private void notifyDateTimeStatusChange() {
if (isUpdatingUI) return; if (isUpdatingUI) return;
@@ -248,7 +261,12 @@ public class AppointmentDetailFragment extends Fragment {
String date = binding.etAppointmentDate.getText().toString().trim(); String date = binding.etAppointmentDate.getText().toString().trim();
String time = buildTimeString(); 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)) { if (!viewModel.isValidFutureBooking(status, date, time)) {
DialogUtils.showInfoDialog(requireContext(), "Invalid Time", "Booked appointments must be in the future."); DialogUtils.showInfoDialog(requireContext(), "Invalid Time", "Booked appointments must be in the future.");

View File

@@ -16,6 +16,7 @@ import androidx.navigation.Navigation;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.databinding.FragmentCouponDetailBinding; import com.example.petstoremobile.databinding.FragmentCouponDetailBinding;
import com.example.petstoremobile.dtos.CouponDTO; import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
@@ -95,6 +96,9 @@ public class CouponDetailFragment extends Fragment {
} }
private void loadCouponDetails() { private void loadCouponDetails() {
binding.tvCouponId.setText(DateTimeUtils.formatId(couponId));
binding.tvCouponId.setVisibility(View.VISIBLE);
viewModel.loadCoupon(couponId).observe(getViewLifecycleOwner(), resource -> { viewModel.loadCoupon(couponId).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
CouponDTO coupon = resource.data; CouponDTO coupon = resource.data;

View File

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

View File

@@ -4,6 +4,7 @@ import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
@@ -17,6 +18,7 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentPetDetailBinding; import com.example.petstoremobile.databinding.FragmentPetDetailBinding;
import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PetDTO; 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.utils.UIUtils;
import com.example.petstoremobile.viewmodels.PetDetailViewModel; import com.example.petstoremobile.viewmodels.PetDetailViewModel;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
/** /**
@@ -43,6 +48,8 @@ public class PetDetailFragment extends Fragment {
private PetDetailViewModel viewModel; private PetDetailViewModel viewModel;
private boolean isUpdatingUI = false; private boolean isUpdatingUI = false;
@Inject TokenManager tokenManager;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -81,8 +88,19 @@ public class PetDetailFragment extends Fragment {
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> { viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> {
PetDetailViewModel.ViewState state = viewModel.getViewState().getValue(); 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); 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() { private void savePet() {
if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return; 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.isNotEmpty(binding.etPetBreed, "Breed")) return;
if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return; if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return;
if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return; if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return;
String name = binding.etPetName.getText().toString().trim(); String name = binding.etPetName.getText().toString().trim();
String species = binding.etPetSpecies.getText().toString().trim(); List<DropdownDTO> 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(); String breed = binding.etPetBreed.getText().toString().trim();
int age = Integer.parseInt(binding.etPetAge.getText().toString().trim()); int age = Integer.parseInt(binding.etPetAge.getText().toString().trim());
double price = Double.parseDouble(binding.etPetPrice.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.setCustomerId(customerId);
petDTO.setStoreId(storeId); 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 -> { viewModel.savePet(petDTO).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING); setLoading(resource.status == Resource.Status.LOADING);
@@ -202,7 +237,6 @@ public class PetDetailFragment extends Fragment {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
PetDTO p = resource.data; PetDTO p = resource.data;
binding.etPetName.setText(p.getPetName()); binding.etPetName.setText(p.getPetName());
binding.etPetSpecies.setText(p.getPetSpecies());
binding.etPetBreed.setText(p.getPetBreed()); binding.etPetBreed.setText(p.getPetBreed());
binding.etPetAge.setText(String.valueOf(p.getPetAge())); binding.etPetAge.setText(String.valueOf(p.getPetAge()));
if (p.getPetPrice() != null) { if (p.getPetPrice() != null) {
@@ -241,6 +275,11 @@ public class PetDetailFragment extends Fragment {
private void setupSpinner() { private void setupSpinner() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, new String[]{}); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, new String[]{});
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPetSpecies, p -> {
if (isUpdatingUI) return;
viewModel.onSpeciesSelected(p);
});
binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
@@ -290,7 +329,7 @@ public class PetDetailFragment extends Fragment {
binding.btnDeletePet.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); binding.btnDeletePet.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE);
binding.btnSavePet.setText(state.saveButtonText); 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.isBreedEnabled, binding.etPetBreed);
UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer); UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer);
UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore); UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore);
@@ -301,6 +340,13 @@ public class PetDetailFragment extends Fragment {
updateCustomerSpinnerSelection(state.selectedCustomerId); updateCustomerSpinnerSelection(state.selectedCustomerId);
updateStoreSpinnerSelection(state.selectedStoreId); updateStoreSpinnerSelection(state.selectedStoreId);
List<DropdownDTO> 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) { if (!state.isCustomerEnabled && binding.spinnerCustomer.getSelectedItemPosition() != 0) {
binding.spinnerCustomer.setSelection(0); binding.spinnerCustomer.setSelection(0);
} }
@@ -308,9 +354,22 @@ public class PetDetailFragment extends Fragment {
binding.spinnerStore.setSelection(0); 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; isUpdatingUI = false;
} }
private boolean isStaff() {
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void clearSpinnerError(Spinner spinner) { private void clearSpinnerError(Spinner spinner) {
View selectedView = spinner.getSelectedView(); View selectedView = spinner.getSelectedView();
if (selectedView instanceof TextView) { if (selectedView instanceof TextView) {

View File

@@ -17,6 +17,10 @@ import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.api.auth.TokenManager;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
@@ -27,6 +31,8 @@ public class SaleDetailFragment extends Fragment {
private FragmentSaleDetailBinding binding; private FragmentSaleDetailBinding binding;
private SaleDetailViewModel viewModel; private SaleDetailViewModel viewModel;
@Inject TokenManager tokenManager;
private final String[] PAYMENT_METHODS = { "Cash", "Card"}; private final String[] PAYMENT_METHODS = { "Cash", "Card"};
@Override @Override
@@ -42,11 +48,13 @@ public class SaleDetailFragment extends Fragment {
if (viewModel.isViewOnly()) { if (viewModel.isViewOnly()) {
binding.llAddItemRow.setVisibility(View.GONE); binding.llAddItemRow.setVisibility(View.GONE);
binding.llCouponInput.setVisibility(View.GONE);
binding.btnSaveSale.setVisibility(View.GONE); binding.btnSaveSale.setVisibility(View.GONE);
UIUtils.setViewsEnabled(false, binding.spinnerSaleStore, binding.spinnerSaleCustomer, binding.spinnerPaymentMethod); UIUtils.setViewsEnabled(false, binding.spinnerSaleStore, binding.spinnerSaleCustomer, binding.spinnerPaymentMethod);
} else { } else {
loadData(); loadData();
setupAddItem(); setupAddItem();
setupCoupon();
} }
binding.btnSaleBack.setOnClickListener(v -> navigateBack()); binding.btnSaleBack.setOnClickListener(v -> navigateBack());
@@ -56,10 +64,18 @@ public class SaleDetailFragment extends Fragment {
return binding.getRoot(); return binding.getRoot();
} }
private boolean isStaff() {
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void observeViewModel() { private void observeViewModel() {
viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> { viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> {
Long selectedStoreId = isStaff() ? tokenManager.getPrimaryStoreId() : -1L;
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, list, 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 -> { viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> {
@@ -185,13 +201,77 @@ public class SaleDetailFragment extends Fragment {
if (sale.getItems() != null) { if (sale.getItems() != null) {
binding.llSaleItems.removeAllViews(); binding.llSaleItems.removeAllViews();
for (SaleDTO.SaleItemDTO item : sale.getItems()) { 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() { private void setupAddItem() {
binding.btnAddItem.setOnClickListener(v -> { binding.btnAddItem.setOnClickListener(v -> {
if (!InputValidator.isSpinnerSelected(binding.spinnerSaleProduct, "Product")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerSaleProduct, "Product")) return;
@@ -228,15 +308,16 @@ public class SaleDetailFragment extends Fragment {
break; 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; if (getContext() == null) return;
LinearLayout row = new LinearLayout(getContext()); LinearLayout row = new LinearLayout(getContext());
row.setOrientation(LinearLayout.HORIZONTAL); row.setOrientation(LinearLayout.HORIZONTAL);
row.setPadding(0, 8, 0, 8); row.setPadding(0, 8, 0, 8);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
TextView tvName = new TextView(getContext()); TextView tvName = new TextView(getContext());
tvName.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)); 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(tvName);
row.addView(tvQty); row.addView(tvQty);
row.addView(tvPrice); 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); binding.llSaleItems.addView(row);
} }
private void updateTotal() { private void updateTotal() {
BigDecimal total = viewModel.calculateSubtotal(); BigDecimal subtotal = viewModel.calculateSubtotal();
binding.tvSaleSubtotal.setText("$" + total); 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); 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); SaleDTO dto = new SaleDTO(store.getId(), payment, viewModel.getCartItems().getValue(), false, null, customerId);
dto.setCouponId(viewModel.getAppliedCouponId());
viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> {
if (resource != null) { if (resource != null) {

View File

@@ -24,8 +24,8 @@ public class AdoptionRepository extends BaseRepository {
/** /**
* Retrieves a paginated list of all adoptions from the API. * Retrieves a paginated list of all adoptions from the API.
*/ */
public LiveData<Resource<PageResponse<AdoptionDTO>>> getAllAdoptions(int page, int size, String query, String status, Long storeId, String date, Long employeeId) { public LiveData<Resource<PageResponse<AdoptionDTO>>> 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)); return executeCall(adoptionApi.getAllAdoptions(page, size, query, status, storeId, date, employeeId, sort));
} }
/** /**

View File

@@ -24,8 +24,8 @@ public class AppointmentRepository extends BaseRepository {
/** /**
* Retrieves a paginated list of all appointments from the API with filtering. * Retrieves a paginated list of all appointments from the API with filtering.
*/ */
public LiveData<Resource<PageResponse<AppointmentDTO>>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date, Long employeeId) { public LiveData<Resource<PageResponse<AppointmentDTO>>> 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)); return executeCall(appointmentApi.getAllAppointments(page, size, query, status, storeId, date, employeeId, sort));
} }
/** /**

View File

@@ -30,6 +30,10 @@ public class CouponRepository extends BaseRepository {
return executeCall(couponApi.getCouponById(id)); return executeCall(couponApi.getCouponById(id));
} }
public LiveData<Resource<CouponDTO>> getCouponByCode(String code) {
return executeCall(couponApi.getCouponByCode(code));
}
public LiveData<Resource<CouponDTO>> createCoupon(CouponDTO coupon) { public LiveData<Resource<CouponDTO>> createCoupon(CouponDTO coupon) {
return executeCall(couponApi.createCoupon(coupon)); return executeCall(couponApi.createCoupon(coupon));
} }

View File

@@ -23,23 +23,26 @@ public class CustomerRepository extends BaseRepository {
this.customerApi = customerApi; this.customerApi = customerApi;
} }
/**
* Retrieves a paginated list of all customers from the API.
*/
public LiveData<Resource<PageResponse<CustomerDTO>>> getAllCustomers(int page, int size) { public LiveData<Resource<PageResponse<CustomerDTO>>> getAllCustomers(int page, int size) {
return executeCall(customerApi.getAllCustomers(page, size)); return executeCall(customerApi.getAllCustomers(page, size));
} }
/**
* Retrieves a specific customer by their ID.
*/
public LiveData<Resource<CustomerDTO>> getCustomerById(Long id) { public LiveData<Resource<CustomerDTO>> getCustomerById(Long id) {
return executeCall(customerApi.getCustomerById(id)); return executeCall(customerApi.getCustomerById(id));
} }
/** public LiveData<Resource<CustomerDTO>> createCustomer(CustomerDTO dto) {
* Retrieves a list of customer dropdowns from the API. return executeCall(customerApi.registerCustomer(dto));
*/ }
public LiveData<Resource<CustomerDTO>> updateCustomer(Long id, CustomerDTO dto) {
return executeCall(customerApi.updateCustomer(id, dto));
}
public LiveData<Resource<Void>> deleteCustomer(Long id) {
return executeCall(customerApi.deleteCustomer(id));
}
public LiveData<Resource<List<DropdownDTO>>> getCustomerDropdowns() { public LiveData<Resource<List<DropdownDTO>>> getCustomerDropdowns() {
return executeCall(customerApi.getCustomerDropdowns()); return executeCall(customerApi.getCustomerDropdowns());
} }

View File

@@ -54,6 +54,10 @@ public class PetRepository extends BaseRepository {
return executeCall(petApi.getPetDropdowns()); return executeCall(petApi.getPetDropdowns());
} }
public LiveData<Resource<List<DropdownDTO>>> getPetSpeciesDropdowns() {
return executeCall(petApi.getPetSpeciesDropdowns());
}
/** /**
* Retrieves available pets for a specific store. * Retrieves available pets for a specific store.
*/ */

View File

@@ -13,6 +13,8 @@ import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -71,18 +73,17 @@ public class AdoptionDetailViewModel extends ViewModel {
state.saveButtonText = isEditing ? "Save" : "Add"; state.saveButtonText = isEditing ? "Save" : "Add";
state.isAdoptionIdVisible = isEditing; state.isAdoptionIdVisible = isEditing;
state.isDeleteVisible = isEditing; state.isDeleteVisible = isEditing;
state.isFeeEnabled = false; // fee is always read-only state.isFeeEnabled = false;
if (!isEditing) { if (!isEditing) {
state.isCustomerEnabled = true; state.isCustomerEnabled = true;
state.isStoreEnabled = true; state.isStoreEnabled = true;
state.isPetEnabled = false; // until customer selected state.isPetEnabled = false;
state.isEmployeeEnabled = false; // until store selected state.isEmployeeEnabled = false;
state.isDateEnabled = true; state.isDateEnabled = true;
state.isStatusEnabled = true; state.isStatusEnabled = true;
state.availableStatuses = new String[]{"Pending"}; state.availableStatuses = new String[]{"Pending"};
state.selectedStatus = "Pending"; state.selectedStatus = "Pending";
} else { } else {
// edit: date-based logic applied after load
state.isCustomerEnabled = false; state.isCustomerEnabled = false;
state.isStoreEnabled = false; state.isStoreEnabled = false;
state.isPetEnabled = false; state.isPetEnabled = false;
@@ -95,12 +96,12 @@ public class AdoptionDetailViewModel extends ViewModel {
public void loadInitialFormData(boolean isEditing) { public void loadInitialFormData(boolean isEditing) {
// Pets are loaded dynamically based on store selection; no pre-load needed. // 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) { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
customerList.setValue(r.data); customerList.setValue(r.data);
} }
}); });
storeRepository.getStoreDropdowns().observeForever(r -> { observeOnce(storeRepository.getStoreDropdowns(), r -> {
if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
storeList.setValue(r.data); storeList.setValue(r.data);
} }
@@ -155,7 +156,7 @@ public class AdoptionDetailViewModel extends ViewModel {
} }
private void loadAvailablePetsByStore(Long storeId) { 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) { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
List<DropdownDTO> dropdowns = new ArrayList<>(); List<DropdownDTO> dropdowns = new ArrayList<>();
for (com.example.petstoremobile.dtos.PetDTO pet : r.data.getContent()) { for (com.example.petstoremobile.dtos.PetDTO pet : r.data.getContent()) {
@@ -167,7 +168,7 @@ public class AdoptionDetailViewModel extends ViewModel {
} }
private void loadPetPrice(Long petId) { 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) { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
com.example.petstoremobile.dtos.PetDTO pet = r.data; com.example.petstoremobile.dtos.PetDTO pet = r.data;
// In edit mode, add the pet to the list so the spinner can display its name // 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) { 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) { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) {
employeeList.setValue(r.data); employeeList.setValue(r.data);
} }
@@ -217,6 +218,10 @@ public class AdoptionDetailViewModel extends ViewModel {
s.isEmployeeEnabled = true; s.isEmployeeEnabled = true;
s.isDateEnabled = true; s.isDateEnabled = true;
s.isStatusEnabled = 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<Resource<AdoptionDTO>> loadAdoption() { public LiveData<Resource<AdoptionDTO>> loadAdoption() {
MutableLiveData<Resource<AdoptionDTO>> result = new MutableLiveData<>(); MutableLiveData<Resource<AdoptionDTO>> result = new MutableLiveData<>();
adoptionRepository.getAdoptionById(adoptionId).observeForever(resource -> { observeOnce(adoptionRepository.getAdoptionById(adoptionId), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
AdoptionDTO a = resource.data; AdoptionDTO a = resource.data;
String formattedStatus = DateTimeUtils.formatStatusFromBackend( String formattedStatus = DateTimeUtils.formatStatusFromBackend(
@@ -248,10 +253,10 @@ public class AdoptionDetailViewModel extends ViewModel {
String adoptionDate = a.getAdoptionDate() != null ? a.getAdoptionDate() : ""; String adoptionDate = a.getAdoptionDate() != null ? a.getAdoptionDate() : "";
updateViewState(state -> { updateViewState(state -> {
state.selectedPetId = a.getPetId() != null ? a.getPetId() : -1; state.selectedPetId = a.getPetId();
state.selectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; state.selectedCustomerId = a.getCustomerId();
state.selectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1; state.selectedStoreId = a.getSourceStoreId();
state.selectedEmployeeId = a.getEmployeeId() != null ? a.getEmployeeId() : -1; state.selectedEmployeeId = a.getEmployeeId();
state.adoptionDate = adoptionDate; state.adoptionDate = adoptionDate;
state.adoptionFee = a.getAdoptionFee() != null ? a.getAdoptionFee().toPlainString() : ""; state.adoptionFee = a.getAdoptionFee() != null ? a.getAdoptionFee().toPlainString() : "";
state.selectedStatus = formattedStatus; state.selectedStatus = formattedStatus;
@@ -321,6 +326,21 @@ public class AdoptionDetailViewModel extends ViewModel {
void run(T target); void run(T target);
} }
/**
* Observes a LiveData once, removing the observer after the first response.
* */
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public static class ViewState { public static class ViewState {
public boolean isEditing = false; public boolean isEditing = false;
public boolean isAdoptionIdVisible = false; public boolean isAdoptionIdVisible = false;

View File

@@ -11,6 +11,8 @@ import com.example.petstoremobile.repositories.AdoptionRepository;
import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -53,7 +55,7 @@ public class AdoptionListViewModel extends ViewModel {
if ("All Statuses".equals(status)) status = null; if ("All Statuses".equals(status)) status = null;
isLoading.setValue(true); 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 != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<AdoptionDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue()); List<AdoptionDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue());
@@ -70,13 +72,25 @@ public class AdoptionListViewModel extends ViewModel {
} }
public void loadStores() { 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) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent()); stores.setValue(resource.data.getContent());
} }
}); });
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public LiveData<Resource<Void>> bulkDeleteAdoptions(List<String> ids) { public LiveData<Resource<Void>> bulkDeleteAdoptions(List<String> ids) {
return adoptionRepository.bulkDeleteAdoptions(new BulkDeleteRequest(ids)); return adoptionRepository.bulkDeleteAdoptions(new BulkDeleteRequest(ids));
} }

View File

@@ -8,6 +8,8 @@ import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.repositories.SaleRepository; import com.example.petstoremobile.repositories.SaleRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
@@ -49,7 +51,7 @@ public class AnalyticsViewModel extends ViewModel {
public void loadAnalytics() { public void loadAnalytics() {
isLoading.setValue(true); isLoading.setValue(true);
errorMessage.setValue(null); 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 != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
cachedSales = resource.data.getContent(); cachedSales = resource.data.getContent();
@@ -247,6 +249,18 @@ public class AnalyticsViewModel extends ViewModel {
return "Daily Revenue (" + s + " " + e + ")"; return "Daily Revenue (" + s + " " + e + ")";
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public static class FilterState { public static class FilterState {
public String startDate = ""; public String startDate = "";
public String endDate = ""; public String endDate = "";

View File

@@ -15,6 +15,8 @@ import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -42,6 +44,7 @@ public class AppointmentDetailViewModel extends ViewModel {
private long appointmentId = -1; private long appointmentId = -1;
private boolean isOriginallyCancel = false; private boolean isOriginallyCancel = false;
private boolean isOriginallyCompletedOrMissed = false;
private Long currentCustomerId; private Long currentCustomerId;
private Long currentStoreId; private Long currentStoreId;
private Long currentPetId; private Long currentPetId;
@@ -71,14 +74,14 @@ public class AppointmentDetailViewModel extends ViewModel {
* Loads initial dropdown data for customers, stores, and services. * Loads initial dropdown data for customers, stores, and services.
*/ */
public void loadInitialFormData() { public void loadInitialFormData() {
customerRepository.getCustomerDropdowns().observeForever(r -> { observeOnce(customerRepository.getCustomerDropdowns(), r -> {
if (r.status == Resource.Status.SUCCESS) customers.setValue(r.data); if (r != null && r.status == Resource.Status.SUCCESS) customers.setValue(r.data);
}); });
storeRepository.getStoreDropdowns().observeForever(r -> { observeOnce(storeRepository.getStoreDropdowns(), r -> {
if (r.status == Resource.Status.SUCCESS) stores.setValue(r.data); if (r != null && r.status == Resource.Status.SUCCESS) stores.setValue(r.data);
}); });
serviceRepository.getAllServices(0, 200, null, "serviceName").observeForever(r -> { observeOnce(serviceRepository.getAllServices(0, 200, null, "serviceName"), r -> {
if (r.status == Resource.Status.SUCCESS && r.data != null) services.setValue(r.data.getContent()); 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. * Loads the list of pets for a specific customer.
*/ */
private void loadPetsForCustomer(Long customerId) { private void loadPetsForCustomer(Long customerId) {
petRepository.getCustomerPets(customerId).observeForever(r -> { observeOnce(petRepository.getCustomerPets(customerId), r -> {
if (r.status == Resource.Status.SUCCESS) customerPets.setValue(r.data); 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. * Loads the list of employees for a specific store.
*/ */
private void loadEmployeesForStore(Long storeId) { private void loadEmployeesForStore(Long storeId) {
storeRepository.getStoreEmployees(storeId).observeForever(r -> { observeOnce(storeRepository.getStoreEmployees(storeId), r -> {
if (r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data); if (r != null && r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data);
}); });
} }
@@ -227,10 +230,12 @@ public class AppointmentDetailViewModel extends ViewModel {
*/ */
public LiveData<Resource<AppointmentDTO>> loadAppointment() { public LiveData<Resource<AppointmentDTO>> loadAppointment() {
MutableLiveData<Resource<AppointmentDTO>> result = new MutableLiveData<>(); MutableLiveData<Resource<AppointmentDTO>> result = new MutableLiveData<>();
repository.getAppointmentById(appointmentId).observeForever(resource -> { observeOnce(repository.getAppointmentById(appointmentId), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
AppointmentDTO a = resource.data; AppointmentDTO a = resource.data;
isOriginallyCancel = "CANCELLED".equalsIgnoreCase(a.getAppointmentStatus()); isOriginallyCancel = "CANCELLED".equalsIgnoreCase(a.getAppointmentStatus());
isOriginallyCompletedOrMissed = "COMPLETED".equalsIgnoreCase(a.getAppointmentStatus())
|| "MISSED".equalsIgnoreCase(a.getAppointmentStatus());
currentCustomerId = a.getCustomerId(); currentCustomerId = a.getCustomerId();
currentStoreId = a.getStoreId(); currentStoreId = a.getStoreId();
currentPetId = a.getPetId(); currentPetId = a.getPetId();
@@ -282,21 +287,18 @@ public class AppointmentDetailViewModel extends ViewModel {
public void onDateOrTimeChanged(String date, String time, String currentStatus) { public void onDateOrTimeChanged(String date, String time, String currentStatus) {
updateViewState(s -> { updateViewState(s -> {
s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus); s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus);
// Keep selectedStatus if still valid; prefer explicit currentStatus from UI if valid
java.util.List<String> available = java.util.Arrays.asList(s.availableStatuses); java.util.List<String> available = java.util.Arrays.asList(s.availableStatuses);
if (!currentStatus.isEmpty() && available.contains(currentStatus)) { if (!currentStatus.isEmpty() && available.contains(currentStatus)) {
s.selectedStatus = currentStatus; s.selectedStatus = currentStatus;
} else if (!available.contains(s.selectedStatus) && s.availableStatuses.length > 0) { } else if (!available.contains(s.selectedStatus) && s.availableStatuses.length > 0) {
s.selectedStatus = s.availableStatuses[0]; s.selectedStatus = s.availableStatuses[0];
} }
boolean isPast = DateTimeUtils.isDateTimeInPast(date, time);
if (isOriginallyCancel) { if (isOriginallyCancel) {
s.isPast = true; s.isPast = true;
setAllFieldsEnabled(s, false); setAllFieldsEnabled(s, false);
s.isStatusEnabled = false; s.isStatusEnabled = false;
s.isSaveVisible = false; s.isSaveVisible = false;
} else if (isPast) { } else if (isOriginallyCompletedOrMissed) {
s.isPast = true; s.isPast = true;
setAllFieldsEnabled(s, false); setAllFieldsEnabled(s, false);
s.isStatusEnabled = true; s.isStatusEnabled = true;
@@ -322,7 +324,7 @@ public class AppointmentDetailViewModel extends ViewModel {
if (!isEditing) return new String[]{"Booked"}; if (!isEditing) return new String[]{"Booked"};
if (date == null || date.isEmpty()) return new String[]{}; if (date == null || date.isEmpty()) return new String[]{};
if (isOriginallyCancel) return new String[]{"Cancelled"}; 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"}; return new String[]{"Booked", "Cancelled"};
} }
@@ -355,8 +357,8 @@ public class AppointmentDetailViewModel extends ViewModel {
s.isCustomerEnabled = true; s.isCustomerEnabled = true;
s.isStoreEnabled = true; s.isStoreEnabled = true;
s.isServiceEnabled = true; s.isServiceEnabled = true;
s.isPetEnabled = false; // until customer selected s.isPetEnabled = false;
s.isStaffEnabled = false; // until store selected s.isStaffEnabled = false;
s.availableStatuses = new String[]{"Booked"}; s.availableStatuses = new String[]{"Booked"};
s.selectedStatus = "Booked"; s.selectedStatus = "Booked";
} }
@@ -385,6 +387,19 @@ public class AppointmentDetailViewModel extends ViewModel {
void run(T t); void run(T t);
} }
/** Observes a LiveData once, removing the observer after the first non-loading response. */
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> 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. * A Class to show the states of Appointment Detail Fragment.
*/ */

View File

@@ -11,6 +11,8 @@ import com.example.petstoremobile.repositories.AppointmentRepository;
import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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) { public void loadAppointments(String query, String status, Long storeId, String date, Long employeeId) {
isLoading.setValue(true); 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 != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
appointments.setValue(resource.data.getContent()); appointments.setValue(resource.data.getContent());
@@ -52,13 +54,25 @@ public class AppointmentListViewModel extends ViewModel {
} }
public void loadStores() { 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) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent()); stores.setValue(resource.data.getContent());
} }
}); });
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public LiveData<Resource<Void>> bulkDeleteAppointments(List<String> ids) { public LiveData<Resource<Void>> bulkDeleteAppointments(List<String> ids) {
return appointmentRepository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); return appointmentRepository.bulkDeleteAppointments(new BulkDeleteRequest(ids));
} }

View File

@@ -9,6 +9,8 @@ import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.repositories.CouponRepository; import com.example.petstoremobile.repositories.CouponRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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) { public void loadCoupons(int page, int size, Boolean active, String discountType, String sort) {
isLoading.setValue(true); 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 != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
coupons.setValue(resource.data.getContent()); coupons.setValue(resource.data.getContent());
@@ -44,6 +46,18 @@ public class CouponListViewModel extends ViewModel {
}); });
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public LiveData<Resource<Void>> bulkDeleteCoupons(List<Long> ids) { public LiveData<Resource<Void>> bulkDeleteCoupons(List<Long> ids) {
return repository.bulkDeleteCoupons(ids); return repository.bulkDeleteCoupons(ids);
} }

View File

@@ -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<Resource<CustomerDTO>> loadCustomer(long id) {
return repository.getCustomerById(id);
}
public LiveData<Resource<CustomerDTO>> saveCustomer(CustomerDTO dto) {
if (isEditing && customerId > 0) {
return repository.updateCustomer(customerId, dto);
} else {
return repository.createCustomer(dto);
}
}
public LiveData<Resource<Void>> deleteCustomer() {
return repository.deleteCustomer(customerId);
}
}

View File

@@ -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<List<CustomerDTO>> customers = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<CustomerDTO>> filteredCustomers = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private String lastQuery = "";
private String lastStatus = "All Statuses";
@Inject
public CustomerListViewModel(CustomerRepository repository) {
this.repository = repository;
}
public LiveData<List<CustomerDTO>> getFilteredCustomers() { return filteredCustomers; }
public LiveData<Boolean> 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 <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> 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<CustomerDTO> all = customers.getValue();
if (all == null) return;
List<CustomerDTO> 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);
}
}

View File

@@ -11,6 +11,8 @@ import com.example.petstoremobile.repositories.InventoryRepository;
import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -51,7 +53,7 @@ public class InventoryListViewModel extends ViewModel {
} }
isLoading.setValue(true); 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 != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<InventoryDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(inventory.getValue()); List<InventoryDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(inventory.getValue());
@@ -68,13 +70,25 @@ public class InventoryListViewModel extends ViewModel {
} }
public void loadStores() { 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) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent()); stores.setValue(resource.data.getContent());
} }
}); });
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public LiveData<Resource<Void>> bulkDeleteInventory(List<String> ids) { public LiveData<Resource<Void>> bulkDeleteInventory(List<String> ids) {
return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids)); return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids));
} }

View File

@@ -11,6 +11,8 @@ import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -30,12 +32,16 @@ public class PetDetailViewModel extends ViewModel {
private final MutableLiveData<List<DropdownDTO>> customerList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<DropdownDTO>> customerList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> speciesList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false); private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private final MutableLiveData<ViewState> viewState = new MutableLiveData<>(new ViewState()); private final MutableLiveData<ViewState> viewState = new MutableLiveData<>(new ViewState());
private long petId = -1; private long petId = -1;
private Long selectedCustomerId = null; private Long selectedCustomerId = null;
private Long selectedStoreId = null; private Long selectedStoreId = null;
private String selectedSpecies = null;
private boolean isOriginallyOwnedOrAdopted = false;
private Long originalCustomerId = null;
@Inject @Inject
public PetDetailViewModel(PetRepository petRepository, CustomerRepository customerRepository, StoreRepository storeRepository) { public PetDetailViewModel(PetRepository petRepository, CustomerRepository customerRepository, StoreRepository storeRepository) {
@@ -45,17 +51,23 @@ public class PetDetailViewModel extends ViewModel {
} }
public void loadInitialFormData() { public void loadInitialFormData() {
customerRepository.getCustomerDropdowns().observeForever(resource -> { observeOnce(customerRepository.getCustomerDropdowns(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
customerList.setValue(resource.data); customerList.setValue(resource.data);
} }
}); });
storeRepository.getStoreDropdowns().observeForever(resource -> { observeOnce(storeRepository.getStoreDropdowns(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList.setValue(resource.data); 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) { public void setPetId(long id) {
@@ -72,6 +84,14 @@ public class PetDetailViewModel extends ViewModel {
return current != null && current.isEditing; return current != null && current.isEditing;
} }
public boolean isOriginallyOwnedOrAdopted() {
return isOriginallyOwnedOrAdopted;
}
public Long getOriginalCustomerId() {
return originalCustomerId;
}
public LiveData<ViewState> getViewState() { public LiveData<ViewState> getViewState() {
return viewState; return viewState;
} }
@@ -87,6 +107,16 @@ public class PetDetailViewModel extends ViewModel {
updateViewState(state -> state.selectedCustomerId = selectedCustomerId); updateViewState(state -> state.selectedCustomerId = selectedCustomerId);
} }
public void onSpeciesSelected(int position) {
List<DropdownDTO> 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) { public void onStoreSelected(int position) {
List<DropdownDTO> list = storeList.getValue(); List<DropdownDTO> list = storeList.getValue();
if (position > 0 && list != null && position <= list.size()) { if (position > 0 && list != null && position <= list.size()) {
@@ -123,8 +153,10 @@ public class PetDetailViewModel extends ViewModel {
if (!isEditing) { if (!isEditing) {
selectedCustomerId = null; selectedCustomerId = null;
selectedStoreId = null; selectedStoreId = null;
selectedSpecies = null;
state.selectedCustomerId = null; state.selectedCustomerId = null;
state.selectedStoreId = null; state.selectedStoreId = null;
state.selectedSpecies = null;
state.selectedStatus = STATUS_AVAILABLE; state.selectedStatus = STATUS_AVAILABLE;
state.isCustomerEnabled = false; state.isCustomerEnabled = false;
state.isStoreEnabled = true; state.isStoreEnabled = true;
@@ -134,15 +166,20 @@ public class PetDetailViewModel extends ViewModel {
public LiveData<Resource<PetDTO>> loadPet() { public LiveData<Resource<PetDTO>> loadPet() {
MutableLiveData<Resource<PetDTO>> result = new MutableLiveData<>(); MutableLiveData<Resource<PetDTO>> result = new MutableLiveData<>();
petRepository.getPetById(petId).observeForever(resource -> { observeOnce(petRepository.getPetById(petId), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
PetDTO pet = resource.data; PetDTO pet = resource.data;
selectedCustomerId = pet.getCustomerId(); selectedCustomerId = pet.getCustomerId();
selectedStoreId = pet.getStoreId(); selectedStoreId = pet.getStoreId();
selectedSpecies = pet.getPetSpecies();
isOriginallyOwnedOrAdopted = STATUS_OWNED.equalsIgnoreCase(pet.getPetStatus())
|| STATUS_ADOPTED.equalsIgnoreCase(pet.getPetStatus());
originalCustomerId = pet.getCustomerId();
updateViewState(state -> { updateViewState(state -> {
state.selectedCustomerId = selectedCustomerId; state.selectedCustomerId = selectedCustomerId;
state.selectedStoreId = selectedStoreId; state.selectedStoreId = selectedStoreId;
state.selectedSpecies = selectedSpecies;
state.selectedStatus = normalizeStatus(pet.getPetStatus()); state.selectedStatus = normalizeStatus(pet.getPetStatus());
applyStatusRules(state, false); applyStatusRules(state, false);
}); });
@@ -174,6 +211,10 @@ public class PetDetailViewModel extends ViewModel {
return storeList; return storeList;
} }
public LiveData<List<DropdownDTO>> getSpeciesList() {
return speciesList;
}
public LiveData<Boolean> getIsLoading() { public LiveData<Boolean> getIsLoading() {
return isLoading; return isLoading;
} }
@@ -227,6 +268,17 @@ public class PetDetailViewModel extends ViewModel {
void run(T target); void run(T target);
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public static class ViewState { public static class ViewState {
public boolean isEditing = false; public boolean isEditing = false;
@@ -240,6 +292,7 @@ public class PetDetailViewModel extends ViewModel {
public String saveButtonText = "Add"; public String saveButtonText = "Add";
public String[] availableStatuses = new String[]{STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED}; public String[] availableStatuses = new String[]{STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED};
public String selectedStatus = STATUS_AVAILABLE; public String selectedStatus = STATUS_AVAILABLE;
public String selectedSpecies = null;
public Long selectedCustomerId = null; public Long selectedCustomerId = null;
public Long selectedStoreId = null; public Long selectedStoreId = null;
} }

View File

@@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.dtos.StoreDTO; 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.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -26,6 +29,7 @@ public class PetListViewModel extends ViewModel {
private final MutableLiveData<List<PetDTO>> pets = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<PetDTO>> pets = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<String>> speciesOptions = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false); private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject @Inject
@@ -36,6 +40,7 @@ public class PetListViewModel extends ViewModel {
public LiveData<List<PetDTO>> getPets() { return pets; } public LiveData<List<PetDTO>> getPets() { return pets; }
public LiveData<List<StoreDTO>> getStores() { return stores; } public LiveData<List<StoreDTO>> getStores() { return stores; }
public LiveData<List<String>> getSpeciesOptions() { return speciesOptions; }
public LiveData<Boolean> getIsLoading() { return isLoading; } public LiveData<Boolean> getIsLoading() { return isLoading; }
public void loadPets(String query, String status, String species, Long storeId) { 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; if ("All Species".equals(species)) species = null;
isLoading.setValue(true); 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 != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
pets.setValue(resource.data.getContent()); 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<String> labels = new ArrayList<>();
labels.add("All Species");
for (DropdownDTO dto : resource.data) {
labels.add(dto.getLabel());
}
speciesOptions.setValue(labels);
}
});
}
public void loadStores() { 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) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent()); stores.setValue(resource.data.getContent());
} }
}); });
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public LiveData<Resource<Void>> bulkDeletePets(List<String> ids) { public LiveData<Resource<Void>> bulkDeletePets(List<String> ids) {
return petRepository.bulkDeletePets(new BulkDeleteRequest(ids)); return petRepository.bulkDeletePets(new BulkDeleteRequest(ids));
} }

View File

@@ -10,6 +10,8 @@ import com.example.petstoremobile.repositories.CategoryRepository;
import com.example.petstoremobile.repositories.ProductRepository; import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -38,7 +40,7 @@ public class ProductListViewModel extends ViewModel {
public void loadProducts(String query, Long categoryId) { public void loadProducts(String query, Long categoryId) {
isLoading.setValue(true); 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 != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
products.setValue(resource.data.getContent()); products.setValue(resource.data.getContent());
@@ -51,10 +53,22 @@ public class ProductListViewModel extends ViewModel {
} }
public void loadCategories() { public void loadCategories() {
productRepository.getCategoryDropdowns().observeForever(resource -> { observeOnce(productRepository.getCategoryDropdowns(), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
categories.setValue(resource.data); categories.setValue(resource.data);
} }
}); });
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
} }

View File

@@ -13,6 +13,8 @@ import com.example.petstoremobile.repositories.ProductSupplierRepository;
import com.example.petstoremobile.repositories.SupplierRepository; import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -45,7 +47,7 @@ public class ProductSupplierListViewModel extends ViewModel {
public void loadProductSuppliers(String query, Long productId, Long supplierId) { public void loadProductSuppliers(String query, Long productId, Long supplierId) {
isLoading.setValue(true); 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 != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
productSuppliers.setValue(resource.data.getContent()); productSuppliers.setValue(resource.data.getContent());
@@ -58,19 +60,31 @@ public class ProductSupplierListViewModel extends ViewModel {
} }
public void loadFilterData() { 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) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
products.setValue(resource.data.getContent()); 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) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
suppliers.setValue(resource.data.getContent()); suppliers.setValue(resource.data.getContent());
} }
}); });
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public LiveData<Resource<Void>> bulkDeleteProductSuppliers(List<String> ids) { public LiveData<Resource<Void>> bulkDeleteProductSuppliers(List<String> ids) {
return psRepository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids)); return psRepository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids));
} }

View File

@@ -10,6 +10,8 @@ import com.example.petstoremobile.repositories.PurchaseOrderRepository;
import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -38,7 +40,7 @@ public class PurchaseOrderListViewModel extends ViewModel {
public void loadPurchaseOrders(String query, Long storeId) { public void loadPurchaseOrders(String query, Long storeId) {
isLoading.setValue(true); 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 != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
purchaseOrders.setValue(resource.data.getContent()); purchaseOrders.setValue(resource.data.getContent());
@@ -51,10 +53,22 @@ public class PurchaseOrderListViewModel extends ViewModel {
} }
public void loadStores() { 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) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent()); stores.setValue(resource.data.getContent());
} }
}); });
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
} }

View File

@@ -4,9 +4,11 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.SaleDTO;
import com.example.petstoremobile.repositories.CouponRepository;
import com.example.petstoremobile.repositories.CustomerRepository; import com.example.petstoremobile.repositories.CustomerRepository;
import com.example.petstoremobile.repositories.ProductRepository; import com.example.petstoremobile.repositories.ProductRepository;
import com.example.petstoremobile.repositories.SaleRepository; import com.example.petstoremobile.repositories.SaleRepository;
@@ -27,6 +29,7 @@ public class SaleDetailViewModel extends ViewModel {
private final StoreRepository storeRepository; private final StoreRepository storeRepository;
private final CustomerRepository customerRepository; private final CustomerRepository customerRepository;
private final ProductRepository productRepository; private final ProductRepository productRepository;
private final CouponRepository couponRepository;
private long saleId = -1; private long saleId = -1;
private boolean viewOnly = false; private boolean viewOnly = false;
@@ -35,15 +38,18 @@ public class SaleDetailViewModel extends ViewModel {
private final MutableLiveData<List<DropdownDTO>> customerList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<DropdownDTO>> customerList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<ProductDTO>> productList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<ProductDTO>> productList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<SaleDTO.SaleItemDTO>> cartItems = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<SaleDTO.SaleItemDTO>> cartItems = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<CouponDTO> appliedCoupon = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false); private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject @Inject
public SaleDetailViewModel(SaleRepository saleRepository, StoreRepository storeRepository, public SaleDetailViewModel(SaleRepository saleRepository, StoreRepository storeRepository,
CustomerRepository customerRepository, ProductRepository productRepository) { CustomerRepository customerRepository, ProductRepository productRepository,
CouponRepository couponRepository) {
this.saleRepository = saleRepository; this.saleRepository = saleRepository;
this.storeRepository = storeRepository; this.storeRepository = storeRepository;
this.customerRepository = customerRepository; this.customerRepository = customerRepository;
this.productRepository = productRepository; this.productRepository = productRepository;
this.couponRepository = couponRepository;
} }
public void setSaleId(long id, boolean viewOnly) { public void setSaleId(long id, boolean viewOnly) {
@@ -89,8 +95,46 @@ public class SaleDetailViewModel extends ViewModel {
cartItems.setValue(currentCart); cartItems.setValue(currentCart);
} }
public void removeFromCart(Long prodId) {
List<SaleDTO.SaleItemDTO> currentCart = new ArrayList<>(cartItems.getValue());
currentCart.removeIf(item -> item.getProdId().equals(prodId));
cartItems.setValue(currentCart);
}
public LiveData<List<SaleDTO.SaleItemDTO>> getCartItems() { return cartItems; } public LiveData<List<SaleDTO.SaleItemDTO>> getCartItems() { return cartItems; }
public LiveData<Resource<CouponDTO>> lookupCoupon(String code) {
return couponRepository.getCouponByCode(code);
}
public void setAppliedCoupon(CouponDTO coupon) {
appliedCoupon.setValue(coupon);
}
public void clearCoupon() {
appliedCoupon.setValue(null);
}
public LiveData<CouponDTO> 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() { public BigDecimal calculateSubtotal() {
BigDecimal total = BigDecimal.ZERO; BigDecimal total = BigDecimal.ZERO;
List<SaleDTO.SaleItemDTO> items = cartItems.getValue(); List<SaleDTO.SaleItemDTO> items = cartItems.getValue();

View File

@@ -11,6 +11,8 @@ import com.example.petstoremobile.repositories.SaleRepository;
import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -51,7 +53,7 @@ public class SaleListViewModel extends ViewModel {
} }
isLoading.setValue(true); 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 != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<SaleDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue()); List<SaleDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue());
@@ -68,10 +70,22 @@ public class SaleListViewModel extends ViewModel {
} }
public void loadStores() { 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) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent()); stores.setValue(resource.data.getContent());
} }
}); });
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
} }

View File

@@ -8,6 +8,8 @@ import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.repositories.ServiceRepository; import com.example.petstoremobile.repositories.ServiceRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import javax.inject.Inject; import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel; import dagger.hilt.android.lifecycle.HiltViewModel;
@@ -55,7 +57,7 @@ public class ServiceDetailViewModel extends ViewModel {
public LiveData<Resource<ServiceDTO>> loadService() { public LiveData<Resource<ServiceDTO>> loadService() {
MutableLiveData<Resource<ServiceDTO>> result = new MutableLiveData<>(); MutableLiveData<Resource<ServiceDTO>> result = new MutableLiveData<>();
repository.getServiceById(serviceId).observeForever(resource -> { observeOnce(repository.getServiceById(serviceId), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
ServiceDTO service = resource.data; ServiceDTO service = resource.data;
updateViewState(state -> { updateViewState(state -> {
@@ -106,6 +108,18 @@ public class ServiceDetailViewModel extends ViewModel {
void run(T target); void run(T target);
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public static class ViewState { public static class ViewState {
public boolean isEditing = false; public boolean isEditing = false;
public boolean isDeleteVisible = false; public boolean isDeleteVisible = false;

View File

@@ -10,6 +10,8 @@ import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.repositories.ServiceRepository; import com.example.petstoremobile.repositories.ServiceRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -46,7 +48,7 @@ public class ServiceListViewModel extends ViewModel {
} }
isLoading.setValue(true); 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 != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<ServiceDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(services.getValue()); List<ServiceDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(services.getValue());
@@ -62,6 +64,18 @@ public class ServiceListViewModel extends ViewModel {
}); });
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public LiveData<Resource<Void>> bulkDeleteServices(List<String> ids) { public LiveData<Resource<Void>> bulkDeleteServices(List<String> ids) {
return repository.bulkDeleteServices(new BulkDeleteRequest(ids)); return repository.bulkDeleteServices(new BulkDeleteRequest(ids));
} }

View File

@@ -10,6 +10,8 @@ import com.example.petstoremobile.repositories.EmployeeRepository;
import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -42,7 +44,7 @@ public class StaffListViewModel extends ViewModel {
public void loadStaff() { public void loadStaff() {
isLoading.setValue(true); isLoading.setValue(true);
repository.getAllEmployees(0, 100).observeForever(resource -> { observeOnce(repository.getAllEmployees(0, 100), resource -> {
if (resource != null) { if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
employees.setValue(resource.data.getContent()); employees.setValue(resource.data.getContent());
@@ -56,13 +58,25 @@ public class StaffListViewModel extends ViewModel {
} }
public void loadStores() { 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) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
stores.setValue(resource.data.getContent()); stores.setValue(resource.data.getContent());
} }
}); });
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public void filter(String query, Long storeId, String status) { public void filter(String query, Long storeId, String status) {
this.lastQuery = query; this.lastQuery = query;
this.lastStoreId = storeId; this.lastStoreId = storeId;

View File

@@ -8,6 +8,8 @@ import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.repositories.SupplierRepository; import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import javax.inject.Inject; import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel; import dagger.hilt.android.lifecycle.HiltViewModel;
@@ -55,7 +57,7 @@ public class SupplierDetailViewModel extends ViewModel {
public LiveData<Resource<SupplierDTO>> loadSupplier() { public LiveData<Resource<SupplierDTO>> loadSupplier() {
MutableLiveData<Resource<SupplierDTO>> result = new MutableLiveData<>(); MutableLiveData<Resource<SupplierDTO>> result = new MutableLiveData<>();
repository.getSupplierById(supId).observeForever(resource -> { observeOnce(repository.getSupplierById(supId), resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
SupplierDTO s = resource.data; SupplierDTO s = resource.data;
updateViewState(state -> { updateViewState(state -> {
@@ -99,6 +101,18 @@ public class SupplierDetailViewModel extends ViewModel {
void run(T target); void run(T target);
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public static class ViewState { public static class ViewState {
public boolean isEditing = false; public boolean isEditing = false;
public boolean isDeleteVisible = false; public boolean isDeleteVisible = false;

View File

@@ -9,6 +9,8 @@ import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.repositories.SupplierRepository; import com.example.petstoremobile.repositories.SupplierRepository;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import androidx.lifecycle.Observer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -33,7 +35,7 @@ public class SupplierListViewModel extends ViewModel {
public void loadSuppliers(String query) { public void loadSuppliers(String query) {
isLoading.setValue(true); isLoading.setValue(true);
repository.getAllSuppliers(0, 100, query, "supCompany").observeForever(resource -> { observeOnce(repository.getAllSuppliers(0, 100, query, "supCompany"), resource -> {
if (resource != null) { if (resource != null) {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
suppliers.setValue(resource.data.getContent()); suppliers.setValue(resource.data.getContent());
@@ -45,6 +47,18 @@ public class SupplierListViewModel extends ViewModel {
}); });
} }
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
liveData.observeForever(new Observer<Resource<T>>() {
@Override
public void onChanged(Resource<T> resource) {
if (resource == null || resource.status != Resource.Status.LOADING) {
liveData.removeObserver(this);
}
handler.onChanged(resource);
}
});
}
public LiveData<Resource<Void>> bulkDeleteSuppliers(List<String> ids) { public LiveData<Resource<Void>> bulkDeleteSuppliers(List<String> ids) {
return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids)); return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids));
} }

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/forgot_password_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/primary_dark"
android:padding="32dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginBottom="24dp"
android:scaleType="fitCenter"
android:src="@drawable/petstore_logo" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Forgot Password"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Enter your email to reset your password"
android:textColor="@color/text_light"
android:textSize="14sp"
android:textAlignment="center"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/white"
android:padding="24dp"
android:elevation="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Email Address"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="example@email.com"
android:inputType="textEmailAddress"
android:layout_marginBottom="24dp"
android:textColor="@color/text_dark"/>
<Button
android:id="@+id/btnSubmit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send Reset Link"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"
android:textSize="16sp"/>
<Button
android:id="@+id/btnBackToLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Back to Login"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:textColor="@color/accent_blue"
android:layout_marginTop="8dp"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.5"/>
</LinearLayout>

View File

@@ -109,6 +109,18 @@
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="16sp"/> android:textSize="16sp"/>
<TextView
android:id="@+id/tvForgotPassword"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:text="Forgot your password?"
android:textColor="@color/accent_blue"
android:textSize="14sp"
android:clickable="true"
android:focusable="true"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -60,6 +60,17 @@
android:padding="16dp" android:padding="16dp"
android:layout_marginBottom="16dp"> android:layout_marginBottom="16dp">
<TextView
android:id="@+id/tvCouponId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="8dp"
android:visibility="gone"/>
<!-- Coupon Code --> <!-- Coupon Code -->
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_grey">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/headerCustomer"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageButton
android:id="@+id/btnHamburgerCustomer"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/baseline_menu_36"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Open menu"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Customers"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginStart="8dp"/>
<ImageButton
android:id="@+id/btnToggleFilterCustomer"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_search"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="@color/white"
android:contentDescription="Toggle filter"/>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutFilterCustomer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:visibility="gone"
android:background="@color/primary_dark"
android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@drawable/bg_search_bar"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@android:drawable/ic_menu_search"
android:alpha="0.6"/>
<EditText
android:id="@+id/etSearchCustomer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="Search customers..."
android:inputType="text"
android:background="@android:color/transparent"
android:textColor="@color/text_dark"
android:textColorHint="#99000000"
android:textSize="14sp"
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
</LinearLayout>
<Spinner
android:id="@+id/spinnerStatusCustomer"
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:layout_marginTop="8dp"/>
</LinearLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshCustomer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewCustomer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAddCustomer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:backgroundTint="@color/accent_coral"
android:contentDescription="Add Customer"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@color/white"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,252 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_grey">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/primary_dark"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvCustomerMode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add Customer"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeleteCustomer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/accent_coral"
android:text="Delete"
android:textColor="@color/white"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_card"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/tvCustomerId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
android:textSize="11sp"
android:textStyle="italic"
android:layout_gravity="end"
android:layout_marginBottom="8dp"/>
<!-- Username -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Username"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter username"
android:inputType="text"
android:layout_marginBottom="16dp"/>
<!-- Password -->
<TextView
android:id="@+id/tvCustomerPasswordLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Password"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter password (min 6 characters)"
android:inputType="textPassword"
android:layout_marginBottom="16dp"/>
<!-- First Name -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="First Name"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerFirstName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter first name"
android:inputType="textPersonName"
android:layout_marginBottom="16dp"/>
<!-- Last Name -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Last Name"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerLastName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter last name"
android:inputType="textPersonName"
android:layout_marginBottom="16dp"/>
<!-- Email -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Email"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter email"
android:inputType="textEmailAddress"
android:layout_marginBottom="16dp"/>
<!-- Phone -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Phone"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/etCustomerPhone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter phone number"
android:inputType="phone"
android:layout_marginBottom="16dp"/>
<!-- Status -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Status"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerCustomerStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Loyalty Points (read-only) -->
<TextView
android:id="@+id/tvLoyaltyPointsLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Loyalty Points"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvCustomerLoyaltyPoints"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/text_dark"
android:textSize="16sp"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp">
<Button
android:id="@+id/btnCustomerBack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Back"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnSaveCustomer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Save"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"/>
</LinearLayout>
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:indeterminateTint="@color/accent_coral"/>
</FrameLayout>

View File

@@ -146,6 +146,23 @@
android:textSize="15sp"/> android:textSize="15sp"/>
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/drawerCustomers"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Customers"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/drawerServices" android:id="@+id/drawerServices"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -345,6 +362,7 @@
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="15sp"/> android:textSize="15sp"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -95,14 +95,11 @@
android:textSize="12sp" android:textSize="12sp"
android:layout_marginBottom="4dp"/> android:layout_marginBottom="4dp"/>
<EditText <Spinner
android:id="@+id/etPetSpecies" android:id="@+id/spinnerPetSpecies"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="e.g. Dog, Cat, Bird" android:layout_marginBottom="16dp"/>
android:inputType="text"
android:layout_marginBottom="16dp"
android:textColor="@color/text_dark"/>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -262,6 +262,66 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"/> android:orientation="vertical"/>
<LinearLayout
android:id="@+id/llCouponInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Coupon Code"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<EditText
android:id="@+id/etCouponCode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Enter coupon code"
android:inputType="textCapCharacters"
android:layout_marginEnd="8dp"/>
<Button
android:id="@+id/btnApplyCoupon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Apply"
android:backgroundTint="@color/primary_medium"
android:textColor="@color/white"/>
<Button
android:id="@+id/btnClearCoupon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Clear"
android:backgroundTint="@color/accent_coral"
android:textColor="@color/white"
android:layout_marginStart="4dp"
android:visibility="gone"/>
</LinearLayout>
<TextView
android:id="@+id/tvCouponInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:layout_marginTop="4dp"
android:visibility="gone"/>
</LinearLayout>
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/ivCustomerProfile"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="16dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder"
app:shapeAppearanceOverlay="@style/CircleImageView"
app:strokeWidth="2dp"
app:strokeColor="#BDBDBD"
android:padding="2dp"
android:contentDescription="Customer Profile Image" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvCustomerFullName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="Full Name"
android:textColor="@color/text_dark"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvCustomerLoyalty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingTop="3dp"
android:paddingEnd="8dp"
android:paddingBottom="3dp"
android:text="0 pts"
android:textColor="@color/white"
android:textSize="11sp"
android:background="#FF9800" />
</LinearLayout>
<TextView
android:id="@+id/tvCustomerUsername"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@username"
android:textColor="#888888"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tvCustomerEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="email@example.com"
android:textColor="#888888"
android:textSize="13sp"
android:ellipsize="end"
android:maxLines="1"/>
<TextView
android:id="@+id/tvCustomerStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Active"
android:textColor="@color/accent_coral"
android:textSize="13sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0"
android:layout_marginTop="12dp"/>
</LinearLayout>

View File

@@ -151,6 +151,18 @@
android:label="Staff Details" android:label="Staff Details"
tools:layout="@layout/fragment_staff_detail" /> tools:layout="@layout/fragment_staff_detail" />
<fragment
android:id="@+id/nav_customer"
android:name="com.example.petstoremobile.fragments.listfragments.CustomerFragment"
android:label="Customers"
tools:layout="@layout/fragment_customer" />
<fragment
android:id="@+id/nav_customer_detail"
android:name="com.example.petstoremobile.fragments.listfragments.detailfragments.CustomerDetailFragment"
android:label="Customer Details"
tools:layout="@layout/fragment_customer_detail" />
<fragment <fragment
android:id="@+id/nav_analytics" android:id="@+id/nav_analytics"
android:name="com.example.petstoremobile.fragments.listfragments.AnalyticsFragment" android:name="com.example.petstoremobile.fragments.listfragments.AnalyticsFragment"

View File

@@ -12,6 +12,8 @@ public class SaleResponse {
private String employeeName; private String employeeName;
private Long storeId; private Long storeId;
private String storeName; private String storeName;
private Long customerId;
private String customerName;
private BigDecimal totalAmount; private BigDecimal totalAmount;
private BigDecimal subtotalAmount; private BigDecimal subtotalAmount;
private BigDecimal couponDiscountAmount; private BigDecimal couponDiscountAmount;
@@ -77,6 +79,22 @@ public class SaleResponse {
this.storeName = storeName; this.storeName = storeName;
} }
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public BigDecimal getTotalAmount() { public BigDecimal getTotalAmount() {
return totalAmount; return totalAmount;
} }

View File

@@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -20,6 +21,8 @@ import java.util.List;
@Service @Service
public class SaleService { public class SaleService {
private static final BigDecimal EMPLOYEE_DISCOUNT_PERCENT = new BigDecimal("0.10");
private final SaleRepository saleRepository; private final SaleRepository saleRepository;
private final ProductRepository productRepository; private final ProductRepository productRepository;
private final StoreRepository storeRepository; private final StoreRepository storeRepository;
@@ -76,6 +79,7 @@ public class SaleService {
if (request.getCouponId() != null) { if (request.getCouponId() != null) {
Coupon coupon = couponRepository.findById(request.getCouponId()) Coupon coupon = couponRepository.findById(request.getCouponId())
.orElseThrow(() -> new ResourceNotFoundException("Coupon not found with id: " + request.getCouponId())); .orElseThrow(() -> new ResourceNotFoundException("Coupon not found with id: " + request.getCouponId()));
validateCoupon(coupon);
sale.setCoupon(coupon); sale.setCoupon(coupon);
} }
@@ -85,8 +89,9 @@ public class SaleService {
sale.setCart(cart); sale.setCart(cart);
} }
User customer = null;
if (request.getCustomerId() != null) { if (request.getCustomerId() != null) {
User customer = userRepository.findById(request.getCustomerId()) customer = userRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
sale.setCustomer(customer); sale.setCustomer(customer);
} }
@@ -97,7 +102,7 @@ public class SaleService {
sale.setOriginalSale(originalSale); sale.setOriginalSale(originalSale);
} }
BigDecimal totalAmount = BigDecimal.ZERO; BigDecimal subtotalAmount = BigDecimal.ZERO;
List<SaleItem> saleItems = new ArrayList<>(); List<SaleItem> saleItems = new ArrayList<>();
if (sale.getIsRefund() && sale.getOriginalSale() != null) { if (sale.getIsRefund() && sale.getOriginalSale() != null) {
@@ -145,9 +150,11 @@ public class SaleService {
saleItem.setUnitPrice(unitPrice); saleItem.setUnitPrice(unitPrice);
saleItems.add(saleItem); saleItems.add(saleItem);
totalAmount = totalAmount.add(itemTotal); subtotalAmount = subtotalAmount.add(itemTotal);
} }
totalAmount = totalAmount.negate(); subtotalAmount = subtotalAmount.negate();
sale.setSubtotalAmount(subtotalAmount);
sale.setTotalAmount(subtotalAmount);
} else { } else {
for (var itemRequest : request.getItems()) { for (var itemRequest : request.getItems()) {
Product product = productRepository.findById(itemRequest.getProdId()) Product product = productRepository.findById(itemRequest.getProdId())
@@ -174,17 +181,77 @@ public class SaleService {
saleItem.setUnitPrice(unitPrice); saleItem.setUnitPrice(unitPrice);
saleItems.add(saleItem); saleItems.add(saleItem);
totalAmount = totalAmount.add(itemTotal); subtotalAmount = subtotalAmount.add(itemTotal);
}
sale.setSubtotalAmount(subtotalAmount);
BigDecimal couponDiscount = calculateCouponDiscount(sale.getCoupon(), subtotalAmount);
sale.setCouponDiscountAmount(couponDiscount);
BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount));
sale.setEmployeeDiscountAmount(employeeDiscount);
BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount);
sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO));
sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue());
if (customer != null) {
customer.setLoyaltyPoints(customer.getLoyaltyPoints() + sale.getPointsEarned());
userRepository.save(customer);
} }
} }
sale.setTotalAmount(totalAmount);
sale.setItems(saleItems); sale.setItems(saleItems);
Sale savedSale = saleRepository.save(sale); Sale savedSale = saleRepository.save(sale);
return mapToResponse(savedSale); return mapToResponse(savedSale);
} }
private void validateCoupon(Coupon coupon) {
if (!Boolean.TRUE.equals(coupon.getActive())) {
throw new BusinessException("Coupon is not active");
}
LocalDateTime now = LocalDateTime.now();
if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) {
throw new BusinessException("Coupon has not started yet");
}
if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) {
throw new BusinessException("Coupon has expired");
}
}
private BigDecimal calculateCouponDiscount(Coupon coupon, BigDecimal subtotal) {
if (coupon == null || subtotal.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
if (coupon.getMinOrderAmount() != null && subtotal.compareTo(coupon.getMinOrderAmount()) < 0) {
return BigDecimal.ZERO;
}
BigDecimal discount = BigDecimal.ZERO;
String type = coupon.getDiscountType().trim().toUpperCase();
if ("PERCENTAGE".equals(type) || "PERCENT".equals(type)) {
discount = subtotal.multiply(coupon.getDiscountValue().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP));
} else if ("FIXED".equals(type)) {
discount = coupon.getDiscountValue();
}
return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP);
}
private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) {
if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
if (customer.getRole() == User.Role.STAFF || customer.getRole() == User.Role.ADMIN) {
return remainingAmount.multiply(EMPLOYEE_DISCOUNT_PERCENT).setScale(2, RoundingMode.HALF_UP);
}
return BigDecimal.ZERO;
}
private SaleResponse mapToResponse(Sale sale) { private SaleResponse mapToResponse(Sale sale) {
SaleResponse response = new SaleResponse(); SaleResponse response = new SaleResponse();
response.setSaleId(sale.getSaleId()); response.setSaleId(sale.getSaleId());
@@ -197,6 +264,11 @@ public class SaleService {
response.setStoreName(sale.getStore().getStoreName()); response.setStoreName(sale.getStore().getStoreName());
} }
if (sale.getCustomer() != null) {
response.setCustomerId(sale.getCustomer().getId());
response.setCustomerName(sale.getCustomer().getFirstName() + " " + sale.getCustomer().getLastName());
}
response.setTotalAmount(sale.getTotalAmount()); response.setTotalAmount(sale.getTotalAmount());
response.setSubtotalAmount(sale.getSubtotalAmount()); response.setSubtotalAmount(sale.getSubtotalAmount());
response.setCouponDiscountAmount(sale.getCouponDiscountAmount()); response.setCouponDiscountAmount(sale.getCouponDiscountAmount());