diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 253cb04b..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(grep -E \"\\\\.\\(tsx?|jsx?\\)$\")", - "Bash(grep -E \"\\\\.js$|^d\")" - ] - } -} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..ed803dd4 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,73 @@ +name: Build and Deploy + +on: + push: + branches: [main, azure-deploy] + +env: + REGISTRY: ghcr.io + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set image names (lowercase) + run: | + OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + echo "BACKEND_IMAGE=ghcr.io/${OWNER}/petshop-backend" >> $GITHUB_ENV + echo "FRONTEND_IMAGE=ghcr.io/${OWNER}/petshop-web" >> $GITHUB_ENV + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push backend image + uses: docker/build-push-action@v5 + with: + context: ./backend + push: true + tags: ${{ env.BACKEND_IMAGE }}:latest + + - name: Build and push frontend image + uses: docker/build-push-action@v5 + with: + context: ./web + push: true + tags: ${{ env.FRONTEND_IMAGE }}:latest + build-args: | + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }} + + - name: Log in to Azure + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Deploy backend + run: | + az containerapp update \ + --name ${{ secrets.AZURE_BACKEND_APP_NAME }} \ + --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ + --image ${{ env.BACKEND_IMAGE }}:latest \ + --registry-server ${{ env.REGISTRY }} \ + --registry-username ${{ github.actor }} \ + --registry-password ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy frontend + run: | + az containerapp update \ + --name ${{ secrets.AZURE_FRONTEND_APP_NAME }} \ + --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ + --image ${{ env.FRONTEND_IMAGE }}:latest \ + --registry-server ${{ env.REGISTRY }} \ + --registry-username ${{ github.actor }} \ + --registry-password ${{ secrets.GITHUB_TOKEN }} diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml new file mode 100644 index 00000000..5022297f --- /dev/null +++ b/.idea/caches/deviceStreaming.xml @@ -0,0 +1,1570 @@ + + + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 00000000..c61ea334 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index caaf8e17..146d8d64 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -16,9 +16,9 @@ val localProperties = Properties().apply { fun quoted(value: String): String = "\"$value\"" val emulatorBackendUrl = - (localProperties.getProperty("petstore.backend.emulatorUrl") ?: "http://10.0.2.2:8080/").trim() + (localProperties.getProperty("petstore.backend.emulatorUrl") ?: "https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io/").trim() val deviceBackendUrl = - (localProperties.getProperty("petstore.backend.deviceUrl") ?: "http://10.0.0.200:8080/").trim() + (localProperties.getProperty("petstore.backend.deviceUrl") ?: "https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io/").trim() android { namespace = "com.example.petstoremobile" diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java index 385444ed..82ea9934 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java @@ -1,6 +1,7 @@ package com.example.petstoremobile.activities; import android.os.Bundle; +import android.view.View; import android.widget.Toast; import androidx.activity.EdgeToEdge; @@ -9,14 +10,26 @@ import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.databinding.ActivityForgotPasswordBinding; import com.example.petstoremobile.utils.InputValidator; +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; + import dagger.hilt.android.AndroidEntryPoint; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; @AndroidEntryPoint public class ForgotPasswordActivity extends AppCompatActivity { + @Inject + AuthApi authApi; + private ActivityForgotPasswordBinding binding; @Override @@ -33,14 +46,39 @@ public class ForgotPasswordActivity extends AppCompatActivity { }); binding.btnSubmit.setOnClickListener(v -> { - if (InputValidator.isValidEmail(binding.etEmail)) { - String email = binding.etEmail.getText().toString().trim(); - // TODO: Implement password reset logic here - Toast.makeText(this, "If this email is linked, a reset email will be sent.", Toast.LENGTH_LONG).show(); - finish(); - } + if (!InputValidator.isValidEmail(binding.etEmail)) return; + String email = binding.etEmail.getText().toString().trim(); + sendResetLink(email); }); binding.btnBackToLogin.setOnClickListener(v -> finish()); } + + private void sendResetLink(String email) { + binding.btnSubmit.setEnabled(false); + + Map body = new HashMap<>(); + body.put("usernameOrEmail", email); + + authApi.forgotPassword(body).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (binding == null) return; + binding.btnSubmit.setEnabled(true); + Toast.makeText(ForgotPasswordActivity.this, + "If this email is registered, a reset link will be sent.", + Toast.LENGTH_LONG).show(); + finish(); + } + + @Override + public void onFailure(Call call, Throwable t) { + if (binding == null) return; + binding.btnSubmit.setEnabled(true); + Toast.makeText(ForgotPasswordActivity.this, + "Could not send reset link. Please try again.", + Toast.LENGTH_LONG).show(); + } + }); + } } 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/adapters/MessageAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java index 0c3a51a0..052f34cb 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java @@ -11,11 +11,15 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.LazyHeaders; +import com.bumptech.glide.signature.ObjectKey; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.ItemMessageReceivedBinding; import com.example.petstoremobile.databinding.ItemMessageSentBinding; import com.example.petstoremobile.models.Message; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; +import java.util.Locale; public class MessageAdapter extends RecyclerView.Adapter { @@ -28,6 +32,7 @@ public class MessageAdapter extends RecyclerView.Adapter messages; private Long currentUserId; + private Long staffId; private String token; private String baseUrl; private OnAttachmentClickListener attachmentClickListener; @@ -35,6 +40,13 @@ public class MessageAdapter extends RecyclerView.Adapter messages, Long currentUserId) { this.messages = messages; this.currentUserId = currentUserId; + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + Message m = messages.get(position); + return m.getId() != null ? m.getId() : position; } public void setCurrentUserId(Long id) { @@ -42,6 +54,11 @@ public class MessageAdapter extends RecyclerView.Adapter { if (listener != null) listener.onAttachmentClick(m); }; @@ -116,8 +134,10 @@ public class MessageAdapter extends RecyclerView.Adapter { @@ -136,7 +155,29 @@ public class MessageAdapter extends RecyclerView.Adapter 19 ? timestamp.substring(0, 19) : timestamp; + SimpleDateFormat input = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); + Date date = input.parse(normalized); + return new SimpleDateFormat("MMM d, HH:mm", Locale.getDefault()).format(date); + } catch (Exception e) { + return ""; + } + } + private static void displayAttachment(Message m, ImageView iv, TextView tvName, String token, String baseUrl) { // Check if there's an attachment by looking at name or mime type if (m.getAttachmentName() != null || m.getAttachmentMimeType() != null) { @@ -150,6 +191,7 @@ public class MessageAdapter extends RecyclerView.Adapter listener.onPurchaseOrderClick(position)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java index deca5624..a7b6c2bb 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java @@ -48,6 +48,12 @@ public class SaleAdapter extends RecyclerView.Adapter deleteAvatar(); + //forgot password endpoint + @POST("api/v1/auth/forgot-password") + Call forgotPassword(@Body Map body); + } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java index a75d7812..8302ba86 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java @@ -34,6 +34,12 @@ public class MessageDTO { @SerializedName("attachmentSizeBytes") private Long attachmentSizeBytes; + @SerializedName("senderRole") + private String senderRole; + + @SerializedName("senderDisplayName") + private String senderDisplayName; + public MessageDTO() {} public Long getId() { return id; } @@ -65,4 +71,10 @@ public class MessageDTO { public Long getAttachmentSizeBytes() { return attachmentSizeBytes; } public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; } + + public String getSenderRole() { return senderRole; } + public void setSenderRole(String senderRole) { this.senderRole = senderRole; } + + public String getSenderDisplayName() { return senderDisplayName; } + public void setSenderDisplayName(String senderDisplayName) { this.senderDisplayName = senderDisplayName; } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index c9171db2..88bb0f09 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -182,6 +182,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis LinearLayoutManager lm = new LinearLayoutManager(getContext()); lm.setStackFromEnd(true); binding.rvMessages.setLayoutManager(lm); + binding.rvMessages.setItemAnimator(null); binding.rvMessages.setAdapter(messageAdapter); setConversationActive(false, null); } @@ -285,9 +286,14 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> { + int prevSize = messageList.size(); messageList.clear(); messageList.addAll(list); - messageAdapter.notifyDataSetChanged(); + if (prevSize > 0 && list.size() == prevSize + 1) { + messageAdapter.notifyItemInserted(list.size() - 1); + } else { + messageAdapter.notifyDataSetChanged(); + } scrollToBottom(); }); @@ -356,6 +362,8 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis binding.tvChatTitle.setText(chat.getCustomerName()); binding.chatDrawerLayout.closeDrawer(GravityCompat.START); + messageAdapter.setStaffId(chat.getStaffId()); + if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId); viewModel.loadMessageHistory(activeConversationId); } @@ -464,9 +472,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis public void onMessageReceived(MessageDTO dto) { requireActivity().runOnUiThread(() -> { if (activeConversationId != null && activeConversationId.equals(dto.getConversationId())) { - if (!tokenManager.getUserId().equals(dto.getSenderId())) { - viewModel.addMessageLocally(dto); - } + viewModel.addMessageLocally(dto); } viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), "")); }); 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..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; @@ -10,6 +11,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; @@ -27,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; @@ -38,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; @@ -74,6 +79,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() { @@ -89,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/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index cfc05233..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", "Cancelled"}; - SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions); + String[] statuses = {"All Statuses", "Completed", "Pending", "Missed", "Cancelled"}; + 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/AnalyticsFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java index b8656b60..89aeb098 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java @@ -7,11 +7,13 @@ import android.widget.*; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentAnalyticsBinding; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.AnalyticsViewModel; import dagger.hilt.android.AndroidEntryPoint; +import javax.inject.Inject; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; @@ -19,6 +21,9 @@ import java.util.*; @AndroidEntryPoint public class AnalyticsFragment extends Fragment { + @Inject + TokenManager tokenManager; + private FragmentAnalyticsBinding binding; private AnalyticsViewModel viewModel; private boolean filtersExpanded = false; @@ -33,6 +38,7 @@ public class AnalyticsFragment extends Fragment { viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class); setupFilterPanel(); + setupViewModeToggle(); observeViewModel(); viewModel.loadAnalytics(); @@ -42,6 +48,39 @@ public class AnalyticsFragment extends Fragment { return binding.getRoot(); } + private static final int COLOR_SELECTED = 0xFF4ECDC4; + private static final int COLOR_UNSELECTED = 0xFFCBD5E1; + + private void setupViewModeToggle() { + updateViewModeButtonStyles(viewModel.getViewMode()); + + binding.btnMyAnalytics.setOnClickListener(v -> { + viewModel.setViewMode("mine"); + updateViewModeButtonStyles("mine"); + updateStoreFilterVisibility("mine"); + }); + + binding.btnStoreAnalytics.setOnClickListener(v -> { + viewModel.setViewMode("store"); + updateViewModeButtonStyles("store"); + updateStoreFilterVisibility("store"); + }); + } + + private void updateViewModeButtonStyles(String mode) { + binding.btnMyAnalytics.setBackgroundTintList( + android.content.res.ColorStateList.valueOf(mode.equals("mine") ? COLOR_SELECTED : COLOR_UNSELECTED)); + binding.btnStoreAnalytics.setBackgroundTintList( + android.content.res.ColorStateList.valueOf(mode.equals("store") ? COLOR_SELECTED : COLOR_UNSELECTED)); + } + + private void updateStoreFilterVisibility(String mode) { + boolean isAdmin = "ADMIN".equalsIgnoreCase(tokenManager.getRole()); + int vis = (isAdmin && mode.equals("store")) ? View.VISIBLE : View.GONE; + binding.tvStoreFilterLabel.setVisibility(vis); + binding.spinnerFilterStore.setVisibility(vis); + } + // Filter Panel private void setupFilterPanel() { @@ -96,6 +135,9 @@ public class AnalyticsFragment extends Fragment { int topNPos = binding.spinnerTopN.getSelectedItemPosition(); filter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5; + Object store = binding.spinnerFilterStore.getSelectedItem(); + viewModel.setStoreFilter(store != null ? store.toString() : "All Stores"); + updateFilterSummary(); viewModel.applyFilter(filter); } @@ -104,8 +146,8 @@ public class AnalyticsFragment extends Fragment { binding.etFilterStartDate.setText(""); binding.etFilterEndDate.setText(""); binding.spinnerTopN.setSelection(0); - // Reset payment method to "All" SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, "All"); + SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, "All Stores"); updateFilterSummary(); viewModel.resetFilter(); } @@ -162,6 +204,16 @@ public class AnalyticsFragment extends Fragment { methods.toArray(new String[0])); SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, currentSelection); }); + + viewModel.getAvailableStores().observe(getViewLifecycleOwner(), stores -> { + if (stores == null || stores.isEmpty()) return; + String currentSelection = binding.spinnerFilterStore.getSelectedItem() != null + ? binding.spinnerFilterStore.getSelectedItem().toString() : "All Stores"; + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerFilterStore, + stores.toArray(new String[0])); + SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, currentSelection); + updateStoreFilterVisibility(viewModel.getViewMode()); + }); } @Override @@ -224,17 +276,22 @@ public class AnalyticsFragment extends Fragment { } // Employee Performance - binding.llEmployeePerformance.removeAllViews(); - if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) { - BigDecimal maxEmp = data.employeePerformance.get(0).getValue(); - if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; - for (Map.Entry e : data.employeePerformance) { - addBarRow(binding.llEmployeePerformance, e.getKey(), - "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f"); + boolean showEmployeeSection = viewModel.getViewMode().equals("store"); + View empParent = (View) binding.llEmployeePerformance.getParent(); + if (empParent != null) empParent.setVisibility(showEmployeeSection ? View.VISIBLE : View.GONE); + if (showEmployeeSection) { + binding.llEmployeePerformance.removeAllViews(); + if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) { + BigDecimal maxEmp = data.employeePerformance.get(0).getValue(); + if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; + for (Map.Entry e : data.employeePerformance) { + addBarRow(binding.llEmployeePerformance, e.getKey(), + "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f"); + } + } else { + addEmptyRow(binding.llEmployeePerformance, "No data"); } - } else { - addEmptyRow(binding.llEmployeePerformance, "No data"); } // Daily Revenue 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/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index 5c6589f3..f6d0c388 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -17,6 +17,7 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SaleAdapter; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentSaleBinding; +import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.SpinnerUtils; @@ -57,10 +58,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis setupStoreFilter(); setupPaymentMethodFilter(); setupRefundStatusFilter(); + setupCustomerFilter(); setupSwipeRefresh(); setupFilterToggle(); observeViewModel(); - + loadSales(true); UIUtils.setupHamburgerMenu(binding.btnHamburger, this); @@ -89,6 +91,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis StoreDTO::getStoreName, "All Stores", null, StoreDTO::getStoreId); }); + viewModel.getCustomers().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCustomer, list, + CustomerDTO::getFullName, "All Customers", null, CustomerDTO::getCustomerId); + }); + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { binding.swipeRefreshSale.setRefreshing(loading); }); @@ -98,16 +105,17 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis public void onResume() { super.onResume(); if (!isStaff()) viewModel.loadStores(); + viewModel.loadCustomers(); } private void setupFilterToggle() { if (isStaff()) { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale, - binding.spinnerPaymentMethod, binding.spinnerRefundStatus); + binding.spinnerPaymentMethod, binding.spinnerRefundStatus, binding.spinnerCustomer); binding.spinnerStore.setVisibility(View.GONE); } else { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale, - binding.spinnerPaymentMethod, binding.spinnerStore, binding.spinnerRefundStatus); + binding.spinnerPaymentMethod, binding.spinnerStore, binding.spinnerRefundStatus, binding.spinnerCustomer); } } @@ -133,6 +141,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerRefundStatus, refundStatuses, () -> loadSales(true)); } + private void setupCustomerFilter() { + SpinnerUtils.setupFilterSpinner(binding.spinnerCustomer, () -> loadSales(true)); + } + private void setupRecyclerView() { adapter = new SaleAdapter(saleList, this); binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); @@ -189,7 +201,13 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis isRefund = binding.spinnerRefundStatus.getSelectedItemPosition() == 2; } - viewModel.loadSales(reset, query, paymentMethod, storeId, isRefund); + Long customerId = null; + List customerList = viewModel.getCustomers().getValue(); + if (binding.spinnerCustomer.getSelectedItemPosition() > 0 && customerList != null && !customerList.isEmpty()) { + customerId = customerList.get(binding.spinnerCustomer.getSelectedItemPosition() - 1).getCustomerId(); + } + + viewModel.loadSales(reset, query, paymentMethod, storeId, isRefund, customerId); } @Override 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/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index 770c6b82..6abb918c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -114,9 +114,18 @@ public class ProductSupplierDetailFragment extends Fragment { viewModel.setEditMode(productId, supplierId); preselectedProductId = productId; preselectedSupplierId = supplierId; - + binding.tvPSMode.setText("Edit Product Supplier"); binding.btnDeletePS.setVisibility(View.VISIBLE); + + viewModel.loadProductSupplier().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + if (resource.data.getCost() != null) { + binding.etPSCost.setText(resource.data.getCost().toPlainString()); + } + } + }); } else { binding.tvPSMode.setText("Add Product Supplier"); binding.btnDeletePS.setVisibility(View.GONE); 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/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java index 8a4dff16..4963b272 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java @@ -111,6 +111,7 @@ public class SaleDetailFragment extends Fragment { binding.llLoyaltyPoints.setVisibility(View.GONE); binding.cbUseLoyaltyPoints.setChecked(false); } + updateTotal(); }); } @@ -420,6 +421,15 @@ public class SaleDetailFragment extends Fragment { } binding.tvSaleDetailTotal.setText("Total: $" + String.format(Locale.getDefault(), "%.2f", total)); + + CustomerDTO customer = viewModel.getSelectedCustomerData().getValue(); + if (customer != null && !viewModel.isViewOnly()) { + int pointsToEarn = total.max(BigDecimal.ZERO).intValue(); + binding.tvPointsToEarn.setText("+" + pointsToEarn + " pts"); + binding.llPointsToEarn.setVisibility(View.VISIBLE); + } else { + binding.llPointsToEarn.setVisibility(View.GONE); + } } private void saveSale() { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java index 94b2b307..9bb3435d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java @@ -28,8 +28,6 @@ public class StaffDetailFragment extends Fragment { private FragmentStaffDetailBinding binding; private StaffDetailViewModel viewModel; - private final String[] ROLES = {"STAFF", "ADMIN"}; - private final String[] STAFF_ROLES = {"STORE_MANAGER", "SALES_ASSOCIATE", "GROOMER", "VETERINARIAN"}; private final String[] STATUSES = {"Active", "Inactive"}; private long preselectedStoreId = -1; @@ -59,8 +57,6 @@ public class StaffDetailFragment extends Fragment { } private void setupSpinners() { - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffRole, ROLES); - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffType, STAFF_ROLES); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES); } @@ -112,8 +108,6 @@ public class StaffDetailFragment extends Fragment { binding.etStaffEmail.setText(e.getEmail()); binding.etStaffPhone.setText(e.getPhone()); - SpinnerUtils.setSelectionByValue(binding.spinnerStaffRole, e.getRole()); - SpinnerUtils.setSelectionByValue(binding.spinnerStaffType, e.getStaffRole()); binding.spinnerStaffStatus.setSelection(Boolean.TRUE.equals(e.getActive()) ? 0 : 1); preselectedStoreId = e.getPrimaryStoreId() != null ? e.getPrimaryStoreId() : -1; @@ -131,17 +125,35 @@ public class StaffDetailFragment extends Fragment { private void save() { if (!InputValidator.isNotEmpty(binding.etStaffUsername, "Username")) return; - + + String password = binding.etStaffPassword.getText().toString().trim(); + String confirmPassword = binding.etStaffConfirmPassword.getText().toString().trim(); + if (!viewModel.isEditing()) { if (!InputValidator.isNotEmpty(binding.etStaffPassword, "Password")) return; - String pass = binding.etStaffPassword.getText().toString(); - if (pass.length() < 6) { + if (password.length() < 6) { binding.etStaffPassword.setError("At least 6 characters"); binding.etStaffPassword.requestFocus(); return; } + if (!password.equals(confirmPassword)) { + binding.etStaffConfirmPassword.setError("Passwords do not match"); + binding.etStaffConfirmPassword.requestFocus(); + return; + } + } else if (!password.isEmpty()) { + if (password.length() < 6) { + binding.etStaffPassword.setError("At least 6 characters"); + binding.etStaffPassword.requestFocus(); + return; + } + if (!password.equals(confirmPassword)) { + binding.etStaffConfirmPassword.setError("Passwords do not match"); + binding.etStaffConfirmPassword.requestFocus(); + return; + } } - + if (!InputValidator.isNotEmpty(binding.etStaffFirstName, "First Name")) return; if (!InputValidator.isNotEmpty(binding.etStaffLastName, "Last Name")) return; if (!InputValidator.isValidEmail(binding.etStaffEmail)) return; @@ -149,13 +161,12 @@ public class StaffDetailFragment extends Fragment { if (!InputValidator.isSpinnerSelected(binding.spinnerStaffStore, "Primary Store")) return; String username = binding.etStaffUsername.getText().toString().trim(); - String password = binding.etStaffPassword.getText().toString().trim(); String firstName = binding.etStaffFirstName.getText().toString().trim(); String lastName = binding.etStaffLastName.getText().toString().trim(); String email = binding.etStaffEmail.getText().toString().trim(); String phone = binding.etStaffPhone.getText().toString().trim(); - String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()]; - String staffRole = STAFF_ROLES[binding.spinnerStaffType.getSelectedItemPosition()]; + String role = "STAFF"; + String staffRole = null; boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0; List stores = viewModel.getStoreList().getValue(); diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Message.java b/android/app/src/main/java/com/example/petstoremobile/models/Message.java index 082baa58..e6547d23 100644 --- a/android/app/src/main/java/com/example/petstoremobile/models/Message.java +++ b/android/app/src/main/java/com/example/petstoremobile/models/Message.java @@ -11,6 +11,8 @@ public class Message { private String attachmentName; private String attachmentMimeType; private Long attachmentSizeBytes; + private String senderRole; + private String senderDisplayName; public Message() {} @@ -49,4 +51,10 @@ public class Message { public Long getAttachmentSizeBytes() { return attachmentSizeBytes; } public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; } + + public String getSenderRole() { return senderRole; } + public void setSenderRole(String senderRole) { this.senderRole = senderRole; } + + public String getSenderDisplayName() { return senderDisplayName; } + public void setSenderDisplayName(String senderDisplayName) { this.senderDisplayName = senderDisplayName; } } \ No newline at end of file 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/repositories/SaleRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java index 36ac8b30..182a7f0e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java @@ -20,8 +20,8 @@ public class SaleRepository extends BaseRepository { this.saleApi = saleApi; } - public LiveData>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, Boolean isRefund, String sortBy) { - return executeCall(saleApi.getAllSales(page, size, query, paymentMethod, storeId, isRefund, sortBy)); + public LiveData>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, Boolean isRefund, Long customerId, String sortBy) { + return executeCall(saleApi.getAllSales(page, size, query, paymentMethod, storeId, isRefund, customerId, sortBy)); } public LiveData> getSaleById(Long id) { diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java index 0d45ad12..9ad2d5f4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java @@ -100,13 +100,12 @@ public class InputValidator { return true; } - // Checks if the phone number is valid in (XXX) XXX-XXXX format + // Checks if the phone number is valid: XXX-XXX-XXXX, (XXX) XXX-XXXX, or XXXXXXXXXX public static boolean isValidPhone(EditText field) { String phone = field.getText().toString().trim(); - // Matches (XXX) XXX-XXXX format - String pattern = "^\\(\\d{3}\\) \\d{3}-\\d{4}$"; + String pattern = "^(\\(\\d{3}\\) \\d{3}-\\d{4}|\\d{3}-\\d{3}-\\d{4}|\\d{10})$"; if (phone.isEmpty() || !phone.matches(pattern)) { - field.setError("Enter a valid phone number: (XXX) XXX-XXXX"); + field.setError("Enter a valid phone number"); field.requestFocus(); return 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..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 @@ -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; @@ -32,6 +32,11 @@ 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; @Inject public ActivityLogListViewModel(ActivityLogRepository repository, StoreRepository storeRepository) { @@ -42,10 +47,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 +62,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, currentStartDate, currentEndDate), 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,19 +90,26 @@ 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); } + 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/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/AnalyticsViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java index 3e5082ec..4a9b94d9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.repositories.SaleRepository; import com.example.petstoremobile.utils.Resource; @@ -21,6 +22,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TreeMap; +import java.util.stream.Collectors; import javax.inject.Inject; @@ -29,33 +31,40 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class AnalyticsViewModel extends ViewModel { private final SaleRepository saleRepository; + private final TokenManager tokenManager; private final MutableLiveData analyticsData = new MutableLiveData<>(); private final MutableLiveData isLoading = new MutableLiveData<>(false); private final MutableLiveData errorMessage = new MutableLiveData<>(); private final MutableLiveData> availablePaymentMethods = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> availableStores = new MutableLiveData<>(new ArrayList<>()); private List cachedSales = new ArrayList<>(); private FilterState currentFilter = new FilterState(); + private String viewMode = "store"; + private String storeFilter = "All Stores"; @Inject - public AnalyticsViewModel(SaleRepository saleRepository) { + public AnalyticsViewModel(SaleRepository saleRepository, TokenManager tokenManager) { this.saleRepository = saleRepository; + this.tokenManager = tokenManager; } public LiveData getAnalyticsData() { return analyticsData; } public LiveData getIsLoading() { return isLoading; } public LiveData getErrorMessage() { return errorMessage; } public LiveData> getAvailablePaymentMethods() { return availablePaymentMethods; } + public LiveData> getAvailableStores() { return availableStores; } public void loadAnalytics() { isLoading.setValue(true); errorMessage.setValue(null); - observeOnce(saleRepository.getAllSales(0, 2000, null, null, null, null, "saleDate,desc"), resource -> { + observeOnce(saleRepository.getAllSales(0, 2000, null, null, null, null, null, "saleDate,desc"), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { cachedSales = resource.data.getContent(); derivePaymentMethods(); + deriveStores(); applyCurrentFilter(); isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { @@ -73,14 +82,61 @@ public class AnalyticsViewModel extends ViewModel { public void resetFilter() { currentFilter = new FilterState(); + storeFilter = "All Stores"; applyCurrentFilter(); } + public void setViewMode(String mode) { + viewMode = mode; + applyCurrentFilter(); + } + + public String getViewMode() { + return viewMode; + } + + public void setStoreFilter(String store) { + storeFilter = (store != null && !store.isEmpty()) ? store : "All Stores"; + applyCurrentFilter(); + } + + public String getStoreFilter() { + return storeFilter; + } + private void applyCurrentFilter() { - List filtered = filterSales(cachedSales, currentFilter); + List salesForMode; + if (viewMode.equals("mine")) { + String currentUser = tokenManager.getUsername(); + salesForMode = cachedSales.stream() + .filter(s -> currentUser != null && currentUser.equalsIgnoreCase(s.getEmployeeName() != null ? s.getEmployeeName() : "")) + .collect(Collectors.toList()); + } else { + salesForMode = cachedSales; + } + if (!storeFilter.equals("All Stores") && !storeFilter.isEmpty()) { + final String sf = storeFilter; + salesForMode = salesForMode.stream() + .filter(s -> sf.equalsIgnoreCase(s.getStoreName() != null ? s.getStoreName() : "")) + .collect(Collectors.toList()); + } + List filtered = filterSales(salesForMode, currentFilter); computeAnalytics(filtered, currentFilter); } + private void deriveStores() { + java.util.Set stores = new java.util.TreeSet<>(); + for (SaleDTO s : cachedSales) { + if (s.getStoreName() != null && !s.getStoreName().isEmpty()) { + stores.add(s.getStoreName()); + } + } + List result = new ArrayList<>(); + result.add("All Stores"); + result.addAll(stores); + availableStores.setValue(result); + } + private void derivePaymentMethods() { java.util.Set methods = new java.util.TreeSet<>(); for (SaleDTO s : cachedSales) { 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/ChatListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java index 88123907..4d14a3b4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java @@ -133,6 +133,11 @@ public class ChatListViewModel extends ViewModel { public void addMessageLocally(MessageDTO dto) { List current = new ArrayList<>(messageList.getValue()); + if (dto.getId() != null) { + for (Message m : current) { + if (dto.getId().equals(m.getId())) return; + } + } current.add(dtoToModel(dto)); messageList.setValue(current); } @@ -168,6 +173,8 @@ public class ChatListViewModel extends ViewModel { m.setIsRead(dto.getIsRead()); m.setAttachmentUrl(dto.getAttachmentUrl()); m.setAttachmentName(dto.getAttachmentName()); + m.setSenderRole(dto.getSenderRole()); + m.setSenderDisplayName(dto.getSenderDisplayName()); m.setAttachmentMimeType(dto.getAttachmentMimeType()); m.setAttachmentSizeBytes(dto.getAttachmentSizeBytes()); return m; 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/ProductSupplierDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java index 552a99fc..2cbf5036 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java @@ -46,6 +46,10 @@ public class ProductSupplierDetailViewModel extends ViewModel { this.editSupplierId = supplierId; } + public LiveData> loadProductSupplier() { + return psRepository.getProductSupplierById(editProductId, editSupplierId); + } + public boolean isEditing() { return isEditing; } public long getEditProductId() { return editProductId; } public long getEditSupplierId() { return editSupplierId; } 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/RefundViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java index fab83dd9..ff3d069e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java @@ -35,7 +35,7 @@ public class RefundViewModel extends ViewModel { } public LiveData>> loadAllSales() { - return saleRepository.getAllSales(0, 1000, null, null, null, null, "saleDate,desc"); + return saleRepository.getAllSales(0, 1000, null, null, null, null, null, "saleDate,desc"); } public void setAllSales(List sales) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java index 7df7269a..aee0f948 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java @@ -4,9 +4,11 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.CustomerRepository; import com.example.petstoremobile.repositories.SaleRepository; import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.utils.Resource; @@ -24,27 +26,31 @@ import dagger.hilt.android.lifecycle.HiltViewModel; public class SaleListViewModel extends ViewModel { private final SaleRepository saleRepository; private final StoreRepository storeRepository; + private final CustomerRepository customerRepository; private final MutableLiveData> sales = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> customers = 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 SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository) { + public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository, CustomerRepository customerRepository) { this.saleRepository = saleRepository; this.storeRepository = storeRepository; + this.customerRepository = customerRepository; } public LiveData> getSales() { return sales; } public LiveData> getStores() { return stores; } + public LiveData> getCustomers() { return customers; } public LiveData getIsLoading() { return isLoading; } public boolean isLastPage() { return isLastPage; } - public void loadSales(boolean reset, String query, String paymentMethod, Long storeId, Boolean isRefund) { + public void loadSales(boolean reset, String query, String paymentMethod, Long storeId, Boolean isRefund, Long customerId) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; if (reset) { @@ -53,7 +59,7 @@ public class SaleListViewModel extends ViewModel { } isLoading.setValue(true); - observeOnce(saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, isRefund, "saleDate,desc"), resource -> { + observeOnce(saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, isRefund, customerId, "saleDate,desc"), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { List currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue()); @@ -77,6 +83,14 @@ public class SaleListViewModel extends ViewModel { }); } + public void loadCustomers() { + observeOnce(customerRepository.getAllCustomers(0, 500), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + customers.setValue(resource.data.getContent()); + } + }); + } + private void observeOnce(LiveData> liveData, Observer> handler) { liveData.observeForever(new Observer>() { @Override 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); diff --git a/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java b/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java index f0f5d7ec..3aad4b8d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java +++ b/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java @@ -95,6 +95,7 @@ public class StompChatManager { headers.put("Authorization", "Bearer " + authToken); stompClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, webSocketUrl, headers); + stompClient.withClientHeartbeat(0).withServerHeartbeat(0); compositeDisposable.add( stompClient.lifecycle() 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 @@ + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml index 7192ea69..674020aa 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml @@ -59,14 +59,26 @@ - + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/activity-log-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/activity-log-view.fxml index 7caa317a..afc632ac 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/activity-log-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/activity-log-view.fxml @@ -2,51 +2,80 @@ + + + + + + - + - + + - - diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml index c24826d3..e55b0e0c 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml @@ -2,6 +2,7 @@ + @@ -10,6 +11,7 @@ + @@ -71,8 +73,11 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +