From a8b5ee361eacac5c8cff4a002d0a841597dfd774 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:16:11 -0600 Subject: [PATCH] cleaning code --- .../fragments/ChatFragment.java | 29 +- .../fragments/ProfileFragment.java | 10 + .../listfragments/AnalyticsFragment.java | 3 +- .../AdoptionDetailFragment.java | 66 +- .../AppointmentDetailFragment.java | 52 +- .../InventoryDetailFragment.java | 73 +- .../detailfragments/PetDetailFragment.java | 75 +-- .../ProductDetailFragment.java | 43 +- .../ProductSupplierDetailFragment.java | 31 +- .../PurchaseOrderDetailFragment.java | 7 + .../detailfragments/RefundFragment.java | 88 +-- .../detailfragments/SaleDetailFragment.java | 42 +- .../ServiceDetailFragment.java | 14 +- .../detailfragments/StaffDetailFragment.java | 100 +-- .../SupplierDetailFragment.java | 11 + .../PetProfileFragment.java | 37 +- .../viewmodels/ChatListViewModel.java | 7 + .../viewmodels/PetDetailViewModel.java | 9 + .../viewmodels/SaleDetailViewModel.java | 9 + .../res/layout/fragment_adoption_detail.xml | 372 +++++----- .../main/res/layout/fragment_analytics.xml | 618 ++++++++--------- .../layout/fragment_appointment_detail.xml | 474 ++++++------- .../app/src/main/res/layout/fragment_chat.xml | 222 +++--- .../res/layout/fragment_inventory_detail.xml | 270 ++++---- .../main/res/layout/fragment_pet_detail.xml | 399 +++++------ .../main/res/layout/fragment_pet_profile.xml | 562 ++++++++-------- .../res/layout/fragment_product_detail.xml | 336 ++++----- .../fragment_product_supplier_detail.xml | 236 ++++--- .../src/main/res/layout/fragment_profile.xml | 380 ++++++----- .../layout/fragment_purchase_order_detail.xml | 230 ++++--- .../src/main/res/layout/fragment_refund.xml | 418 ++++++------ .../main/res/layout/fragment_sale_detail.xml | 636 +++++++++--------- .../res/layout/fragment_service_detail.xml | 298 ++++---- .../main/res/layout/fragment_staff_detail.xml | 406 +++++------ .../res/layout/fragment_supplier_detail.xml | 316 ++++----- 35 files changed, 3600 insertions(+), 3279 deletions(-) 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 5731b9e9..3c6a9d6c 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 @@ -214,9 +214,11 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis Toast.makeText(requireContext(), "Downloading " + message.getAttachmentName() + "...", Toast.LENGTH_SHORT).show(); viewModel.downloadAttachment(message.getId()).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { saveFileToDownloads(resource.data, message.getAttachmentName(), message.getAttachmentMimeType()); - } else if (resource != null && resource.status == Resource.Status.ERROR) { + } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(requireContext(), "Download failed: " + resource.message, Toast.LENGTH_SHORT).show(); } }); @@ -285,9 +287,13 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis scrollToBottom(); }); - viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { - // Can show a progress bar if needed - }); + viewModel.getIsLoading().observe(getViewLifecycleOwner(), this::setLoading); + } + + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } } private void updateTitleAndStateIfActive(List list) { @@ -324,15 +330,12 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1); getActivity().getIntent().removeExtra("conversation_id"); } else { - // Restore last active conversation if any activeConversationId = viewModel.getLastActiveConversationId(); } viewModel.loadCustomers(); - // if (activeConversationId != null) { - // Re-subscribe and load history if there is an active conversation ID if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId); viewModel.loadMessageHistory(activeConversationId); } else { @@ -360,7 +363,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis binding.etMessage.setText(""); viewModel.sendMessage(activeConversationId, text).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.addMessageLocally(resource.data); viewModel.loadConversations(); } @@ -416,10 +421,12 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis removeAttachment(); viewModel.sendMessageWithAttachment(activeConversationId, contentPart, filePart).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.addMessageLocally(resource.data); viewModel.loadConversations(); - } else if (resource != null && resource.status == Resource.Status.ERROR) { + } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(requireContext(), "Failed to send attachment: " + resource.message, Toast.LENGTH_SHORT).show(); } }); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index af469295..b31243c2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -172,6 +172,12 @@ public class ProfileFragment extends Fragment { return binding.getRoot(); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -184,6 +190,7 @@ public class ProfileFragment extends Fragment { private void loadProfileData() { viewModel.getMe().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { currentUser = resource.data; @@ -229,6 +236,7 @@ public class ProfileFragment extends Fragment { //Call the backend to upload the avatar viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show(); loadProfileData(); @@ -247,6 +255,7 @@ public class ProfileFragment extends Fragment { private void deleteAvatar() { viewModel.deleteAvatar().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { hasImage = false; binding.imgProfile.setImageResource(R.drawable.placeholder); @@ -266,6 +275,7 @@ public class ProfileFragment extends Fragment { viewModel.updateMe(updates).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { currentUser = resource.data; Toast.makeText(getContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show(); 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 ef26db92..44719127 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 @@ -41,6 +41,7 @@ public class AnalyticsFragment extends Fragment { viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay); viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); if (loading) { binding.tvTotalRevenue.setText("Loading..."); binding.tvTotalTransactions.setText("..."); @@ -117,8 +118,6 @@ public class AnalyticsFragment extends Fragment { if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) { BigDecimal maxEmp = data.employeePerformance.get(data.employeePerformance.size() - 1).getValue(); if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; - // Sorting is ascending from VM for some reason? Let's check VM... it says b.getValue().compareTo(a.getValue()) which is DESC. - // Wait, computeAnalytics sorts them... let's assume DESC as per VM code. maxEmp = data.employeePerformance.get(0).getValue(); if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index eae36642..9b0f022b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -1,6 +1,5 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; -import android.app.DatePickerDialog; import android.os.Bundle; import android.view.*; import android.widget.*; @@ -12,7 +11,9 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.databinding.FragmentAdoptionDetailBinding; import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; @@ -73,6 +74,12 @@ public class AdoptionDetailFragment extends Fragment { viewModel.getEmployeeList().observe(getViewLifecycleOwner(), list -> refreshEmployeeSpinner()); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -116,29 +123,27 @@ public class AdoptionDetailFragment extends Fragment { } private void setupDatePicker() { - binding.etAdoptionDate.setOnClickListener(v -> { - Calendar c = Calendar.getInstance(); - new DatePickerDialog(requireContext(), - (dp, y, m, d) -> binding.etAdoptionDate.setText( - String.format("%04d-%02d-%02d", y, m + 1, d)), - c.get(Calendar.YEAR), - c.get(Calendar.MONTH), - c.get(Calendar.DAY_OF_MONTH)).show(); - }); + binding.etAdoptionDate.setOnClickListener(v -> UIUtils.showDatePicker(requireContext(), binding.etAdoptionDate, null)); } private void loadSpinnersData() { viewModel.loadPets().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.setPetList(resource.data); } }); viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.setCustomerList(resource.data); } }); viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.setStoreList(resource.data); } @@ -165,7 +170,9 @@ public class AdoptionDetailFragment extends Fragment { private void loadEmployees(Long storeId) { viewModel.loadEmployees(storeId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.setEmployeeList(resource.data); } }); @@ -183,7 +190,7 @@ public class AdoptionDetailFragment extends Fragment { long adoptionId = a.getLong("adoptionId"); viewModel.setAdoptionId(adoptionId); binding.tvAdoptionMode.setText("Edit Adoption"); - binding.tvAdoptionId.setText("ID: " + adoptionId); + binding.tvAdoptionId.setText(DateTimeUtils.formatId(adoptionId)); binding.tvAdoptionId.setVisibility(View.VISIBLE); binding.btnDeleteAdoption.setVisibility(View.VISIBLE); loadAdoptionData(); @@ -199,6 +206,7 @@ public class AdoptionDetailFragment extends Fragment { private void loadAdoptionData() { viewModel.loadAdoption().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { AdoptionDTO a = resource.data; preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; @@ -224,29 +232,16 @@ public class AdoptionDetailFragment extends Fragment { } private void saveAdoption() { - if (binding.spinnerAdoptionCustomer.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; - } - if (binding.spinnerAdoptionPet.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return; - } - if (binding.spinnerAdoptionStore.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return; - } - String date = binding.etAdoptionDate.getText().toString().trim(); - if (date.isEmpty()) { - Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; - } + if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionCustomer, "Customer")) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionPet, "Pet")) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionStore, "Store")) return; + if (!InputValidator.isNotEmpty(binding.etAdoptionDate, "Adoption Date")) return; BigDecimal fee = BigDecimal.ZERO; String feeStr = binding.etAdoptionFee.getText().toString().trim(); if (!feeStr.isEmpty()) { - try { - fee = new BigDecimal(feeStr); - } catch (NumberFormatException e) { - Toast.makeText(getContext(), "Invalid fee format", Toast.LENGTH_SHORT).show(); - return; - } + if (!InputValidator.isPositiveDecimal(binding.etAdoptionFee, "Adoption Fee")) return; + fee = new BigDecimal(feeStr); } DropdownDTO customer = viewModel.getCustomerList().getValue().get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); @@ -258,6 +253,7 @@ public class AdoptionDetailFragment extends Fragment { employeeId = viewModel.getEmployeeList().getValue().get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId(); } + String adoptionDate = binding.etAdoptionDate.getText().toString().trim(); String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; AdoptionDTO dto = new AdoptionDTO( @@ -265,12 +261,14 @@ public class AdoptionDetailFragment extends Fragment { customer.getId(), employeeId, store.getId(), - date, + adoptionDate, status, fee ); viewModel.saveAdoption(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), viewModel.isEditing() ? "Updated" : "Saved", Toast.LENGTH_SHORT).show(); navigateBack(); @@ -281,8 +279,10 @@ public class AdoptionDetailFragment extends Fragment { } private void confirmDelete() { - DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption", () -> + DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption Record", () -> viewModel.deleteAdoption().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); navigateBack(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index bf63fa41..bb2b9d58 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -19,6 +19,7 @@ import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; @@ -140,6 +141,12 @@ public class AppointmentDetailFragment extends Fragment { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, list, DropdownDTO::getLabel, "-- Select Staff --", preselectedStaffId, DropdownDTO::getId)); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + private void applyViewState(AppointmentDetailViewModel.ViewState state) { isUpdatingUI = true; @@ -189,22 +196,25 @@ public class AppointmentDetailFragment extends Fragment { private void loadAppointmentData() { viewModel.loadAppointment().observe(getViewLifecycleOwner(), resource -> { - if (resource == null || resource.status != Resource.Status.SUCCESS || resource.data == null) return; - AppointmentDTO a = resource.data; - preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; - preselectedServiceId = a.getServiceId() != null ? a.getServiceId() : -1; - preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; - preselectedStoreId = a.getStoreId() != null ? a.getStoreId() : -1; - preselectedStaffId = a.getEmployeeId() != null ? a.getEmployeeId() : -1; + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + AppointmentDTO a = resource.data; + preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; + preselectedServiceId = a.getServiceId() != null ? a.getServiceId() : -1; + preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; + preselectedStoreId = a.getStoreId() != null ? a.getStoreId() : -1; + preselectedStaffId = a.getEmployeeId() != null ? a.getEmployeeId() : -1; - binding.etAppointmentDate.setText(a.getAppointmentDate()); - parseAndSetTimeSpinners(a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00"); - - String status = a.getAppointmentStatus(); - if (status != null && !status.isEmpty()) { - SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, DateTimeUtils.formatStatusFromBackend(status)); + binding.etAppointmentDate.setText(a.getAppointmentDate()); + parseAndSetTimeSpinners(a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00"); + + String status = a.getAppointmentStatus(); + if (status != null && !status.isEmpty()) { + SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, DateTimeUtils.formatStatusFromBackend(status)); + } + notifyDateTimeStatusChange(); } - notifyDateTimeStatusChange(); }); } @@ -221,6 +231,8 @@ public class AppointmentDetailFragment extends Fragment { } viewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue(); String message = (state != null && state.isEditing) ? "Updated" : "Saved"; @@ -233,11 +245,11 @@ public class AppointmentDetailFragment extends Fragment { } private boolean validateRequiredFields() { - if (binding.spinnerCustomer.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a customer"); - if (binding.spinnerStore.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a store"); - if (binding.spinnerPet.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a pet"); - if (binding.spinnerService.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a service"); - if (binding.etAppointmentDate.getText().toString().trim().isEmpty()) return UIUtils.showToast(getContext(), "Select a date"); + if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Customer")) return false; + if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return false; + if (!InputValidator.isSpinnerSelected(binding.spinnerPet, "Pet")) return false; + if (!InputValidator.isSpinnerSelected(binding.spinnerService, "Service")) return false; + if (!InputValidator.isNotEmpty(binding.etAppointmentDate, "Appointment Date")) return false; return true; } @@ -261,6 +273,8 @@ public class AppointmentDetailFragment extends Fragment { private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () -> viewModel.deleteAppointment().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) navigateBack(); })); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index a6d1c613..ee52c960 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -8,7 +8,6 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -17,9 +16,11 @@ import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding; import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.InventoryDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -67,6 +68,12 @@ public class InventoryDetailFragment extends Fragment { viewModel.getProductList().observe(getViewLifecycleOwner(), list -> refreshProductSpinner()); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -75,11 +82,15 @@ public class InventoryDetailFragment extends Fragment { private void loadSpinnersData() { viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.setStoreList(resource.data); } }); viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.setProductList(resource.data.getContent()); } @@ -123,6 +134,7 @@ public class InventoryDetailFragment extends Fragment { private void loadInventoryData() { viewModel.loadInventory().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { InventoryDTO inv = resource.data; binding.etQuantity.setText(String.valueOf(inv.getQuantity())); @@ -138,19 +150,9 @@ public class InventoryDetailFragment extends Fragment { } private void saveInventory() { - if (binding.spinnerInventoryStore.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Please select a store", Toast.LENGTH_SHORT).show(); - return; - } - if (binding.spinnerInventoryProduct.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Please select a product", Toast.LENGTH_SHORT).show(); - return; - } - - if (!InputValidator.isNotEmpty(binding.etQuantity, "Quantity") || - !InputValidator.isPositiveInteger(binding.etQuantity, "Quantity")) { - return; - } + if (!InputValidator.isSpinnerSelected(binding.spinnerInventoryStore, "Store")) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerInventoryProduct, "Product")) return; + if (!InputValidator.isPositiveInteger(binding.etQuantity, "Quantity")) return; int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim()); DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); @@ -160,34 +162,37 @@ public class InventoryDetailFragment extends Fragment { setButtonsEnabled(false); viewModel.saveInventory(request).observe(getViewLifecycleOwner(), resource -> { - setButtonsEnabled(true); - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), viewModel.isEditing() ? "Inventory updated" : "Inventory created", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status != Resource.Status.LOADING) { + setButtonsEnabled(true); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), viewModel.isEditing() ? "Inventory updated" : "Inventory created", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } } }); } private void confirmDelete() { - new AlertDialog.Builder(requireContext()) - .setTitle("Delete inventory item?") - .setMessage("This cannot be undone.") - .setPositiveButton("Delete", (d, w) -> deleteInventory()) - .setNegativeButton("Cancel", null) - .show(); + DialogUtils.showDeleteConfirmDialog(requireContext(), "Inventory Item", this::deleteInventory); } private void deleteInventory() { setButtonsEnabled(false); viewModel.deleteInventory().observe(getViewLifecycleOwner(), resource -> { - setButtonsEnabled(true); - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status != Resource.Status.LOADING) { + setButtonsEnabled(true); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } } }); } @@ -197,8 +202,6 @@ public class InventoryDetailFragment extends Fragment { } private void setButtonsEnabled(boolean enabled) { - binding.btnSaveInventory.setEnabled(enabled); - binding.btnDeleteInventory.setEnabled(enabled); - binding.btnInventoryBack.setEnabled(enabled); + UIUtils.setViewsEnabled(enabled, binding.btnSaveInventory, binding.btnDeleteInventory, binding.btnInventoryBack); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index d260d723..ee1b34fc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -21,6 +21,7 @@ import com.example.petstoremobile.databinding.FragmentPetDetailBinding; import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; @@ -65,7 +66,6 @@ public class PetDetailFragment extends Fragment { observeViewModel(); handleArguments(); - //set button click listeners binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSavePet.setOnClickListener(v -> savePet()); binding.btnDeletePet.setOnClickListener(v -> deletePet()); @@ -76,36 +76,41 @@ public class PetDetailFragment extends Fragment { viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> updateStoreSpinnerSelection()); viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.setCustomerList(resource.data); } }); viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.setStoreList(resource.data); } }); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Handles the saving of pet data (adding/updating). - */ private void savePet() { - // Validates all fields using InputValidator if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return; if (!InputValidator.isNotEmpty(binding.etPetSpecies, "Species")) return; if (!InputValidator.isNotEmpty(binding.etPetBreed, "Breed")) return; if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return; if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return; - //get all the values from the fields String name = binding.etPetName.getText().toString().trim(); String species = binding.etPetSpecies.getText().toString().trim(); String breed = binding.etPetBreed.getText().toString().trim(); @@ -113,37 +118,27 @@ public class PetDetailFragment extends Fragment { double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim()); String status = binding.spinnerPetStatus.getSelectedItem().toString(); - // Get selected customer Long customerId = null; - int customerPos = binding.spinnerCustomer.getSelectedItemPosition(); - if (customerPos > 0) { // 0 means no customer for pet - customerId = viewModel.getCustomerList().getValue().get(customerPos - 1).getId(); + if (binding.spinnerCustomer.getSelectedItemPosition() > 0) { + customerId = viewModel.getCustomerList().getValue().get(binding.spinnerCustomer.getSelectedItemPosition() - 1).getId(); } - // Get selected store Long storeId = null; - int storePos = binding.spinnerStore.getSelectedItemPosition(); - if (storePos > 0) { - storeId = viewModel.getStoreList().getValue().get(storePos - 1).getId(); + if (binding.spinnerStore.getSelectedItemPosition() > 0) { + storeId = viewModel.getStoreList().getValue().get(binding.spinnerStore.getSelectedItemPosition() - 1).getId(); } - // Validation: If status is Available, a store must be selected if ("Available".equalsIgnoreCase(status)) { if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return; } - - // Validation: If status is Owned, an owner must be selected if ("Owned".equalsIgnoreCase(status)) { if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return; } - - // Validation: If status is Adopted, an owner and store must be selected if ("Adopted".equalsIgnoreCase(status)) { if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return; } - //create a pet object to send to the API PetDTO petDTO = new PetDTO(); petDTO.setPetName(name); petDTO.setPetSpecies(species); @@ -155,6 +150,8 @@ public class PetDetailFragment extends Fragment { petDTO.setStoreId(storeId); viewModel.savePet(petDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { if (viewModel.isEditing()) { ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) viewModel.getPetId()); @@ -170,12 +167,11 @@ public class PetDetailFragment extends Fragment { }); } - /** - * Displays a confirmation dialog and handles the deletion of a pet. - */ private void deletePet() { - DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () -> + DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () -> { viewModel.deletePet().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) viewModel.getPetId()); Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); @@ -183,32 +179,24 @@ public class PetDetailFragment extends Fragment { } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } - })); + }); + }); } - /** - * Navigates back to the pet list screen. - */ private void navigateToPetList() { NavHostFragment.findNavController(this).popBackStack(R.id.nav_pet, false); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Handles arguments passed to the fragment to determine if it's in edit or add mode. - */ private void handleArguments() { if (getArguments() != null && getArguments().containsKey("petId")) { long petId = getArguments().getLong("petId"); viewModel.setPetId(petId); binding.tvMode.setText("Edit Pet"); - binding.tvPetId.setText("ID: " + petId); + binding.tvPetId.setText(DateTimeUtils.formatId(petId)); binding.tvPetId.setVisibility(View.VISIBLE); binding.btnDeletePet.setVisibility(View.VISIBLE); @@ -225,12 +213,10 @@ public class PetDetailFragment extends Fragment { } } - /** - * Fetches specific pet details from the backend using the ID. - */ private void loadPetData() { viewModel.loadPet().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { PetDTO p = resource.data; binding.etPetName.setText(p.getPetName()); @@ -253,9 +239,6 @@ public class PetDetailFragment extends Fragment { }); } - /** - * Updates the customer spinner with the current list and sets the selection if needed. - */ private void updateCustomerSpinnerSelection() { SpinnerUtils.populateSpinner( requireContext(), @@ -268,9 +251,6 @@ public class PetDetailFragment extends Fragment { ); } - /** - * Updates the store spinner with the current list and sets the selection if needed. - */ private void updateStoreSpinnerSelection() { SpinnerUtils.populateSpinner( requireContext(), @@ -283,9 +263,6 @@ public class PetDetailFragment extends Fragment { ); } - /** - * Initializes the spinner for pet status selection. - */ private void setupSpinner() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, new String[]{"Available", "Adopted", "Owned"}); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 2d07c280..9f072b51 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -18,6 +18,7 @@ import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentProductDetailBinding; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.viewmodels.ProductDetailViewModel; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; @@ -111,12 +112,20 @@ public class ProductDetailFragment extends Fragment { viewModel.getCategoryList().observe(getViewLifecycleOwner(), list -> updateCategorySpinner()); viewModel.loadCategories().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.setCategoryList(resource.data.getContent()); } }); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + private void updateCategorySpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, viewModel.getCategoryList().getValue(), CategoryDTO::getCategoryName, "-- Select Category --", @@ -135,7 +144,7 @@ public class ProductDetailFragment extends Fragment { long prodId = a.getLong("prodId"); viewModel.setProdId(prodId); binding.tvProductMode.setText("Edit Product"); - binding.tvProductId.setText("ID: " + prodId); + binding.tvProductId.setText(DateTimeUtils.formatId(prodId)); binding.tvProductId.setVisibility(View.VISIBLE); binding.btnDeleteProduct.setVisibility(View.VISIBLE); loadProductData(); @@ -152,6 +161,7 @@ public class ProductDetailFragment extends Fragment { private void loadProductData() { viewModel.loadProduct().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { ProductDTO p = resource.data; binding.etProductName.setText(p.getProdName()); @@ -185,7 +195,9 @@ public class ProductDetailFragment extends Fragment { private void performPendingImageActions(String successMsg) { if (isImageRemoved) { viewModel.deleteProductImage().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status != Resource.Status.LOADING) { if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); } else { @@ -214,7 +226,9 @@ public class ProductDetailFragment extends Fragment { MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); viewModel.uploadProductImage(body).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status != Resource.Status.LOADING) { if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); } else { @@ -227,15 +241,8 @@ public class ProductDetailFragment extends Fragment { private void saveProduct() { if (!InputValidator.isNotEmpty(binding.etProductName, "Product Name")) return; - - if (binding.spinnerProductCategory.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a category", Toast.LENGTH_SHORT).show(); return; - } - - if (!InputValidator.isNotEmpty(binding.etProductPrice, "Price") || - !InputValidator.isPositiveDecimal(binding.etProductPrice, "Price")) { - return; - } + if (!InputValidator.isSpinnerSelected(binding.spinnerProductCategory, "Category")) return; + if (!InputValidator.isPositiveDecimal(binding.etProductPrice, "Price")) return; String name = binding.etProductName.getText().toString().trim(); String desc = binding.etProductDesc.getText().toString().trim(); @@ -245,7 +252,9 @@ public class ProductDetailFragment extends Fragment { ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); viewModel.saveProduct(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status != Resource.Status.LOADING) { if (resource.status == Resource.Status.SUCCESS) { if (resource.data != null) { viewModel.setProdId(resource.data.getProdId()); @@ -261,9 +270,11 @@ public class ProductDetailFragment extends Fragment { private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () -> viewModel.deleteProduct().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { navigateBack(); - } else if (resource != null && resource.status == Resource.Status.ERROR) { + } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } })); 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 a0e7d1a6..770c6b82 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 @@ -15,6 +15,7 @@ import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.ProductSupplierDetailViewModel; import java.math.BigDecimal; @@ -64,6 +65,12 @@ public class ProductSupplierDetailFragment extends Fragment { viewModel.getSupplierList().observe(getViewLifecycleOwner(), list -> refreshSupplierSpinner()); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -72,11 +79,15 @@ public class ProductSupplierDetailFragment extends Fragment { private void loadSpinnersData() { viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.setProductList(resource.data.getContent()); } }); viewModel.loadSuppliers().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.setSupplierList(resource.data.getContent()); } @@ -113,17 +124,9 @@ public class ProductSupplierDetailFragment extends Fragment { } private void save() { - if (binding.spinnerPSProduct.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return; - } - if (binding.spinnerPSSupplier.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a supplier", Toast.LENGTH_SHORT).show(); return; - } - - if (!InputValidator.isNotEmpty(binding.etPSCost, "Cost") || - !InputValidator.isPositiveDecimal(binding.etPSCost, "Cost")) { - return; - } + if (!InputValidator.isSpinnerSelected(binding.spinnerPSProduct, "Product")) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerPSSupplier, "Supplier")) return; + if (!InputValidator.isPositiveDecimal(binding.etPSCost, "Cost")) return; ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerPSProduct.getSelectedItemPosition() - 1); SupplierDTO supplier = viewModel.getSupplierList().getValue().get(binding.spinnerPSSupplier.getSelectedItemPosition() - 1); @@ -132,6 +135,8 @@ public class ProductSupplierDetailFragment extends Fragment { ProductSupplierDTO dto = new ProductSupplierDTO(product.getProdId(), supplier.getSupId(), cost); viewModel.saveProductSupplier(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), viewModel.isEditing() ? "Updated" : "Saved", Toast.LENGTH_SHORT).show(); navigateBack(); @@ -142,8 +147,10 @@ public class ProductSupplierDetailFragment extends Fragment { } private void confirmDelete() { - DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier", () -> + DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier Relationship", () -> viewModel.deleteProductSupplier().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); navigateBack(); 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 90ebf645..4b28ed92 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 @@ -66,9 +66,16 @@ public class PurchaseOrderDetailFragment extends Fragment { } } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + private void loadPurchaseOrderData() { viewModel.loadPurchaseOrder(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { PurchaseOrderDTO po = resource.data; binding.tvPODetailId.setText("PO #" + po.getPurchaseOrderId()); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java index d94b0041..7d4841f3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java @@ -1,6 +1,5 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; -import android.app.AlertDialog; import android.os.Bundle; import android.util.Log; import android.view.*; @@ -13,7 +12,9 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentRefundBinding; import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.viewmodels.RefundViewModel; +import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.math.RoundingMode; @@ -50,8 +51,7 @@ public class RefundFragment extends Fragment { } private void setupSpinner() { - binding.spinnerRefundPayment.setAdapter(new ArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, PAYMENT_METHODS)); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerRefundPayment, PAYMENT_METHODS); } private void observeViewModel() { @@ -63,9 +63,17 @@ public class RefundFragment extends Fragment { }); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + private void loadAllSales() { viewModel.loadAllSales().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { viewModel.setAllSales(resource.data.getContent()); Bundle args = getArguments(); if (args != null && args.containsKey("saleId")) { @@ -122,11 +130,7 @@ public class RefundFragment extends Fragment { + " | Payment: " + currentSale.getPaymentMethod()); if (currentSale.getPaymentMethod() != null) { - for (int i = 0; i < PAYMENT_METHODS.length; i++) { - if (PAYMENT_METHODS[i].equalsIgnoreCase(currentSale.getPaymentMethod())) { - binding.spinnerRefundPayment.setSelection(i); break; - } - } + SpinnerUtils.setSelectionByValue(binding.spinnerRefundPayment, currentSale.getPaymentMethod()); } if (viewModel.getAvailableItems().getValue() == null || viewModel.getAvailableItems().getValue().isEmpty()) { @@ -263,39 +267,37 @@ public class RefundFragment extends Fragment { } private void showQuantityDialog(RefundViewModel.RefundItem item, int available) { - AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); - builder.setTitle("Refund Quantity"); - builder.setMessage("Product: " + item.productName + "\nAvailable: " + available); - EditText input = new EditText(getContext()); input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); input.setText(String.valueOf(available)); input.setSelectAllOnFocus(true); - builder.setView(input); + input.setPadding(40, 40, 40, 40); - builder.setPositiveButton("Add to Refund", (d, w) -> { - String val = input.getText().toString().trim(); - if (val.isEmpty()) return; - int qty; - try { qty = Integer.parseInt(val); } - catch (Exception e) { - Toast.makeText(getContext(), "Invalid quantity", Toast.LENGTH_SHORT).show(); - return; - } - if (qty <= 0) { - Toast.makeText(getContext(), "Quantity must be at least 1", Toast.LENGTH_SHORT).show(); - return; - } - if (qty > available) { - Toast.makeText(getContext(), "Cannot exceed " + available, Toast.LENGTH_SHORT).show(); - return; - } - - viewModel.addToCart(item, qty); - }); - - builder.setNegativeButton("Cancel", null); - builder.show(); + new androidx.appcompat.app.AlertDialog.Builder(requireContext()) + .setTitle("Refund Quantity") + .setMessage("Product: " + item.productName + "\nAvailable: " + available) + .setView(input) + .setPositiveButton("Add to Refund", (d, w) -> { + String val = input.getText().toString().trim(); + if (val.isEmpty()) return; + int qty; + try { qty = Integer.parseInt(val); } + catch (Exception e) { + Toast.makeText(getContext(), "Invalid quantity", Toast.LENGTH_SHORT).show(); + return; + } + if (qty <= 0) { + Toast.makeText(getContext(), "Quantity must be at least 1", Toast.LENGTH_SHORT).show(); + return; + } + if (qty > available) { + Toast.makeText(getContext(), "Cannot exceed " + available, Toast.LENGTH_SHORT).show(); + return; + } + viewModel.addToCart(item, qty); + }) + .setNegativeButton("Cancel", null) + .show(); } private void updateRefundTotal() { @@ -322,18 +324,16 @@ public class RefundFragment extends Fragment { for (RefundViewModel.RefundItem item : viewModel.getRefundCart().getValue()) total = total.add(item.getTotal()); final BigDecimal finalTotal = total; - new AlertDialog.Builder(requireContext()) - .setTitle("Confirm Refund") - .setMessage("Process refund for Sale #" + viewModel.getCurrentSale().getSaleId() - + "?\nRefund amount: $" + finalTotal.setScale(2, RoundingMode.HALF_UP)) - .setPositiveButton("Yes", (d, w) -> submitRefund(payment)) - .setNegativeButton("No", null) - .show(); + DialogUtils.showConfirmDialog(requireContext(), "Confirm Refund", + "Process refund for Sale #" + viewModel.getCurrentSale().getSaleId() + + "?\nRefund amount: $" + finalTotal.setScale(2, RoundingMode.HALF_UP), + () -> submitRefund(payment)); } private void submitRefund(String payment) { viewModel.submitRefund(payment).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Refund processed successfully!", Toast.LENGTH_LONG).show(); navigateBack(); 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 7ae18823..93a5e244 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 @@ -14,6 +14,7 @@ import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.viewmodels.SaleDetailViewModel; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.UIUtils; import dagger.hilt.android.AndroidEntryPoint; @@ -107,21 +108,35 @@ public class SaleDetailFragment extends Fragment { } } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + private void loadData() { viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setStoreList(resource.data); }); viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setCustomerList(resource.data); }); viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setProductList(resource.data.getContent()); }); } private void loadSaleDetails() { viewModel.loadSaleDetails().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { SaleDTO sale = resource.data; binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount()); binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount())); @@ -157,22 +172,10 @@ public class SaleDetailFragment extends Fragment { private void setupAddItem() { binding.btnAddItem.setOnClickListener(v -> { - if (binding.spinnerSaleProduct.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); - return; - } - String qtyStr = binding.etSaleQuantity.getText().toString().trim(); - if (qtyStr.isEmpty()) { - binding.etSaleQuantity.setError("Enter quantity"); - return; - } - int qty; - try { qty = Integer.parseInt(qtyStr); } - catch (Exception e) { - binding.etSaleQuantity.setError("Invalid quantity"); - return; - } + if (!InputValidator.isSpinnerSelected(binding.spinnerSaleProduct, "Product")) return; + if (!InputValidator.isPositiveInteger(binding.etSaleQuantity, "Quantity")) return; + int qty = Integer.parseInt(binding.etSaleQuantity.getText().toString().trim()); ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerSaleProduct.getSelectedItemPosition() - 1); for (SaleDTO.SaleItemDTO existing : viewModel.getCartItems().getValue()) { @@ -238,10 +241,8 @@ public class SaleDetailFragment extends Fragment { } private void saveSale() { - if (binding.spinnerSaleStore.getSelectedItemPosition() == 0) { - Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); - return; - } + if (!InputValidator.isSpinnerSelected(binding.spinnerSaleStore, "Store")) return; + if (viewModel.getCartItems().getValue() == null || viewModel.getCartItems().getValue().isEmpty()) { Toast.makeText(getContext(), "Add at least one item", Toast.LENGTH_SHORT).show(); return; @@ -259,6 +260,7 @@ public class SaleDetailFragment extends Fragment { viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); navigateBack(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index 7fdaae9b..2374fc4c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -17,6 +17,7 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentServiceDetailBinding; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; @@ -57,6 +58,12 @@ public class ServiceDetailFragment extends Fragment { binding.btnDeleteService.setOnClickListener(v -> deleteService()); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -81,6 +88,8 @@ public class ServiceDetailFragment extends Fragment { serviceDTO.setServicePrice(price); viewModel.saveService(serviceDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { if (viewModel.isEditing()) { ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) viewModel.getServiceId()); @@ -99,6 +108,8 @@ public class ServiceDetailFragment extends Fragment { private void deleteService() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () -> viewModel.deleteService().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) viewModel.getServiceId()); Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); @@ -118,7 +129,7 @@ public class ServiceDetailFragment extends Fragment { long serviceId = getArguments().getLong("serviceId"); viewModel.setServiceId(serviceId); binding.tvMode.setText("Edit Service"); - binding.tvServiceId.setText("ID: " + serviceId); + binding.tvServiceId.setText(DateTimeUtils.formatId(serviceId)); binding.btnDeleteService.setVisibility(View.VISIBLE); loadServiceData(); } else { @@ -133,6 +144,7 @@ public class ServiceDetailFragment extends Fragment { private void loadServiceData() { viewModel.loadService().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { ServiceDTO s = resource.data; binding.etServiceName.setText(s.getServiceName()); 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 0013a277..1c1bfc4e 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 @@ -4,13 +4,16 @@ import android.os.Bundle; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentStaffDetailBinding; import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.StaffDetailViewModel; import com.example.petstoremobile.utils.Resource; import dagger.hilt.android.AndroidEntryPoint; @@ -36,14 +39,15 @@ public class StaffDetailFragment extends Fragment { binding.btnStaffBack.setOnClickListener(v -> navigateBack()); binding.btnSaveStaff.setOnClickListener(v -> save()); binding.btnDeleteStaff.setOnClickListener(v -> confirmDelete()); + + UIUtils.formatPhoneInput(binding.etStaffPhone); + return binding.getRoot(); } private void setupSpinners() { - binding.spinnerStaffRole.setAdapter(new ArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, ROLES)); - binding.spinnerStaffStatus.setAdapter(new ArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, STATUSES)); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffRole, ROLES); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES); } private void handleArguments() { @@ -62,16 +66,8 @@ public class StaffDetailFragment extends Fragment { binding.etStaffPhone.setText(a.getString("phone", "")); binding.btnDeleteStaff.setVisibility(View.VISIBLE); - String role = a.getString("role", "STAFF"); - for (int i = 0; i < ROLES.length; i++) { - if (ROLES[i].equals(role)) { - binding.spinnerStaffRole.setSelection(i); - break; - } - } - - boolean active = a.getBoolean("active", true); - binding.spinnerStaffStatus.setSelection(active ? 0 : 1); + SpinnerUtils.setSelectionByValue(binding.spinnerStaffRole, a.getString("role", "STAFF")); + binding.spinnerStaffStatus.setSelection(a.getBoolean("active", true) ? 0 : 1); } else { viewModel.setEmployeeId(-1, false); @@ -81,29 +77,39 @@ public class StaffDetailFragment extends Fragment { } } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } private void save() { - String username = binding.etStaffUsername.getText() != null ? binding.etStaffUsername.getText().toString().trim() : ""; - String password = binding.etStaffPassword.getText() != null ? binding.etStaffPassword.getText().toString().trim() : ""; - String firstName = binding.etStaffFirstName.getText() != null ? binding.etStaffFirstName.getText().toString().trim() : ""; - String lastName = binding.etStaffLastName.getText() != null ? binding.etStaffLastName.getText().toString().trim() : ""; - String email = binding.etStaffEmail.getText() != null ? binding.etStaffEmail.getText().toString().trim() : ""; - String phone = binding.etStaffPhone.getText() != null ? binding.etStaffPhone.getText().toString().trim() : ""; + if (!InputValidator.isNotEmpty(binding.etStaffUsername, "Username")) return; + + if (!viewModel.isEditing()) { + if (!InputValidator.isNotEmpty(binding.etStaffPassword, "Password")) return; + String pass = binding.etStaffPassword.getText().toString(); + if (pass.length() < 6) { + binding.etStaffPassword.setError("At least 6 characters"); + binding.etStaffPassword.requestFocus(); + return; + } + } + + if (!InputValidator.isNotEmpty(binding.etStaffFirstName, "First Name")) return; + if (!InputValidator.isNotEmpty(binding.etStaffLastName, "Last Name")) return; + if (!InputValidator.isValidEmail(binding.etStaffEmail)) return; + if (!InputValidator.isValidPhone(binding.etStaffPhone)) 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()]; boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0; - if (username.isEmpty()) { binding.etStaffUsername.setError("Required"); return; } - if (!viewModel.isEditing() && password.isEmpty()) { - binding.etStaffPassword.setError("Required for new account"); return; - } - if (!viewModel.isEditing() && password.length() < 6) { - binding.etStaffPassword.setError("At least 6 characters"); return; - } - if (firstName.isEmpty()) { binding.etStaffFirstName.setError("Required"); return; } - if (lastName.isEmpty()) { binding.etStaffLastName.setError("Required"); return; } - if (email.isEmpty()) { binding.etStaffEmail.setError("Required"); return; } - if (phone.isEmpty()) { binding.etStaffPhone.setError("Required"); return; } - EmployeeDTO dto = new EmployeeDTO( username, password.isEmpty() ? null : password, @@ -117,6 +123,7 @@ public class StaffDetailFragment extends Fragment { viewModel.saveEmployee(dto).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), viewModel.isEditing() ? "Updated successfully" : "Staff account created", Toast.LENGTH_SHORT).show(); navigateBack(); @@ -128,21 +135,18 @@ public class StaffDetailFragment extends Fragment { } private void confirmDelete() { - new AlertDialog.Builder(requireContext()) - .setTitle("Delete Staff Account?") - .setMessage("This will permanently delete this staff account.") - .setPositiveButton("Yes", (d, w) -> - viewModel.deleteEmployee().observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - if (resource.status == Resource.Status.SUCCESS) { - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Delete failed: " + resource.message, - Toast.LENGTH_SHORT).show(); - } - } - })) - .setNegativeButton("No", null).show(); + DialogUtils.showDeleteConfirmDialog(requireContext(), "Staff Account", () -> + viewModel.deleteEmployee().observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, + Toast.LENGTH_SHORT).show(); + } + } + })); } private void navigateBack() { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index 62c7c381..a7c64079 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -58,6 +58,12 @@ public class SupplierDetailFragment extends Fragment { binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -85,6 +91,8 @@ public class SupplierDetailFragment extends Fragment { supplierDTO.setSupPhone(phone); viewModel.saveSupplier(supplierDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { if (viewModel.isEditing()) { ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) viewModel.getSupId()); @@ -103,6 +111,8 @@ public class SupplierDetailFragment extends Fragment { private void deleteSupplier() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () -> viewModel.deleteSupplier().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) viewModel.getSupId()); Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); @@ -138,6 +148,7 @@ public class SupplierDetailFragment extends Fragment { private void loadSupplierData() { viewModel.loadSupplier().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { SupplierDTO s = resource.data; binding.etSupCompany.setText(s.getSupCompany()); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index ee11fb4e..73da4613 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -95,6 +95,12 @@ public class PetProfileFragment extends Fragment { return binding.getRoot(); } + private void setLoading(boolean loading) { + if (binding != null && binding.progressBar != null) { + binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + } + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -104,6 +110,7 @@ public class PetProfileFragment extends Fragment { private void loadPetData() { viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { PetDTO pet = resource.data; binding.tvPetName.setText(pet.getPetName()); @@ -172,13 +179,13 @@ public class PetProfileFragment extends Fragment { MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); viewModel.uploadPetImage(petId, body).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show(); - loadPetImage((int) petId); - } else { - Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show(); - } + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show(); + loadPetImage((int) petId); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } catch (Exception e) { @@ -188,14 +195,14 @@ public class PetProfileFragment extends Fragment { private void deletePetImage() { viewModel.deletePetImage(petId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Pet photo removed", Toast.LENGTH_SHORT).show(); - hasImage = false; - binding.imgPet.setImageResource(R.drawable.placeholder); - } else { - Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); - } + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Pet photo removed", Toast.LENGTH_SHORT).show(); + hasImage = false; + binding.imgPet.setImageResource(R.drawable.placeholder); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } 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 6aa64694..0422561d 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 @@ -61,12 +61,15 @@ public class ChatListViewModel extends ViewModel { } public void loadCustomers() { + isLoading.setValue(true); customerRepository.getAllCustomers(0, 100).observeForever(resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { for (CustomerDTO c : resource.data.getContent()) { customerNames.put(c.getCustomerId(), c.getFullName()); } loadConversations(); + } else if (resource != null && resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); } }); } @@ -96,6 +99,7 @@ public class ChatListViewModel extends ViewModel { } public void loadMessageHistory(Long conversationId) { + isLoading.setValue(true); chatRepository.getMessages(conversationId).observeForever(resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { List messages = new ArrayList<>(); @@ -103,6 +107,9 @@ public class ChatListViewModel extends ViewModel { messages.add(dtoToModel(dto)); } messageList.setValue(messages); + isLoading.setValue(false); + } else if (resource != null && resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); } }); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java index 00c074e9..68506f44 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java @@ -27,6 +27,7 @@ public class PetDetailViewModel extends ViewModel { private final MutableLiveData petState = new MutableLiveData<>(); private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); private long petId = -1; private boolean isEditing = false; @@ -91,4 +92,12 @@ public class PetDetailViewModel extends ViewModel { public LiveData> getStoreList() { return storeList; } + + public LiveData getIsLoading() { + return isLoading; + } + + public void setLoading(boolean loading) { + isLoading.setValue(loading); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java index bf84ab32..201fae9e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java @@ -35,6 +35,7 @@ public class SaleDetailViewModel extends ViewModel { private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> cartItems = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); @Inject public SaleDetailViewModel(SaleRepository saleRepository, StoreRepository storeRepository, @@ -106,4 +107,12 @@ public class SaleDetailViewModel extends ViewModel { } return total; } + + public LiveData getIsLoading() { + return isLoading; + } + + public void setLoading(boolean loading) { + isLoading.setValue(loading); + } } diff --git a/android/app/src/main/res/layout/fragment_adoption_detail.xml b/android/app/src/main/res/layout/fragment_adoption_detail.xml index 45ac673f..0608f96f 100644 --- a/android/app/src/main/res/layout/fragment_adoption_detail.xml +++ b/android/app/src/main/res/layout/fragment_adoption_detail.xml @@ -1,214 +1,228 @@ - + android:layout_height="match_parent"> - - - -