diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ActivityLogFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ActivityLogFragment.java index d54d8f6b..03a90fe3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ActivityLogFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ActivityLogFragment.java @@ -10,6 +10,7 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.widget.Toast; @@ -74,6 +75,22 @@ public class ActivityLogFragment extends Fragment { adapter = new ActivityLogAdapter(logList); binding.recyclerViewActivityLog.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewActivityLog.setAdapter(adapter); + + binding.recyclerViewActivityLog.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewActivityLog.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { + viewModel.loadLogs(false); + } + } + }); } private void setupFilters() { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index 125d92d8..a6ba396e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AdoptionAdapter; @@ -114,14 +115,14 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop adapter, "adoption", viewModel::bulkDeleteAdoptions, - this::loadAdoptions + () -> loadAdoptions(true) ); } @Override public void onResume() { super.onResume(); - loadAdoptions(); + loadAdoptions(true); if (!isStaff()) viewModel.loadStores(); } @@ -159,7 +160,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop } else { selectedCalendarDay = null; } - loadAdoptions(); + loadAdoptions(true); }); } @@ -187,26 +188,42 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop adapter = new AdoptionAdapter(adoptionList, this); binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAdoptions.setAdapter(adapter); + + binding.recyclerViewAdoptions.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewAdoptions.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { + loadAdoptions(false); + } + } + }); } private void setupSearch() { - UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions); + UIUtils.attachSearch(binding.etSearchAdoption, () -> loadAdoptions(true)); } private void setupStatusFilter() { String[] statuses = {"All Statuses", "Completed", "Pending", "Missed", "Cancelled"}; - SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions); + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, () -> loadAdoptions(true)); } private void setupStoreFilter() { - SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, this::loadAdoptions); + SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, () -> loadAdoptions(true)); } private void setupSwipeRefresh() { - binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions); + binding.swipeRefreshAdoption.setOnRefreshListener(() -> loadAdoptions(true)); } - private void loadAdoptions() { + private void loadAdoptions(boolean reset) { String query = binding.etSearchAdoption.getText().toString().trim(); String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses"; @@ -230,7 +247,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop if (status.equals("All Statuses")) status = null; else status = status.toUpperCase(); - viewModel.loadAdoptions(true, query, status, storeId, selectedDateString, null); + viewModel.loadAdoptions(reset, query, status, storeId, selectedDateString, null); } private void openDetail(int position) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index 037c7ce9..b8196775 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; @@ -123,14 +124,14 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. adapter, "appointment", viewModel::bulkDeleteAppointments, - this::loadAppointmentData + () -> loadAppointmentData(true) ); } @Override public void onResume() { super.onResume(); - loadAppointmentData(); + loadAppointmentData(true); if (!isStaff()) viewModel.loadStores(); } @@ -143,7 +144,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private void setupMyAppointmentFilter() { binding.btnMyAppointments.setOnClickListener(v -> { - loadAppointmentData(); + loadAppointmentData(true); }); } @@ -177,7 +178,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. } else { selectedCalendarDay = null; } - loadAppointmentData(); + loadAppointmentData(true); }); } @@ -200,20 +201,20 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. } private void setupSearch() { - UIUtils.attachSearch(binding.etSearchAppointment, this::loadAppointmentData); + UIUtils.attachSearch(binding.etSearchAppointment, () -> loadAppointmentData(true)); } private void setupStatusFilter() { String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"}; - SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadAppointmentData); + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, () -> loadAppointmentData(true)); } private void setupStoreFilter() { - SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadAppointmentData); + SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadAppointmentData(true)); } private void setupSwipeRefresh() { - binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData); + binding.swipeRefreshAppointment.setOnRefreshListener(() -> loadAppointmentData(true)); } private void openAppointmentDetails(int position) { @@ -241,7 +242,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. return "STAFF".equalsIgnoreCase(tokenManager.getRole()); } - private void loadAppointmentData() { + private void loadAppointmentData(boolean reset) { String query = binding.etSearchAppointment.getText().toString().trim(); String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; @@ -270,13 +271,29 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. if (status.equals("All Statuses")) status = null; else status = status.toUpperCase(); - viewModel.loadAppointments(query, status, storeId, selectedDateString, employeeId); + viewModel.loadAppointments(reset, query, status, storeId, selectedDateString, employeeId); } private void setupRecyclerView() { adapter = new AppointmentAdapter(appointmentList, this); binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAppointments.setAdapter(adapter); + + binding.recyclerViewAppointments.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewAppointments.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { + loadAppointmentData(false); + } + } + }); } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java index 548d6018..c0beebfe 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java @@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.Navigation; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.CouponAdapter; @@ -46,7 +47,7 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl setupSwipeRefresh(); observeViewModel(); - viewModel.loadCoupons(0, 100, null, null, null); + applyFilters(true); binding.fabAddCoupon.setOnClickListener(v -> openDetail(-1)); binding.btnBulkDeleteCoupons.setOnClickListener(v -> confirmBulkDelete()); @@ -74,38 +75,54 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl adapter = new CouponAdapter(couponList, this); binding.recyclerViewCoupon.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewCoupon.setAdapter(adapter); + + binding.recyclerViewCoupon.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewCoupon.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { + applyFilters(false); + } + } + }); } private void setupStatusFilter() { String[] statuses = {"All Statuses", "Active", "Inactive"}; - SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCoupon, statuses, this::applyFilters); + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCoupon, statuses, () -> applyFilters(true)); } private void setupTypeFilter() { String[] types = {"All Types", "FIXED", "PERCENT"}; - SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerTypeCoupon, types, this::applyFilters); + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerTypeCoupon, types, () -> applyFilters(true)); } private void setupSearch() { - UIUtils.attachSearch(binding.etSearchCoupon, this::applyFilters); + UIUtils.attachSearch(binding.etSearchCoupon, () -> applyFilters(true)); } private void setupSwipeRefresh() { - binding.swipeRefreshCoupon.setOnRefreshListener(this::applyFilters); + binding.swipeRefreshCoupon.setOnRefreshListener(() -> applyFilters(true)); } - private void applyFilters() { - String statusStr = binding.spinnerStatusCoupon.getSelectedItem() != null ? + private void applyFilters(boolean reset) { + String statusStr = binding.spinnerStatusCoupon.getSelectedItem() != null ? binding.spinnerStatusCoupon.getSelectedItem().toString() : "All Statuses"; Boolean active = null; if (statusStr.equals("Active")) active = true; else if (statusStr.equals("Inactive")) active = false; - String typeStr = binding.spinnerTypeCoupon.getSelectedItem() != null ? + String typeStr = binding.spinnerTypeCoupon.getSelectedItem() != null ? binding.spinnerTypeCoupon.getSelectedItem().toString() : "All Types"; String discountType = typeStr.equals("All Types") ? null : typeStr; - viewModel.loadCoupons(0, 100, active, discountType, null); + viewModel.loadCoupons(reset, active, discountType, null); } private void openDetail(long id) { @@ -133,7 +150,7 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl viewModel.bulkDeleteCoupons(ids).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { adapter.setSelectionMode(false); - applyFilters(); + applyFilters(true); } else if (resource.status == Resource.Status.ERROR) { UIUtils.showToast(requireContext(), resource.message); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java index 69fb967c..5afe4d1f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java @@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.CustomerAdapter; import com.example.petstoremobile.api.auth.TokenManager; @@ -44,7 +45,7 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust setupSwipeRefresh(); observeViewModel(); - viewModel.loadCustomers(); + viewModel.loadCustomers(true); binding.fabAddCustomer.setOnClickListener(v -> openDetail(-1)); @@ -73,6 +74,22 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust adapter.setToken(tokenManager.getToken()); binding.recyclerViewCustomer.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewCustomer.setAdapter(adapter); + + binding.recyclerViewCustomer.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewCustomer.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { + viewModel.loadCustomers(false); + } + } + }); } private void setupStatusFilter() { @@ -92,7 +109,7 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust } private void setupSwipeRefresh() { - binding.swipeRefreshCustomer.setOnRefreshListener(viewModel::loadCustomers); + binding.swipeRefreshCustomer.setOnRefreshListener(() -> viewModel.loadCustomers(true)); } private void openDetail(int position) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index cd00726a..96225e6a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -8,11 +8,11 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.PetAdapter; @@ -21,7 +21,6 @@ import com.example.petstoremobile.databinding.FragmentPetBinding; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.PetListViewModel; @@ -91,7 +90,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen viewModel.getSpeciesOptions().observe(getViewLifecycleOwner(), options -> { String[] arr = options.toArray(new String[0]); - SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, arr, this::loadPetData); + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, arr, () -> loadPetData(true)); }); } @@ -104,14 +103,14 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen adapter, "pet", viewModel::bulkDeletePets, - this::loadPetData + () -> loadPetData(true) ); } @Override public void onResume() { super.onResume(); - loadPetData(); + loadPetData(true); viewModel.loadSpecies(); if (!isStaff()) viewModel.loadStores(); } @@ -132,28 +131,28 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } private void setupSearch() { - UIUtils.attachSearch(binding.etSearchPet, this::loadPetData); + UIUtils.attachSearch(binding.etSearchPet, () -> loadPetData(true)); } private void setupStatusFilter() { String[] statuses = {"All Statuses", "Available", "Adopted", "Owned", "Pending"}; - SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData); + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, () -> loadPetData(true)); } private void setupSpeciesFilter() { String[] initial = {"All Species"}; - SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, initial, this::loadPetData); + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, initial, () -> loadPetData(true)); } private void setupStoreFilter() { - SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData); + SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadPetData(true)); } private void setupSwipeRefresh() { - binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData); + binding.swipeRefreshPet.setOnRefreshListener(() -> loadPetData(true)); } - private void loadPetData() { + private void loadPetData(boolean reset) { String query = binding.etSearchPet.getText().toString().trim(); String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species"; @@ -169,7 +168,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } } - viewModel.loadPets(query, status, species, storeId); + viewModel.loadPets(reset, query, status, species, storeId); } private void setupRecyclerView() { @@ -178,6 +177,22 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen adapter.setToken(tokenManager.getToken()); binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPets.setAdapter(adapter); + + binding.recyclerViewPets.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewPets.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { + loadPetData(false); + } + } + }); } private void openPetProfile(int position) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 566f4ee7..0820aca6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; @@ -85,28 +86,28 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc @Override public void onResume() { super.onResume(); - loadProductData(); + loadProductData(true); viewModel.loadCategories(); } private void setupFilterToggle() { - UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, + UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchProduct, binding.spinnerCategory); } private void setupSearch() { - UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData); + UIUtils.attachSearch(binding.etSearchProduct, () -> loadProductData(true)); } private void setupCategoryFilter() { - SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData); + SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, () -> loadProductData(true)); } private void setupSwipeRefresh() { - binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData); + binding.swipeRefreshProduct.setOnRefreshListener(() -> loadProductData(true)); } - private void loadProductData() { + private void loadProductData(boolean reset) { String query = binding.etSearchProduct.getText().toString().trim(); if (query.isEmpty()) query = null; @@ -116,7 +117,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getId(); } - viewModel.loadProducts(query, categoryId); + viewModel.loadProducts(reset, query, categoryId); } private void setupRecyclerView() { @@ -124,6 +125,22 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc adapter.setBaseUrl(baseUrl); binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewProducts.setAdapter(adapter); + + binding.recyclerViewProducts.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewProducts.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { + loadProductData(false); + } + } + }); } private void openProductDetails(int position) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index 6a066528..c4751ae3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductSupplierAdapter; @@ -34,7 +35,7 @@ public class ProductSupplierFragment extends Fragment private FragmentProductSupplierBinding binding; private List psList = new ArrayList<>(); - + private ProductSupplierAdapter adapter; private ProductSupplierListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; @@ -97,14 +98,14 @@ public class ProductSupplierFragment extends Fragment adapter, "relationship", viewModel::bulkDeleteProductSuppliers, - this::loadData + () -> loadData(true) ); } @Override public void onResume() { super.onResume(); - loadData(); + loadData(true); viewModel.loadFilterData(); } @@ -123,25 +124,41 @@ public class ProductSupplierFragment extends Fragment adapter = new ProductSupplierAdapter(psList, this); binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPS.setAdapter(adapter); + + binding.recyclerViewPS.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewPS.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { + loadData(false); + } + } + }); } private void setupSearch() { - UIUtils.attachSearch(binding.etSearchPS, this::loadData); + UIUtils.attachSearch(binding.etSearchPS, () -> loadData(true)); } private void setupProductFilter() { - SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData); + SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, () -> loadData(true)); } private void setupSupplierFilter() { - SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData); + SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, () -> loadData(true)); } private void setupSwipeRefresh() { - binding.swipeRefreshPS.setOnRefreshListener(this::loadData); + binding.swipeRefreshPS.setOnRefreshListener(() -> loadData(true)); } - private void loadData() { + private void loadData(boolean reset) { String query = binding.etSearchPS.getText().toString().trim(); if (query.isEmpty()) query = null; @@ -157,7 +174,7 @@ public class ProductSupplierFragment extends Fragment supplierId = suppliers.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId(); } - viewModel.loadProductSuppliers(query, productId, supplierId); + viewModel.loadProductSuppliers(reset, query, productId, supplierId); } private void openDetail(int position) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index 10b9ee3c..f4e4e230 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.PurchaseOrderAdapter; @@ -83,7 +84,7 @@ public class PurchaseOrderFragment extends Fragment @Override public void onResume() { super.onResume(); - loadData(); + loadData(true); if (!isStaff()) viewModel.loadStores(); } @@ -101,24 +102,40 @@ public class PurchaseOrderFragment extends Fragment } private void setupSearch() { - UIUtils.attachSearch(binding.etSearchPO, this::loadData); + UIUtils.attachSearch(binding.etSearchPO, () -> loadData(true)); } private void setupStoreFilter() { - SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadData); + SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadData(true)); } private void setupRecyclerView() { adapter = new PurchaseOrderAdapter(poList, this); binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPO.setAdapter(adapter); + + binding.recyclerViewPO.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewPO.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { + loadData(false); + } + } + }); } private void setupSwipeRefresh() { - binding.swipeRefreshPO.setOnRefreshListener(this::loadData); + binding.swipeRefreshPO.setOnRefreshListener(() -> loadData(true)); } - private void loadData() { + private void loadData(boolean reset) { String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : ""; if (query.isEmpty()) query = null; @@ -133,7 +150,7 @@ public class PurchaseOrderFragment extends Fragment } } - viewModel.loadPurchaseOrders(query, storeId); + viewModel.loadPurchaseOrders(reset, query, storeId); } private void openDetail(int position) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java index 8540b5f8..e1e92ca7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java @@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.EmployeeAdapter; import com.example.petstoremobile.api.auth.TokenManager; @@ -39,15 +40,15 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye Bundle savedInstanceState) { binding = FragmentStaffBinding.inflate(inflater, container, false); viewModel = new ViewModelProvider(this).get(StaffListViewModel.class); - + setupRecyclerView(); setupSearch(); setupStatusFilter(); setupStoreFilter(); setupSwipeRefresh(); observeViewModel(); - - viewModel.loadStaff(); + + viewModel.loadStaff(true); viewModel.loadStores(); binding.fabAddStaff.setOnClickListener(v -> openDetail(-1)); @@ -82,6 +83,22 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye adapter.setToken(tokenManager.getToken()); binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewStaff.setAdapter(adapter); + + binding.recyclerViewStaff.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewStaff.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { + viewModel.loadStaff(false); + } + } + }); } private void setupStatusFilter() { @@ -99,9 +116,9 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye private void applyFilters() { String query = binding.etSearchStaff.getText().toString().trim(); - String status = binding.spinnerStatusStaff.getSelectedItem() != null ? + String status = binding.spinnerStatusStaff.getSelectedItem() != null ? binding.spinnerStatusStaff.getSelectedItem().toString() : "All Statuses"; - + Long storeId = null; List stores = viewModel.getStores().getValue(); if (binding.spinnerStoreStaff.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { @@ -112,7 +129,7 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye } private void setupSwipeRefresh() { - binding.swipeRefreshStaff.setOnRefreshListener(viewModel::loadStaff); + binding.swipeRefreshStaff.setOnRefreshListener(() -> viewModel.loadStaff(true)); } private void openDetail(int position) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index 78d43bd6..163ac217 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; @@ -52,8 +53,8 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp setupFilterToggle(); setupBulkDelete(); observeViewModel(); - - loadSupplierData(); + + loadSupplierData(true); binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1)); @@ -83,7 +84,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp adapter, "supplier", viewModel::bulkDeleteSuppliers, - this::loadSupplierData + () -> loadSupplierData(true) ); } @@ -98,11 +99,11 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp } private void setupSearch() { - UIUtils.attachSearch(binding.etSearchSupplier, this::loadSupplierData); + UIUtils.attachSearch(binding.etSearchSupplier, () -> loadSupplierData(true)); } private void setupSwipeRefresh() { - binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData); + binding.swipeRefreshSupplier.setOnRefreshListener(() -> loadSupplierData(true)); } private void openSupplierDetails(int position) { @@ -126,15 +127,31 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp } } - private void loadSupplierData() { + private void loadSupplierData(boolean reset) { String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : ""; if (query.isEmpty()) query = null; - viewModel.loadSuppliers(query); + viewModel.loadSuppliers(reset, query); } private void setupRecyclerView() { adapter = new SupplierAdapter(supplierList, this); binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewSuppliers.setAdapter(adapter); + + binding.recyclerViewSuppliers.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewSuppliers.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { + loadSupplierData(false); + } + } + }); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ActivityLogListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ActivityLogListViewModel.java index 8bcb5cc4..ff7ff71c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ActivityLogListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ActivityLogListViewModel.java @@ -20,7 +20,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class ActivityLogListViewModel extends ViewModel { - private static final int LIMIT = 2000; + private static final int PAGE_SIZE = 20; private final ActivityLogRepository repository; private final StoreRepository storeRepository; @@ -33,6 +33,9 @@ public class ActivityLogListViewModel extends ViewModel { private String currentRole = null; private String currentSearch = null; + private int currentLimit = PAGE_SIZE; + private boolean isLastPage = false; + @Inject public ActivityLogListViewModel(ActivityLogRepository repository, StoreRepository storeRepository) { this.repository = repository; @@ -42,10 +45,11 @@ public class ActivityLogListViewModel extends ViewModel { public LiveData> getLogs() { return logs; } public LiveData> getStoreOptions() { return storeOptions; } public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } public void loadInitialData() { loadStores(); - loadLogs(); + loadLogs(true); } private void loadStores() { @@ -56,12 +60,24 @@ public class ActivityLogListViewModel extends ViewModel { }); } - public void loadLogs() { + public void loadLogs(boolean reset) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentLimit = PAGE_SIZE; + isLastPage = false; + } + + if (isLastPage) return; + isLoading.setValue(true); - observeOnce(repository.getActivityLogs(LIMIT, currentStoreId, currentRole, currentSearch), resource -> { + observeOnce(repository.getActivityLogs(currentLimit, currentStoreId, currentRole, currentSearch), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - logs.setValue(resource.data); + List result = resource.data; + logs.setValue(result); + isLastPage = result.size() < currentLimit; + if (!isLastPage) currentLimit += PAGE_SIZE; isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false); @@ -72,17 +88,17 @@ public class ActivityLogListViewModel extends ViewModel { public void setRoleFilter(String role) { currentRole = "All Roles".equals(role) ? null : role; - loadLogs(); + loadLogs(true); } public void setStoreFilter(Long storeId) { currentStoreId = storeId; - loadLogs(); + loadLogs(true); } public void setSearchQuery(String query) { currentSearch = (query == null || query.trim().isEmpty()) ? null : query.trim(); - loadLogs(); + loadLogs(true); } private void observeOnce(LiveData> liveData, Observer> handler) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java index ec3ecfab..005198d1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java @@ -52,6 +52,8 @@ public class AdoptionListViewModel extends ViewModel { isLastPage = false; } + if (isLastPage) return; + if ("All Statuses".equals(status)) status = null; isLoading.setValue(true); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java index 88997684..19924349 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java @@ -29,6 +29,10 @@ public class AppointmentListViewModel extends ViewModel { private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + @Inject public AppointmentListViewModel(AppointmentRepository appointmentRepository, StoreRepository storeRepository) { this.appointmentRepository = appointmentRepository; @@ -38,13 +42,27 @@ public class AppointmentListViewModel extends ViewModel { public LiveData> getAppointments() { return appointments; } public LiveData> getStores() { return stores; } public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadAppointments(boolean reset, String query, String status, Long storeId, String date, Long employeeId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + if (isLastPage) return; - public void loadAppointments(String query, String status, Long storeId, String date, Long employeeId) { isLoading.setValue(true); - observeOnce(appointmentRepository.getAllAppointments(0, 500, query, status, storeId, date, employeeId, "appointmentId,desc"), resource -> { + observeOnce(appointmentRepository.getAllAppointments(currentPage, PAGE_SIZE, query, status, storeId, date, employeeId, "appointmentId,desc"), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - appointments.setValue(resource.data.getContent()); + List currentList = reset ? new ArrayList<>() : new ArrayList<>(appointments.getValue()); + currentList.addAll(resource.data.getContent()); + appointments.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java index e1ea12f8..d825b935 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java @@ -5,7 +5,6 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.CouponDTO; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.repositories.CouponRepository; import com.example.petstoremobile.utils.Resource; @@ -24,6 +23,10 @@ public class CouponListViewModel extends ViewModel { private final MutableLiveData> coupons = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + @Inject public CouponListViewModel(CouponRepository repository) { this.repository = repository; @@ -31,13 +34,27 @@ public class CouponListViewModel extends ViewModel { public LiveData> getCoupons() { return coupons; } public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadCoupons(boolean reset, Boolean active, String discountType, String sort) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + if (isLastPage) return; - public void loadCoupons(int page, int size, Boolean active, String discountType, String sort) { isLoading.setValue(true); - observeOnce(repository.getAllCoupons(page, size, active, discountType, sort), resource -> { + observeOnce(repository.getAllCoupons(currentPage, PAGE_SIZE, active, discountType, sort), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - coupons.setValue(resource.data.getContent()); + List currentList = reset ? new ArrayList<>() : new ArrayList<>(coupons.getValue()); + currentList.addAll(resource.data.getContent()); + coupons.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java index 1d7c18ba..a216c47a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java @@ -25,6 +25,10 @@ public class CustomerListViewModel extends ViewModel { private final MutableLiveData> filteredCustomers = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + private String lastQuery = ""; private String lastStatus = "All Statuses"; @@ -35,14 +39,28 @@ public class CustomerListViewModel extends ViewModel { public LiveData> getFilteredCustomers() { return filteredCustomers; } public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadCustomers(boolean reset) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + if (isLastPage) return; - public void loadCustomers() { isLoading.setValue(true); - observeOnce(repository.getAllCustomers(0, 200), resource -> { + observeOnce(repository.getAllCustomers(currentPage, PAGE_SIZE), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - customers.setValue(resource.data.getContent()); - filter(lastQuery, lastStatus); + List currentList = reset ? new ArrayList<>() : new ArrayList<>(customers.getValue()); + currentList.addAll(resource.data.getContent()); + customers.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + applyFilter(currentList); isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false); @@ -66,23 +84,25 @@ public class CustomerListViewModel extends ViewModel { public void filter(String query, String status) { this.lastQuery = query; this.lastStatus = status; + applyFilter(customers.getValue()); + } - List all = customers.getValue(); + private void applyFilter(List all) { if (all == null) return; List filtered = new ArrayList<>(); - String lowerQuery = query.toLowerCase(); + String lowerQuery = lastQuery.toLowerCase(); for (CustomerDTO c : all) { - boolean matchesQuery = query.isEmpty() || + boolean matchesQuery = lastQuery.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())); + boolean matchesStatus = lastStatus.equals("All Statuses") || + (lastStatus.equals("Active") && Boolean.TRUE.equals(c.getActive())) || + (lastStatus.equals("Inactive") && Boolean.FALSE.equals(c.getActive())); if (matchesQuery && matchesStatus) { filtered.add(c); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java index af6770f8..89f56d51 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java @@ -6,7 +6,6 @@ import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.DropdownDTO; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.repositories.PetRepository; @@ -32,6 +31,10 @@ public class PetListViewModel extends ViewModel { private final MutableLiveData> speciesOptions = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + @Inject public PetListViewModel(PetRepository petRepository, StoreRepository storeRepository) { this.petRepository = petRepository; @@ -42,16 +45,32 @@ public class PetListViewModel extends ViewModel { public LiveData> getStores() { return stores; } public LiveData> getSpeciesOptions() { return speciesOptions; } public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadPets(boolean reset, String query, String status, String species, Long storeId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + if (isLastPage) return; - public void loadPets(String query, String status, String species, Long storeId) { if ("All Statuses".equals(status)) status = null; if ("All Species".equals(species)) species = null; isLoading.setValue(true); - observeOnce(petRepository.getAllPets(0, 100, query, status, species, storeId, null, "petName"), resource -> { + final String finalStatus = status; + final String finalSpecies = species; + observeOnce(petRepository.getAllPets(currentPage, PAGE_SIZE, query, finalStatus, finalSpecies, storeId, null, "petName"), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - pets.setValue(resource.data.getContent()); + List currentList = reset ? new ArrayList<>() : new ArrayList<>(pets.getValue()); + currentList.addAll(resource.data.getContent()); + pets.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java index 503bf8b6..b22dee6c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java @@ -28,6 +28,10 @@ public class ProductListViewModel extends ViewModel { private final MutableLiveData> categories = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + @Inject public ProductListViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) { this.productRepository = productRepository; @@ -37,13 +41,27 @@ public class ProductListViewModel extends ViewModel { public LiveData> getProducts() { return products; } public LiveData> getCategories() { return categories; } public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadProducts(boolean reset, String query, Long categoryId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + if (isLastPage) return; - public void loadProducts(String query, Long categoryId) { isLoading.setValue(true); - observeOnce(productRepository.getAllProducts(query, categoryId, 0, 100, "prodName"), resource -> { + observeOnce(productRepository.getAllProducts(query, categoryId, currentPage, PAGE_SIZE, "prodName"), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - products.setValue(resource.data.getContent()); + List currentList = reset ? new ArrayList<>() : new ArrayList<>(products.getValue()); + currentList.addAll(resource.data.getContent()); + products.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java index 25f7b8a1..930770a4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java @@ -33,6 +33,10 @@ public class ProductSupplierListViewModel extends ViewModel { private final MutableLiveData> suppliers = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + @Inject public ProductSupplierListViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) { this.psRepository = psRepository; @@ -44,13 +48,27 @@ public class ProductSupplierListViewModel extends ViewModel { public LiveData> getProducts() { return products; } public LiveData> getSuppliers() { return suppliers; } public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadProductSuppliers(boolean reset, String query, Long productId, Long supplierId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + if (isLastPage) return; - public void loadProductSuppliers(String query, Long productId, Long supplierId) { isLoading.setValue(true); - observeOnce(psRepository.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName"), resource -> { + observeOnce(psRepository.getAllProductSuppliers(currentPage, PAGE_SIZE, query, productId, supplierId, "productName"), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - productSuppliers.setValue(resource.data.getContent()); + List currentList = reset ? new ArrayList<>() : new ArrayList<>(productSuppliers.getValue()); + currentList.addAll(resource.data.getContent()); + productSuppliers.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java index 296e4792..923d336e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java @@ -28,6 +28,10 @@ public class PurchaseOrderListViewModel extends ViewModel { private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + @Inject public PurchaseOrderListViewModel(PurchaseOrderRepository purchaseOrderRepository, StoreRepository storeRepository) { this.purchaseOrderRepository = purchaseOrderRepository; @@ -37,13 +41,27 @@ public class PurchaseOrderListViewModel extends ViewModel { public LiveData> getPurchaseOrders() { return purchaseOrders; } public LiveData> getStores() { return stores; } public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadPurchaseOrders(boolean reset, String query, Long storeId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + if (isLastPage) return; - public void loadPurchaseOrders(String query, Long storeId) { isLoading.setValue(true); - observeOnce(purchaseOrderRepository.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc"), resource -> { + observeOnce(purchaseOrderRepository.getAllPurchaseOrders(currentPage, PAGE_SIZE, query, storeId, "purchaseOrderId,desc"), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - purchaseOrders.setValue(resource.data.getContent()); + List currentList = reset ? new ArrayList<>() : new ArrayList<>(purchaseOrders.getValue()); + currentList.addAll(resource.data.getContent()); + purchaseOrders.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java index 023d1aaf..3dc40816 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java @@ -28,6 +28,11 @@ public class StaffListViewModel extends ViewModel { private final MutableLiveData> filteredEmployees = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + private String lastQuery = ""; private Long lastStoreId = null; private String lastStatus = "All Statuses"; @@ -41,14 +46,28 @@ public class StaffListViewModel extends ViewModel { public LiveData> getFilteredEmployees() { return filteredEmployees; } public LiveData> getStores() { return stores; } public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadStaff(boolean reset) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + if (isLastPage) return; - public void loadStaff() { isLoading.setValue(true); - observeOnce(repository.getAllEmployees(0, 100), resource -> { + observeOnce(repository.getAllEmployees(currentPage, PAGE_SIZE), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - employees.setValue(resource.data.getContent()); - filter(lastQuery, lastStoreId, lastStatus); + List currentList = reset ? new ArrayList<>() : new ArrayList<>(employees.getValue()); + currentList.addAll(resource.data.getContent()); + employees.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + applyFilter(currentList); isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false); @@ -81,28 +100,27 @@ public class StaffListViewModel extends ViewModel { this.lastQuery = query; this.lastStoreId = storeId; this.lastStatus = status; - - List all = employees.getValue(); + applyFilter(employees.getValue()); + } + + private void applyFilter(List all) { if (all == null) return; List filtered = new ArrayList<>(); - String lowerQuery = query.toLowerCase(); + String lowerQuery = lastQuery.toLowerCase(); for (EmployeeDTO e : all) { - // Search Query Filter - boolean matchesQuery = query.isEmpty() || + boolean matchesQuery = lastQuery.isEmpty() || (e.getFullName() != null && e.getFullName().toLowerCase().contains(lowerQuery)) || (e.getUsername() != null && e.getUsername().toLowerCase().contains(lowerQuery)) || (e.getEmail() != null && e.getEmail().toLowerCase().contains(lowerQuery)) || (e.getPhone() != null && e.getPhone().toLowerCase().contains(lowerQuery)); - // Store Filter - boolean matchesStore = storeId == null || (e.getPrimaryStoreId() != null && e.getPrimaryStoreId().equals(storeId)); + boolean matchesStore = lastStoreId == null || (e.getPrimaryStoreId() != null && e.getPrimaryStoreId().equals(lastStoreId)); - // Status Filter - boolean matchesStatus = status.equals("All Statuses") || - (status.equals("Active") && Boolean.TRUE.equals(e.getActive())) || - (status.equals("Inactive") && Boolean.FALSE.equals(e.getActive())); + boolean matchesStatus = lastStatus.equals("All Statuses") || + (lastStatus.equals("Active") && Boolean.TRUE.equals(e.getActive())) || + (lastStatus.equals("Inactive") && Boolean.FALSE.equals(e.getActive())); if (matchesQuery && matchesStore && matchesStatus) { filtered.add(e); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java index 462030ea..33373e90 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java @@ -25,6 +25,10 @@ public class SupplierListViewModel extends ViewModel { private final MutableLiveData> suppliers = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + @Inject public SupplierListViewModel(SupplierRepository repository) { this.repository = repository; @@ -32,13 +36,27 @@ public class SupplierListViewModel extends ViewModel { public LiveData> getSuppliers() { return suppliers; } public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadSuppliers(boolean reset, String query) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + if (isLastPage) return; - public void loadSuppliers(String query) { isLoading.setValue(true); - observeOnce(repository.getAllSuppliers(0, 100, query, "supCompany"), resource -> { + observeOnce(repository.getAllSuppliers(currentPage, PAGE_SIZE, query, "supCompany"), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - suppliers.setValue(resource.data.getContent()); + List currentList = reset ? new ArrayList<>() : new ArrayList<>(suppliers.getValue()); + currentList.addAll(resource.data.getContent()); + suppliers.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false);