From 017ef65b5ac61ba2a28af28198406c70561c7ecf Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Wed, 15 Apr 2026 00:00:17 -0600 Subject: [PATCH 1/7] desktop chat now shows images --- .../api/dto/chat/MessageResponse.java | 9 ++++++ .../controllers/ChatController.java | 30 ++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageResponse.java index 3432a918..7aecada0 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageResponse.java @@ -14,6 +14,7 @@ public class MessageResponse { private Boolean isRead; private String attachmentName; private String attachmentUrl; + private String attachmentMimeType; public MessageResponse() { } @@ -105,4 +106,12 @@ public class MessageResponse { public void setAttachmentUrl(String attachmentUrl) { this.attachmentUrl = attachmentUrl; } + + public String getAttachmentMimeType() { + return attachmentMimeType; + } + + public void setAttachmentMimeType(String attachmentMimeType) { + this.attachmentMimeType = attachmentMimeType; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java index c746db2e..bf28085d 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java @@ -19,6 +19,7 @@ import javafx.scene.layout.VBox; import javafx.scene.paint.ImagePattern; import javafx.scene.shape.Circle; import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import org.example.petshopdesktop.api.ChatRealtimeClient; import org.example.petshopdesktop.api.dto.chat.ConversationResponse; import org.example.petshopdesktop.api.dto.chat.MessageRequest; @@ -539,10 +540,31 @@ public class ChatController { bubble.getChildren().add(content); } if (message.getAttachmentName() != null && !message.getAttachmentName().isBlank()) { - Label attachmentLabel = new Label("\uD83D\uDCCE " + message.getAttachmentName()); - attachmentLabel.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#475569") + "; -fx-font-size: 12px;"); - attachmentLabel.setWrapText(true); - bubble.getChildren().add(attachmentLabel); + String mimeType = message.getAttachmentMimeType(); + boolean isImage = mimeType != null && mimeType.startsWith("image/"); + if (isImage && message.getId() != null) { + ImageView imageView = new ImageView(); + imageView.setFitWidth(280); + imageView.setFitHeight(280); + imageView.setPreserveRatio(true); + imageView.setSmooth(true); + bubble.getChildren().add(imageView); + String attachmentPath = "/api/v1/chat/messages/" + message.getId() + "/attachment"; + new Thread(() -> { + try { + byte[] bytes = ApiClient.getInstance().getBytes(attachmentPath); + Image img = new Image(new java.io.ByteArrayInputStream(bytes)); + if (!img.isError()) { + Platform.runLater(() -> imageView.setImage(img)); + } + } catch (Exception ignored) {} + }).start(); + } else { + Label attachmentLabel = new Label("\uD83D\uDCCE " + message.getAttachmentName()); + attachmentLabel.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#475569") + "; -fx-font-size: 12px;"); + attachmentLabel.setWrapText(true); + bubble.getChildren().add(attachmentLabel); + } } bubble.getChildren().add(timestamp); From 024a618473a4775f426868beda971afb8c5ebf16 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Wed, 15 Apr 2026 00:27:06 -0600 Subject: [PATCH 2/7] fixed issue for desktop when sending a file too large --- .../example/petshopdesktop/api/ApiClient.java | 30 ++++++++++++------- .../controllers/ChatController.java | 19 +++++++++++- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java b/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java index 50a14bce..baf1b99a 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java @@ -4,7 +4,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.example.petshopdesktop.auth.UserSession; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.SequenceInputStream; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -150,26 +153,33 @@ public class ApiClient { String mimeType = Files.probeContentType(filePath); if (mimeType == null || mimeType.isBlank()) mimeType = "application/octet-stream"; - byte[] fileBytes = Files.readAllBytes(filePath); String fileName = filePath.getFileName().toString(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + filePartName + byte[] fileHeader = ("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + filePartName + "\"; filename=\"" + fileName + "\"\r\nContent-Type: " + mimeType + "\r\n\r\n") - .getBytes(StandardCharsets.UTF_8)); - out.write(fileBytes); - out.write("\r\n".getBytes(StandardCharsets.UTF_8)); + .getBytes(StandardCharsets.UTF_8); + + ByteArrayOutputStream tail = new ByteArrayOutputStream(); + tail.write("\r\n".getBytes(StandardCharsets.UTF_8)); if (textContent != null && !textContent.isBlank()) { - out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + textPartName + tail.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + textPartName + "\"\r\n\r\n" + textContent + "\r\n").getBytes(StandardCharsets.UTF_8)); } - out.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8)); + tail.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8)); + + InputStream body = new SequenceInputStream( + java.util.Collections.enumeration(java.util.Arrays.asList( + new ByteArrayInputStream(fileHeader), + Files.newInputStream(filePath), + new ByteArrayInputStream(tail.toByteArray()) + )) + ); HttpRequest.Builder builder = HttpRequest.newBuilder() .uri(URI.create(baseUrl + path)) .header("Content-Type", "multipart/form-data; boundary=" + boundary) - .POST(HttpRequest.BodyPublishers.ofByteArray(out.toByteArray())) - .timeout(Duration.ofSeconds(30)); + .POST(HttpRequest.BodyPublishers.ofInputStream(() -> body)) + .timeout(Duration.ofSeconds(120)); addAuthHeader(builder); HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); return handleResponse(response, responseClass); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java index bf28085d..4417bd23 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java @@ -204,11 +204,18 @@ public class ChatController { } lblChatStatus.setText("Message sent"); }); + } catch (OutOfMemoryError e) { + Platform.runLater(() -> { + txtMessage.setText(content); + btnSend.setDisable(false); + lblChatStatus.setText("File too large to send"); + }); } catch (Exception e) { Platform.runLater(() -> { txtMessage.setText(content); btnSend.setDisable(false); - lblChatStatus.setText("Chat send failed"); + String msg = e.getMessage(); + lblChatStatus.setText(msg != null && !msg.isBlank() ? msg : "Chat send failed"); ActivityLogger.getInstance().logException( "ChatController.sendMessage", e, @@ -223,6 +230,16 @@ public class ChatController { File file = org.example.petshopdesktop.util.FilePickerSupport.pickAnyFile(btnAttachment.getScene().getWindow()); if (file == null) return; + long maxBytes = 5 * 1024 * 1024; + if (file.length() > maxBytes) { + javafx.scene.control.Alert alert = new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.WARNING); + alert.setTitle("File Too Large"); + alert.setHeaderText("The selected file exceeds the 5 MB limit."); + alert.setContentText("Please choose a smaller file and try again."); + alert.showAndWait(); + return; + } + selectedAttachmentFile = file; btnAttachment.setText("πŸ“Ž " + file.getName()); btnAttachment.setStyle("-fx-background-color: #dcfce7; -fx-background-radius: 12; -fx-text-fill: #166534; -fx-cursor: hand;"); From 4643feb8687df9df38764ff546b4205bf9c389a5 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Wed, 15 Apr 2026 00:46:32 -0600 Subject: [PATCH 3/7] added pagenation to android for each fragment --- .../listfragments/ActivityLogFragment.java | 17 +++++++ .../listfragments/AdoptionFragment.java | 35 ++++++++++---- .../listfragments/AppointmentFragment.java | 37 ++++++++++---- .../listfragments/CouponFragment.java | 37 ++++++++++---- .../listfragments/CustomerFragment.java | 21 +++++++- .../fragments/listfragments/PetFragment.java | 39 ++++++++++----- .../listfragments/ProductFragment.java | 31 +++++++++--- .../ProductSupplierFragment.java | 35 ++++++++++---- .../listfragments/PurchaseOrderFragment.java | 29 ++++++++--- .../listfragments/StaffFragment.java | 29 ++++++++--- .../listfragments/SupplierFragment.java | 31 +++++++++--- .../viewmodels/ActivityLogListViewModel.java | 32 +++++++++---- .../viewmodels/AdoptionListViewModel.java | 2 + .../viewmodels/AppointmentListViewModel.java | 24 ++++++++-- .../viewmodels/CouponListViewModel.java | 25 ++++++++-- .../viewmodels/CustomerListViewModel.java | 40 ++++++++++++---- .../viewmodels/PetListViewModel.java | 27 +++++++++-- .../viewmodels/ProductListViewModel.java | 24 ++++++++-- .../ProductSupplierListViewModel.java | 24 ++++++++-- .../PurchaseOrderListViewModel.java | 24 ++++++++-- .../viewmodels/StaffListViewModel.java | 48 +++++++++++++------ .../viewmodels/SupplierListViewModel.java | 24 ++++++++-- 22 files changed, 501 insertions(+), 134 deletions(-) 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); From c8a1c29cc335f00076e4aabd118169c0eedb7c8e Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Wed, 15 Apr 2026 01:09:56 -0600 Subject: [PATCH 4/7] removed status on purchase order android --- .../adapters/PurchaseOrderAdapter.java | 22 ------------------- .../PurchaseOrderDetailFragment.java | 21 ------------------ .../layout/fragment_purchase_order_detail.xml | 13 ----------- .../main/res/layout/item_purchase_order.xml | 13 ----------- 4 files changed, 69 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java index a31a3d0a..ee98f2a3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java @@ -1,6 +1,5 @@ package com.example.petstoremobile.adapters; -import android.graphics.Color; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; @@ -49,27 +48,6 @@ public class PurchaseOrderAdapter extends RecyclerView.Adapter listener.onPurchaseOrderClick(position)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java index 4b28ed92..d1bbd31c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java @@ -1,6 +1,5 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; -import android.graphics.Color; import android.os.Bundle; import android.view.*; import android.widget.Toast; @@ -82,26 +81,6 @@ public class PurchaseOrderDetailFragment extends Fragment { binding.tvPODetailSupplier.setText(po.getSupplierName()); binding.tvPODetailStore.setText(po.getStoreName() != null ? po.getStoreName() : "N/A"); binding.tvPODetailDate.setText(po.getOrderDate()); - - String status = po.getStatus() != null ? po.getStatus() : ""; - binding.tvPODetailStatus.setText(status); - switch (status.toUpperCase()) { - case "RECEIVED": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#4CAF50")); - break; - case "PLACED": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#2196F3")); - break; - case "PENDING": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#FF9800")); - break; - case "CANCELLED": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#F44336")); - break; - default: - binding.tvPODetailStatus.setTextColor(Color.parseColor("#9E9E9E")); - break; - } } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load order: " + resource.message, Toast.LENGTH_SHORT).show(); } diff --git a/android/app/src/main/res/layout/fragment_purchase_order_detail.xml b/android/app/src/main/res/layout/fragment_purchase_order_detail.xml index c7a4cad8..377ecf3b 100644 --- a/android/app/src/main/res/layout/fragment_purchase_order_detail.xml +++ b/android/app/src/main/res/layout/fragment_purchase_order_detail.xml @@ -105,19 +105,6 @@ android:textSize="15sp" android:layout_marginBottom="16dp"/> - - - diff --git a/android/app/src/main/res/layout/item_purchase_order.xml b/android/app/src/main/res/layout/item_purchase_order.xml index ce1d30f2..3d94ede6 100644 --- a/android/app/src/main/res/layout/item_purchase_order.xml +++ b/android/app/src/main/res/layout/item_purchase_order.xml @@ -27,19 +27,6 @@ android:textSize="18sp" android:textStyle="bold" /> - - Date: Wed, 15 Apr 2026 01:52:35 -0600 Subject: [PATCH 5/7] turned logs to laymen terms and added to android --- .../adapters/ActivityLogAdapter.java | 83 ++++++++-- .../petstoremobile/api/ActivityLogApi.java | 4 +- .../listfragments/ActivityLogFragment.java | 33 ++++ .../repositories/ActivityLogRepository.java | 4 +- .../viewmodels/ActivityLogListViewModel.java | 11 +- .../main/res/layout/fragment_activity_log.xml | 52 ++++++ .../src/main/res/layout/item_activity_log.xml | 52 ++++-- .../backend/config/ActivityLoggingFilter.java | 151 +++++++++++++++--- .../backend/controller/AuthController.java | 2 +- 9 files changed, 343 insertions(+), 49 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ActivityLogAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ActivityLogAdapter.java index 2c61551b..eb571ca2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ActivityLogAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ActivityLogAdapter.java @@ -10,11 +10,19 @@ import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; import com.example.petstoremobile.dtos.ActivityLogDTO; -import com.example.petstoremobile.utils.DateTimeUtils; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; +import java.util.Locale; public class ActivityLogAdapter extends RecyclerView.Adapter { + + private static final String SEPARATOR = " | "; + private static final SimpleDateFormat INPUT_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); + private static final SimpleDateFormat OUTPUT_FORMAT = new SimpleDateFormat("MMM d, HH:mm", Locale.getDefault()); + private final List items; public ActivityLogAdapter(List items) { @@ -32,26 +40,77 @@ public class ActivityLogAdapter extends RecyclerView.Adapter= 16) ? timestamp.substring(11, 16) : null; - holder.tvTimestamp.setText(date != null && time != null ? date + " " + time : date); + + String activity = log.getActivity() != null ? log.getActivity() : ""; + int separatorIndex = activity.indexOf(SEPARATOR); + if (separatorIndex >= 0) { + holder.tvActivity.setText(activity.substring(0, separatorIndex).trim()); + holder.tvTechnical.setText(activity.substring(separatorIndex + SEPARATOR.length()).trim()); + holder.tvTechnical.setVisibility(View.VISIBLE); + } else { + holder.tvActivity.setText(activity); + holder.tvTechnical.setVisibility(View.GONE); + } + + String fullName = firstNonBlank(log.getFullName(), log.getFullNameSnapshot(), "Unknown"); + String username = firstNonBlank(log.getUsername(), log.getUsernameSnapshot(), ""); + holder.tvUser.setText(username.isEmpty() ? fullName : fullName + " (" + username + ")"); + + String role = firstNonBlank(log.getRole(), log.getRoleSnapshot(), ""); + String store = firstNonBlank(log.getStoreName(), log.getStoreNameSnapshot(), ""); + if (!role.isEmpty() && !store.isEmpty()) { + holder.tvMeta.setText(store + " Β· " + formatRole(role)); + } else if (!role.isEmpty()) { + holder.tvMeta.setText(formatRole(role)); + } else if (!store.isEmpty()) { + holder.tvMeta.setText(store); + } else { + holder.tvMeta.setText(""); + } + + holder.tvTimestamp.setText(formatTimestamp(log.getLogTimestamp())); } @Override public int getItemCount() { return items.size(); } + private String formatTimestamp(String raw) { + if (raw == null) return ""; + try { + String normalized = raw.length() > 19 ? raw.substring(0, 19) : raw; + Date date = INPUT_FORMAT.parse(normalized); + return date != null ? OUTPUT_FORMAT.format(date) : raw.substring(0, Math.min(16, raw.length())).replace("T", " "); + } catch (ParseException e) { + return raw.length() >= 16 ? raw.substring(0, 16).replace("T", " ") : raw; + } + } + + private String formatRole(String role) { + if (role == null) return ""; + switch (role.toUpperCase(Locale.ROOT)) { + case "ADMIN": return "Admin"; + case "STAFF": return "Staff"; + case "CUSTOMER": return "Customer"; + default: return role; + } + } + + private String firstNonBlank(String... values) { + for (String v : values) { + if (v != null && !v.isBlank()) return v; + } + return ""; + } + public static class ViewHolder extends RecyclerView.ViewHolder { - TextView tvActivity, tvUser, tvMeta, tvTimestamp; + TextView tvActivity, tvTechnical, tvUser, tvMeta, tvTimestamp; public ViewHolder(@NonNull View itemView) { super(itemView); - tvActivity = itemView.findViewById(R.id.tvLogActivity); - tvUser = itemView.findViewById(R.id.tvLogUser); - tvMeta = itemView.findViewById(R.id.tvLogMeta); + tvActivity = itemView.findViewById(R.id.tvLogActivity); + tvTechnical = itemView.findViewById(R.id.tvLogTechnical); + tvUser = itemView.findViewById(R.id.tvLogUser); + tvMeta = itemView.findViewById(R.id.tvLogMeta); tvTimestamp = itemView.findViewById(R.id.tvLogTimestamp); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ActivityLogApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ActivityLogApi.java index d6b7e2ae..12ceb5bd 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ActivityLogApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ActivityLogApi.java @@ -15,6 +15,8 @@ public interface ActivityLogApi { @Query("limit") int limit, @Query("storeId") Long storeId, @Query("role") String role, - @Query("search") String search + @Query("search") String search, + @Query("startDate") String startDate, + @Query("endDate") String endDate ); } 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 03a90fe3..f369d383 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 @@ -1,5 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; +import android.app.DatePickerDialog; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -28,6 +29,7 @@ import com.example.petstoremobile.viewmodels.ActivityLogListViewModel; import javax.inject.Inject; import java.util.ArrayList; +import java.util.Calendar; import java.util.List; import dagger.hilt.android.AndroidEntryPoint; @@ -39,6 +41,8 @@ public class ActivityLogFragment extends Fragment { private ActivityLogAdapter adapter; private final List logList = new ArrayList<>(); private List storeList = new ArrayList<>(); + private String selectedStartDate = null; + private String selectedEndDate = null; @Inject TokenManager tokenManager; @@ -106,6 +110,35 @@ public class ActivityLogFragment extends Fragment { ? binding.spinnerRoleFilter.getSelectedItem().toString() : "All Roles")); SpinnerUtils.setupFilterSpinner(binding.spinnerStoreFilter, this::onStoreSelected); + + binding.btnStartDate.setOnClickListener(v -> showDatePicker(true)); + binding.btnEndDate.setOnClickListener(v -> showDatePicker(false)); + binding.btnClearDates.setOnClickListener(v -> { + selectedStartDate = null; + selectedEndDate = null; + binding.btnStartDate.setText("Start Date"); + binding.btnEndDate.setText("End Date"); + binding.btnClearDates.setVisibility(View.GONE); + viewModel.setDateRange(null, null); + }); + } + + private void showDatePicker(boolean isStart) { + Calendar cal = Calendar.getInstance(); + new DatePickerDialog(requireContext(), (view, year, month, day) -> { + String date = String.format("%04d-%02d-%02d", year, month + 1, day); + String label = String.format("%02d/%02d/%04d", day, month + 1, year); + if (isStart) { + selectedStartDate = date; + binding.btnStartDate.setText(label); + } else { + selectedEndDate = date; + binding.btnEndDate.setText(label); + } + binding.btnClearDates.setVisibility( + selectedStartDate != null || selectedEndDate != null ? View.VISIBLE : View.GONE); + viewModel.setDateRange(selectedStartDate, selectedEndDate); + }, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show(); } private void onStoreSelected() { diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ActivityLogRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ActivityLogRepository.java index 866ea2ad..6c8acddf 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ActivityLogRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ActivityLogRepository.java @@ -21,7 +21,7 @@ public class ActivityLogRepository extends BaseRepository { this.activityLogApi = activityLogApi; } - public LiveData>> getActivityLogs(int limit, Long storeId, String role, String search) { - return executeCall(activityLogApi.getActivityLogs(limit, storeId, role, search)); + public LiveData>> getActivityLogs(int limit, Long storeId, String role, String search, String startDate, String endDate) { + return executeCall(activityLogApi.getActivityLogs(limit, storeId, role, search, startDate, endDate)); } } 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 ff7ff71c..d944de83 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 @@ -32,6 +32,8 @@ public class ActivityLogListViewModel extends ViewModel { private Long currentStoreId = null; private String currentRole = null; private String currentSearch = null; + private String currentStartDate = null; + private String currentEndDate = null; private int currentLimit = PAGE_SIZE; private boolean isLastPage = false; @@ -71,7 +73,7 @@ public class ActivityLogListViewModel extends ViewModel { if (isLastPage) return; isLoading.setValue(true); - observeOnce(repository.getActivityLogs(currentLimit, currentStoreId, currentRole, currentSearch), resource -> { + observeOnce(repository.getActivityLogs(currentLimit, currentStoreId, currentRole, currentSearch, currentStartDate, currentEndDate), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { List result = resource.data; @@ -101,6 +103,13 @@ public class ActivityLogListViewModel extends ViewModel { loadLogs(true); } + public void setDateRange(String startDate, String endDate) { + currentStartDate = startDate; + currentEndDate = endDate; + loadLogs(true); + } + + private void observeOnce(LiveData> liveData, Observer> handler) { liveData.observeForever(new Observer>() { @Override diff --git a/android/app/src/main/res/layout/fragment_activity_log.xml b/android/app/src/main/res/layout/fragment_activity_log.xml index b81dcea7..35923546 100644 --- a/android/app/src/main/res/layout/fragment_activity_log.xml +++ b/android/app/src/main/res/layout/fragment_activity_log.xml @@ -88,6 +88,57 @@ + + + + -