From aefa00f95d36a1a753036a210dd8f565253e074b Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:12:41 -0600 Subject: [PATCH 01/12] added pending status on pets andiord, also made pets automatically switch to pending when an adoption is in pending --- .../petstoremobile/adapters/PetAdapter.java | 16 +++++++++++++--- .../fragments/listfragments/PetFragment.java | 2 +- .../viewmodels/PetDetailViewModel.java | 4 +++- .../backend/repository/PetRepository.java | 4 ++-- .../petshop/backend/service/AdoptionService.java | 4 ++++ 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java index 7ec19b8a..1f33aea4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java @@ -104,10 +104,20 @@ public class PetAdapter extends RecyclerView.Adapter i binding.tvPetStatus.setText(pet.getPetStatus()); //Set the status color depending on availability. If available, green, otherwise red - if (pet.getPetStatus() != null && pet.getPetStatus().equals("Available")) { - binding.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50")); + if (pet.getPetStatus() != null) { + switch (pet.getPetStatus()) { + case "Available": + binding.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50")); + break; + case "Pending": + binding.tvPetStatus.setBackgroundColor(Color.parseColor("#FF9800")); + break; + default: + binding.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336")); + break; + } } else { - binding.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336")); + binding.tvPetStatus.setBackgroundColor(Color.parseColor("#9E9E9E")); } // Load pet image using Glide 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 a9d153e4..cd00726a 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 @@ -136,7 +136,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } private void setupStatusFilter() { - String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"}; + String[] statuses = {"All Statuses", "Available", "Adopted", "Owned", "Pending"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData); } 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 2f302b42..25eddbbb 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 @@ -25,6 +25,7 @@ public class PetDetailViewModel extends ViewModel { private static final String STATUS_AVAILABLE = "Available"; private static final String STATUS_ADOPTED = "Adopted"; private static final String STATUS_OWNED = "Owned"; + private static final String STATUS_PENDING = "Pending"; private final PetRepository petRepository; private final CustomerRepository customerRepository; @@ -253,6 +254,7 @@ public class PetDetailViewModel extends ViewModel { String normalized = status.trim(); if (STATUS_ADOPTED.equalsIgnoreCase(normalized)) return STATUS_ADOPTED; if (STATUS_OWNED.equalsIgnoreCase(normalized)) return STATUS_OWNED; + if (STATUS_PENDING.equalsIgnoreCase(normalized)) return STATUS_PENDING; return STATUS_AVAILABLE; } @@ -290,7 +292,7 @@ public class PetDetailViewModel extends ViewModel { public boolean isStoreEnabled = true; public String modeTitle = "Add Pet"; public String saveButtonText = "Add"; - public String[] availableStatuses = new String[]{STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED}; + public String[] availableStatuses = new String[]{STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED, STATUS_PENDING}; public String selectedStatus = STATUS_AVAILABLE; public String selectedSpecies = null; public Long selectedCustomerId = null; diff --git a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java index f020c57e..e5b5a958 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -19,7 +19,7 @@ public interface PetRepository extends JpaRepository { "WHERE LOWER(p.petStatus) = 'available' " + "AND NOT EXISTS (" + " SELECT 1 FROM Adoption a " + - " WHERE a.pet = p AND LOWER(a.adoptionStatus) = 'completed'" + + " WHERE a.pet = p AND (LOWER(a.adoptionStatus) = 'completed' OR LOWER(a.adoptionStatus) = 'pending')" + ") " + "ORDER BY p.petName ASC") List findAdoptablePetsOrderByPetNameAsc(); @@ -29,7 +29,7 @@ public interface PetRepository extends JpaRepository { "AND (:storeId IS NULL OR p.store.storeId = :storeId) " + "AND NOT EXISTS (" + " SELECT 1 FROM Adoption a " + - " WHERE a.pet = p AND LOWER(a.adoptionStatus) = 'completed'" + + " WHERE a.pet = p AND (LOWER(a.adoptionStatus) = 'completed' OR LOWER(a.adoptionStatus) = 'pending')" + ") " + "ORDER BY p.petName ASC") List findAdoptablePetsByStore(@Param("storeId") Long storeId); diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index d5d66d67..134a78f2 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -32,6 +32,7 @@ public class AdoptionService { private static final String ADOPTION_STATUS_MISSED = "Missed"; private static final String PET_STATUS_AVAILABLE = "Available"; private static final String PET_STATUS_ADOPTED = "Adopted"; + private static final String PET_STATUS_PENDING = "Pending"; private final AdoptionRepository adoptionRepository; private final PetRepository petRepository; @@ -263,10 +264,13 @@ public class AdoptionService { private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId, User customer) { boolean completedElsewhere = adoptionId != null && adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); + if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) || completedElsewhere) { pet.setPetStatus(PET_STATUS_ADOPTED); pet.setOwner(customer); pet.setStore(null); + } else if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus)) { + pet.setPetStatus(PET_STATUS_PENDING); } else { pet.setPetStatus(PET_STATUS_AVAILABLE); pet.setOwner(null); From a6f90f847782dfee87e1b660a27ea70e32ceab39 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:18:46 -0600 Subject: [PATCH 02/12] Fixed phone validation for andriod --- .../com/example/petstoremobile/adapters/PetAdapter.java | 2 +- .../com/example/petstoremobile/utils/InputValidator.java | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java index 1f33aea4..463295bc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java @@ -103,7 +103,7 @@ public class PetAdapter extends RecyclerView.Adapter i binding.tvPetStatus.setText(pet.getPetStatus()); - //Set the status color depending on availability. If available, green, otherwise red + //Set the status color depending on availability. If available, green, If Pending, yellow, otherwise red if (pet.getPetStatus() != null) { switch (pet.getPetStatus()) { case "Available": 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 ee79ec9f..0d45ad12 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,12 +100,13 @@ public class InputValidator { return true; } - // Checks if the phone number is valid + // Checks if the phone number is valid in (XXX) XXX-XXXX format public static boolean isValidPhone(EditText field) { String phone = field.getText().toString().trim(); - // Android built in phone validation pattern - if (phone.isEmpty() || !android.util.Patterns.PHONE.matcher(phone).matches()) { - field.setError("Enter a valid phone number"); + // Matches (XXX) XXX-XXXX format + String pattern = "^\\(\\d{3}\\) \\d{3}-\\d{4}$"; + if (phone.isEmpty() || !phone.matches(pattern)) { + field.setError("Enter a valid phone number: (XXX) XXX-XXXX"); field.requestFocus(); return false; } From 1b4069e7e462e92f276c0856c2e0fa9752d07445 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:25:28 -0600 Subject: [PATCH 03/12] made sales readonly for andriod --- .../fragments/listfragments/SaleFragment.java | 9 +++++++++ .../detailfragments/SaleDetailFragment.java | 11 +++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) 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 787e99a7..5c6589f3 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 @@ -65,6 +65,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis UIUtils.setupHamburgerMenu(binding.btnHamburger, this); + if (isAdmin()) { + binding.fabAddSale.setVisibility(View.GONE); + binding.btnOpenRefund.setVisibility(View.GONE); + } + binding.fabAddSale.setOnClickListener(v -> NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail)); @@ -110,6 +115,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis return "STAFF".equalsIgnoreCase(tokenManager.getRole()); } + private boolean isAdmin() { + return "ADMIN".equalsIgnoreCase(tokenManager.getRole()); + } + private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true)); } 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 ec111675..9b94a168 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 @@ -68,6 +68,10 @@ public class SaleDetailFragment extends Fragment { return "STAFF".equalsIgnoreCase(tokenManager.getRole()); } + private boolean isAdmin() { + return "ADMIN".equalsIgnoreCase(tokenManager.getRole()); + } + private void observeViewModel() { viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> { Long primaryStoreId = tokenManager.getPrimaryStoreId(); @@ -109,7 +113,7 @@ public class SaleDetailFragment extends Fragment { binding.tvSaleDetailId.setText("ID: " + saleId); boolean isRefund = a.getBoolean("isRefund", false); - if (isRefund) { + if (isRefund || isAdmin()) { binding.btnRefundSale.setVisibility(View.GONE); } @@ -128,9 +132,8 @@ public class SaleDetailFragment extends Fragment { binding.spinnerPaymentMethod.setVisibility(View.GONE); binding.tvSaleStore.setVisibility(View.VISIBLE); binding.tvSalePaymentMethod.setVisibility(View.VISIBLE); - - // Show refund button only if it's not already a refund - binding.btnRefundSale.setVisibility(isRefund ? View.GONE : View.VISIBLE); + + binding.btnRefundSale.setVisibility((isRefund || isAdmin()) ? View.GONE : View.VISIBLE); } loadSaleDetails(); From 884f56c9a7ad66a6d0aac0de5a3aeb7ab8687a44 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:25:11 -0600 Subject: [PATCH 04/12] Can now edit loyalty points for customer on andriod, and pets now have breed dropdown --- .../example/petstoremobile/api/PetApi.java | 3 ++ .../petstoremobile/dtos/CustomerDTO.java | 4 ++ .../CustomerDetailFragment.java | 24 ++++++++--- .../detailfragments/PetDetailFragment.java | 31 ++++++++++++-- .../detailfragments/StaffDetailFragment.java | 2 +- .../repositories/PetRepository.java | 4 ++ .../viewmodels/CustomerDetailViewModel.java | 13 ++++++ .../viewmodels/PetDetailViewModel.java | 42 ++++++++++++++++++- .../viewmodels/StaffDetailViewModel.java | 8 ++++ .../res/layout/fragment_customer_detail.xml | 11 +++-- .../main/res/layout/fragment_pet_detail.xml | 9 ++-- .../petshop/backend/dto/user/UserRequest.java | 10 +++++ .../petshop/backend/service/UserService.java | 6 +++ 13 files changed, 143 insertions(+), 24 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java index b03ab44d..b554166b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -50,6 +50,9 @@ public interface PetApi { @GET("api/v1/dropdowns/pet-species") Call> getPetSpeciesDropdowns(); + @GET("api/v1/dropdowns/pet-breeds") + Call> getPetBreedsDropdowns(@Query("species") String species); + // Get pet by id @GET("api/v1/pets/{id}") Call getPetById(@Path("id") Long id); diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java index 52d96a1e..c190342e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java @@ -17,6 +17,7 @@ public class CustomerDTO { private String createdAt; private String updatedAt; private String password; + private String role; public CustomerDTO() {} @@ -73,4 +74,7 @@ public class CustomerDTO { public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } + + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java index f7a8c5a3..455450cc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java @@ -14,8 +14,11 @@ import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.CustomerDetailViewModel; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.utils.Resource; +import javax.inject.Inject; + import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint @@ -24,6 +27,8 @@ public class CustomerDetailFragment extends Fragment { private FragmentCustomerDetailBinding binding; private CustomerDetailViewModel viewModel; + @Inject TokenManager tokenManager; + private final String[] STATUSES = {"Active", "Inactive"}; @Override @@ -65,7 +70,10 @@ public class CustomerDetailFragment extends Fragment { // Show loyalty points binding.tvLoyaltyPointsLabel.setVisibility(View.VISIBLE); - binding.tvCustomerLoyaltyPoints.setVisibility(View.VISIBLE); + binding.etCustomerLoyaltyPoints.setVisibility(View.VISIBLE); + + boolean isAdmin = "ADMIN".equalsIgnoreCase(tokenManager.getRole()); + binding.etCustomerLoyaltyPoints.setEnabled(isAdmin); loadCustomerData(customerId); } else { @@ -74,7 +82,7 @@ public class CustomerDetailFragment extends Fragment { binding.btnDeleteCustomer.setVisibility(View.GONE); binding.tvCustomerId.setVisibility(View.GONE); binding.tvLoyaltyPointsLabel.setVisibility(View.GONE); - binding.tvCustomerLoyaltyPoints.setVisibility(View.GONE); + binding.etCustomerLoyaltyPoints.setVisibility(View.GONE); } } @@ -91,7 +99,7 @@ public class CustomerDetailFragment extends Fragment { binding.etCustomerPhone.setText(c.getPhone() != null ? c.getPhone() : ""); binding.spinnerCustomerStatus.setSelection(Boolean.TRUE.equals(c.getActive()) ? 0 : 1); int pts = c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0; - binding.tvCustomerLoyaltyPoints.setText(String.valueOf(pts)); + binding.etCustomerLoyaltyPoints.setText(String.valueOf(pts)); } } }); @@ -121,6 +129,12 @@ public class CustomerDetailFragment extends Fragment { if (!InputValidator.isValidEmail(binding.etCustomerEmail)) return; if (!InputValidator.isValidPhone(binding.etCustomerPhone)) return; + Integer loyaltyPoints = null; + if (viewModel.isEditing()) { + if (!InputValidator.isPositiveInteger(binding.etCustomerLoyaltyPoints, "Loyalty Points")) return; + loyaltyPoints = Integer.parseInt(binding.etCustomerLoyaltyPoints.getText().toString().trim()); + } + String username = binding.etCustomerUsername.getText().toString().trim(); String password = viewModel.isEditing() ? null : binding.etCustomerPassword.getText().toString().trim(); String firstName = binding.etCustomerFirstName.getText().toString().trim(); @@ -129,9 +143,7 @@ public class CustomerDetailFragment extends Fragment { String phone = binding.etCustomerPhone.getText().toString().trim(); boolean active = binding.spinnerCustomerStatus.getSelectedItemPosition() == 0; - CustomerDTO dto = new CustomerDTO(username, password, firstName, lastName, email, phone); - dto.setFullName(firstName + " " + lastName); - dto.setActive(active); + CustomerDTO dto = viewModel.createCustomerDto(username, password, firstName, lastName, email, phone, active, loyaltyPoints); viewModel.saveCustomer(dto).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { 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 2d5f1575..69ff8215 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 @@ -101,6 +101,14 @@ public class PetDetailFragment extends Fragment { DropdownDTO::getLabel, "-- Select Species --", null, DropdownDTO::getId); SpinnerUtils.setSelectionByValue(binding.spinnerPetSpecies, selectedSpecies); }); + + viewModel.getBreedList().observe(getViewLifecycleOwner(), list -> { + PetDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + String selectedBreed = state != null ? state.selectedBreed : null; + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPetBreed, list, + DropdownDTO::getLabel, "-- Select Breed --", null, DropdownDTO::getId); + SpinnerUtils.setSelectionByValue(binding.spinnerPetBreed, selectedBreed); + }); } private void setLoading(boolean loading) { @@ -118,7 +126,7 @@ public class PetDetailFragment extends Fragment { private void savePet() { if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerPetSpecies, "Species")) return; - if (!InputValidator.isNotEmpty(binding.etPetBreed, "Breed")) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerPetBreed, "Breed")) return; if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return; if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return; @@ -127,7 +135,11 @@ public class PetDetailFragment extends Fragment { String species = (speciesOptions != null && binding.spinnerPetSpecies.getSelectedItemPosition() > 0) ? speciesOptions.get(binding.spinnerPetSpecies.getSelectedItemPosition() - 1).getLabel() : ""; - String breed = binding.etPetBreed.getText().toString().trim(); + + List breedOptions = viewModel.getBreedList().getValue(); + String breed = (breedOptions != null && binding.spinnerPetBreed.getSelectedItemPosition() > 0) + ? breedOptions.get(binding.spinnerPetBreed.getSelectedItemPosition() - 1).getLabel() + : ""; int age = Integer.parseInt(binding.etPetAge.getText().toString().trim()); double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim()); String status = binding.spinnerPetStatus.getSelectedItem().toString(); @@ -236,7 +248,6 @@ public class PetDetailFragment extends Fragment { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { PetDTO p = resource.data; binding.etPetName.setText(p.getPetName()); - binding.etPetBreed.setText(p.getPetBreed()); binding.etPetAge.setText(String.valueOf(p.getPetAge())); if (p.getPetPrice() != null) { binding.etPetPrice.setText(String.format(Locale.getDefault(), "%.2f", p.getPetPrice())); @@ -279,6 +290,11 @@ public class PetDetailFragment extends Fragment { viewModel.onSpeciesSelected(p); }); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPetBreed, p -> { + if (isUpdatingUI) return; + viewModel.onBreedSelected(p); + }); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerCustomer, p -> { if (isUpdatingUI) return; viewModel.onCustomerSelected(p); @@ -306,7 +322,7 @@ public class PetDetailFragment extends Fragment { binding.btnSavePet.setText(state.saveButtonText); UIUtils.setViewsEnabled(state.isSpeciesEnabled, binding.spinnerPetSpecies); - UIUtils.setViewsEnabled(state.isBreedEnabled, binding.etPetBreed); + UIUtils.setViewsEnabled(state.isBreedEnabled, binding.spinnerPetBreed); UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer); UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore); @@ -323,6 +339,13 @@ public class PetDetailFragment extends Fragment { SpinnerUtils.setSelectionByValue(binding.spinnerPetSpecies, state.selectedSpecies); } + List breeds = viewModel.getBreedList().getValue(); + if (breeds != null) { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPetBreed, breeds, + DropdownDTO::getLabel, "-- Select Breed --", null, DropdownDTO::getId); + SpinnerUtils.setSelectionByValue(binding.spinnerPetBreed, state.selectedBreed); + } + if (!state.isCustomerEnabled && binding.spinnerCustomer.getSelectedItemPosition() != 0) { binding.spinnerCustomer.setSelection(0); } 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 acf171ac..94b2b307 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 @@ -161,7 +161,7 @@ public class StaffDetailFragment extends Fragment { List stores = viewModel.getStoreList().getValue(); Long storeId = stores.get(binding.spinnerStaffStore.getSelectedItemPosition() - 1).getId(); - EmployeeDTO dto = new EmployeeDTO( + EmployeeDTO dto = viewModel.createEmployeeDto( username, password.isEmpty() ? null : password, firstName, diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index f3114beb..11bb9ff1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -58,6 +58,10 @@ public class PetRepository extends BaseRepository { return executeCall(petApi.getPetSpeciesDropdowns()); } + public LiveData>> getPetBreedsDropdowns(String species) { + return executeCall(petApi.getPetBreedsDropdowns(species)); + } + /** * Retrieves available pets for a specific store. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java index e7864b66..0d0ebfaf 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java @@ -31,6 +31,19 @@ public class CustomerDetailViewModel extends ViewModel { public long getCustomerId() { return customerId; } public boolean isEditing() { return isEditing; } + public CustomerDTO createCustomerDto(String username, String password, String firstName, + String lastName, String email, String phone, + boolean active, Integer loyaltyPoints) { + CustomerDTO dto = new CustomerDTO(username, password, firstName, lastName, email, phone); + dto.setFullName(firstName + " " + lastName); + dto.setActive(active); + dto.setRole("CUSTOMER"); + if (isEditing && loyaltyPoints != null) { + dto.setLoyaltyPoints(loyaltyPoints); + } + return dto; + } + public LiveData> loadCustomer(long id) { return repository.getCustomerById(id); } 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 25eddbbb..ddb868a7 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 @@ -34,6 +34,7 @@ public class PetDetailViewModel extends ViewModel { private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> speciesList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> breedList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); @@ -41,6 +42,7 @@ public class PetDetailViewModel extends ViewModel { private Long selectedCustomerId = null; private Long selectedStoreId = null; private String selectedSpecies = null; + private String selectedBreed = null; private boolean isOriginallyOwnedOrAdopted = false; private Long originalCustomerId = null; @@ -112,10 +114,33 @@ public class PetDetailViewModel extends ViewModel { List list = speciesList.getValue(); if (position > 0 && list != null && position <= list.size()) { selectedSpecies = list.get(position - 1).getLabel(); + loadBreeds(selectedSpecies); } else { selectedSpecies = null; + breedList.setValue(new ArrayList<>()); } - updateViewState(state -> state.selectedSpecies = selectedSpecies); + updateViewState(state -> { + state.selectedSpecies = selectedSpecies; + state.isBreedEnabled = !state.isEditing && (selectedSpecies != null); + }); + } + + public void onBreedSelected(int position) { + List list = breedList.getValue(); + if (position > 0 && list != null && position <= list.size()) { + selectedBreed = list.get(position - 1).getLabel(); + } else { + selectedBreed = null; + } + updateViewState(state -> state.selectedBreed = selectedBreed); + } + + private void loadBreeds(String species) { + observeOnce(petRepository.getPetBreedsDropdowns(species), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + breedList.setValue(resource.data); + } + }); } public void onStoreSelected(int position) { @@ -155,12 +180,15 @@ public class PetDetailViewModel extends ViewModel { selectedCustomerId = null; selectedStoreId = null; selectedSpecies = null; + selectedBreed = null; state.selectedCustomerId = null; state.selectedStoreId = null; state.selectedSpecies = null; + state.selectedBreed = null; state.selectedStatus = STATUS_AVAILABLE; state.isCustomerEnabled = false; state.isStoreEnabled = true; + state.isBreedEnabled = false; } }); } @@ -173,15 +201,22 @@ public class PetDetailViewModel extends ViewModel { selectedCustomerId = pet.getCustomerId(); selectedStoreId = pet.getStoreId(); selectedSpecies = pet.getPetSpecies(); + selectedBreed = pet.getPetBreed(); isOriginallyOwnedOrAdopted = STATUS_OWNED.equalsIgnoreCase(pet.getPetStatus()) || STATUS_ADOPTED.equalsIgnoreCase(pet.getPetStatus()); originalCustomerId = pet.getCustomerId(); + if (selectedSpecies != null) { + loadBreeds(selectedSpecies); + } + updateViewState(state -> { state.selectedCustomerId = selectedCustomerId; state.selectedStoreId = selectedStoreId; state.selectedSpecies = selectedSpecies; + state.selectedBreed = selectedBreed; state.selectedStatus = normalizeStatus(pet.getPetStatus()); + state.isBreedEnabled = !state.isEditing && (selectedSpecies != null); applyStatusRules(state, false); }); } @@ -216,6 +251,10 @@ public class PetDetailViewModel extends ViewModel { return speciesList; } + public LiveData> getBreedList() { + return breedList; + } + public LiveData getIsLoading() { return isLoading; } @@ -295,6 +334,7 @@ public class PetDetailViewModel extends ViewModel { public String[] availableStatuses = new String[]{STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED, STATUS_PENDING}; public String selectedStatus = STATUS_AVAILABLE; public String selectedSpecies = null; + public String selectedBreed = null; public Long selectedCustomerId = null; public Long selectedStoreId = null; } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java index cbee3bc3..7927465e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java @@ -60,6 +60,14 @@ public class StaffDetailViewModel extends ViewModel { return isEditing; } + public EmployeeDTO createEmployeeDto(String username, String password, String firstName, + String lastName, String email, String phone, + String role, String staffRole, boolean active, Long storeId) { + EmployeeDTO dto = new EmployeeDTO(username, password, firstName, lastName, email, phone, role, staffRole, active, storeId); + dto.setFullName(firstName + " " + lastName); + return dto; + } + public LiveData> saveEmployee(EmployeeDTO dto) { if (isEditing && employeeId > 0) { return repository.updateEmployee(employeeId, dto); diff --git a/android/app/src/main/res/layout/fragment_customer_detail.xml b/android/app/src/main/res/layout/fragment_customer_detail.xml index a2c63030..83033dc5 100644 --- a/android/app/src/main/res/layout/fragment_customer_detail.xml +++ b/android/app/src/main/res/layout/fragment_customer_detail.xml @@ -195,14 +195,13 @@ android:textSize="12sp" android:layout_marginBottom="4dp"/> - + android:hint="0" + android:inputType="number" + android:layout_marginBottom="16dp"/> diff --git a/android/app/src/main/res/layout/fragment_pet_detail.xml b/android/app/src/main/res/layout/fragment_pet_detail.xml index e9018362..e42d8e30 100644 --- a/android/app/src/main/res/layout/fragment_pet_detail.xml +++ b/android/app/src/main/res/layout/fragment_pet_detail.xml @@ -109,14 +109,11 @@ android:textSize="12sp" android:layout_marginBottom="4dp"/> - + android:layout_marginBottom="16dp"/> Date: Mon, 13 Apr 2026 18:52:07 -0600 Subject: [PATCH 05/12] added loyaltypoint usage to sales unfinished still needs to work with the backend --- .../example/petstoremobile/dtos/SaleDTO.java | 18 ++++ .../detailfragments/SaleDetailFragment.java | 85 +++++++++++++++---- .../viewmodels/SaleDetailViewModel.java | 54 ++++++++++++ .../main/res/layout/fragment_sale_detail.xml | 46 +++++++++- 4 files changed, 184 insertions(+), 19 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java index f309978f..8b2849da 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java @@ -17,6 +17,8 @@ public class SaleDTO { private BigDecimal subtotalAmount; private BigDecimal couponDiscountAmount; private BigDecimal employeeDiscountAmount; + private BigDecimal loyaltyDiscountAmount; + private Integer pointsUsed; private String paymentMethod; private String channel; private Boolean isRefund; @@ -78,6 +80,22 @@ public class SaleDTO { return employeeDiscountAmount; } + public BigDecimal getLoyaltyDiscountAmount() { + return loyaltyDiscountAmount; + } + + public void setLoyaltyDiscountAmount(BigDecimal loyaltyDiscountAmount) { + this.loyaltyDiscountAmount = loyaltyDiscountAmount; + } + + public Integer getPointsUsed() { + return pointsUsed; + } + + public void setPointsUsed(Integer pointsUsed) { + this.pointsUsed = pointsUsed; + } + public String getPaymentMethod() { return paymentMethod; } 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 9b94a168..3acde9a4 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.DateTimeUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.UIUtils; @@ -81,7 +82,7 @@ public class SaleDetailFragment extends Fragment { if (isStaff()) { UIUtils.setViewsEnabled(false, binding.spinnerSaleStore); if (primaryStoreId == null) { - Toast.makeText(requireContext(), "No store assigned to your account. Contact an admin.", Toast.LENGTH_LONG).show(); + UIUtils.showToast(requireContext(), "No store assigned to your account. Contact an admin."); } } }); @@ -100,6 +101,17 @@ public class SaleDetailFragment extends Fragment { renderCartItems(); updateTotal(); }); + + viewModel.getSelectedCustomerData().observe(getViewLifecycleOwner(), customer -> { + if (customer != null && !viewModel.isViewOnly()) { + binding.llLoyaltyPoints.setVisibility(View.VISIBLE); + binding.tvAvailablePoints.setText("(Available: " + customer.getLoyaltyPoints() + ")"); + binding.cbUseLoyaltyPoints.setEnabled(customer.getLoyaltyPoints() >= 20); + } else { + binding.llLoyaltyPoints.setVisibility(View.GONE); + binding.cbUseLoyaltyPoints.setChecked(false); + } + }); } private void handleArguments() { @@ -109,8 +121,8 @@ public class SaleDetailFragment extends Fragment { boolean viewOnly = a.getBoolean("viewOnly", false); viewModel.setSaleId(saleId, viewOnly); - binding.tvSaleMode.setText("Sale #" + saleId); - binding.tvSaleDetailId.setText("ID: " + saleId); + binding.tvSaleMode.setText("Sale #" + DateTimeUtils.formatId(saleId)); + binding.tvSaleDetailId.setText("ID: " + DateTimeUtils.formatId(saleId)); boolean isRefund = a.getBoolean("isRefund", false); if (isRefund || isAdmin()) { @@ -176,23 +188,30 @@ public class SaleDetailFragment extends Fragment { 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())); + binding.tvSaleDetailTotal.setText("Total: $" + String.format(Locale.getDefault(), "%.2f", sale.getTotalAmount())); + binding.tvSaleSubtotal.setText("$" + String.format(Locale.getDefault(), "%.2f", (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount()))); if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { binding.llCouponDiscount.setVisibility(View.VISIBLE); - binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount()); + binding.tvSaleCouponDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getCouponDiscountAmount())); } else { binding.llCouponDiscount.setVisibility(View.GONE); } if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { binding.llEmployeeDiscount.setVisibility(View.VISIBLE); - binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount()); + binding.tvSaleEmployeeDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getEmployeeDiscountAmount())); } else { binding.llEmployeeDiscount.setVisibility(View.GONE); } + if (sale.getLoyaltyDiscountAmount() != null && sale.getLoyaltyDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + binding.llLoyaltyDiscount.setVisibility(View.VISIBLE); + binding.tvSaleLoyaltyDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getLoyaltyDiscountAmount())); + } else { + binding.llLoyaltyDiscount.setVisibility(View.GONE); + } + binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "—"); binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0)); binding.tvSaleStore.setText(sale.getStoreName() != null ? sale.getStoreName() : "—"); @@ -219,7 +238,7 @@ public class SaleDetailFragment extends Fragment { binding.btnApplyCoupon.setOnClickListener(v -> { String code = binding.etCouponCode.getText().toString().trim(); if (code.isEmpty()) { - Toast.makeText(getContext(), "Enter a coupon code", Toast.LENGTH_SHORT).show(); + UIUtils.showToast(getContext(), "Enter a coupon code"); return; } setLoading(true); @@ -289,7 +308,7 @@ public class SaleDetailFragment extends Fragment { for (SaleDTO.SaleItemDTO existing : viewModel.getCartItems().getValue()) { if (existing.getProdId().equals(product.getProdId())) { - Toast.makeText(getContext(), "Product already added", Toast.LENGTH_SHORT).show(); + UIUtils.showToast(getContext(), "Product already added"); return; } } @@ -297,6 +316,19 @@ public class SaleDetailFragment extends Fragment { viewModel.addToCart(new SaleDTO.SaleItemDTO(product.getProdId(), qty)); binding.etSaleQuantity.setText(""); }); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerSaleCustomer, p -> { + if (p > 0) { + Long id = viewModel.getCustomerList().getValue().get(p - 1).getId(); + viewModel.selectCustomer(id); + } else { + viewModel.selectCustomer(null); + } + }); + + binding.cbUseLoyaltyPoints.setOnCheckedChangeListener((v, checked) -> { + viewModel.setUseLoyaltyPoints(checked); + updateTotal(); + }); } private void renderCartItems() { @@ -336,7 +368,7 @@ public class SaleDetailFragment extends Fragment { TextView tvPrice = new TextView(getContext()); tvPrice.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); - tvPrice.setText(price != null ? "$" + price : ""); + tvPrice.setText(price != null ? "$" + String.format(Locale.getDefault(), "%.2f", price) : ""); row.addView(tvName); row.addView(tvQty); @@ -360,23 +392,35 @@ public class SaleDetailFragment extends Fragment { private void updateTotal() { BigDecimal subtotal = viewModel.calculateSubtotal(); - BigDecimal discount = viewModel.calculateDiscount(); - BigDecimal total = subtotal.subtract(discount); - binding.tvSaleSubtotal.setText("$" + subtotal); - if (discount.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal couponDiscount = viewModel.calculateCouponDiscount(); + BigDecimal loyaltyDiscount = viewModel.calculateLoyaltyDiscount(); + BigDecimal total = subtotal.subtract(couponDiscount).subtract(loyaltyDiscount); + + binding.tvSaleSubtotal.setText("$" + String.format(Locale.getDefault(), "%.2f", subtotal)); + + if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) { binding.llCouponDiscount.setVisibility(View.VISIBLE); - binding.tvSaleCouponDiscount.setText("-$" + discount); + binding.tvSaleCouponDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", couponDiscount)); } else { binding.llCouponDiscount.setVisibility(View.GONE); } - binding.tvSaleDetailTotal.setText("Total: $" + total); + + if (loyaltyDiscount.compareTo(BigDecimal.ZERO) > 0) { + binding.llLoyaltyDiscount.setVisibility(View.VISIBLE); + binding.tvLoyaltyDiscountLabel.setText("Loyalty Discount (" + viewModel.calculatePointsToUse() + " pts):"); + binding.tvSaleLoyaltyDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", loyaltyDiscount)); + } else { + binding.llLoyaltyDiscount.setVisibility(View.GONE); + } + + binding.tvSaleDetailTotal.setText("Total: $" + String.format(Locale.getDefault(), "%.2f", total)); } private void saveSale() { 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(); + UIUtils.showToast(getContext(), "Add at least one item"); return; } @@ -390,12 +434,17 @@ public class SaleDetailFragment extends Fragment { SaleDTO dto = new SaleDTO(store.getId(), payment, viewModel.getCartItems().getValue(), false, null, customerId); dto.setCouponId(viewModel.getAppliedCouponId()); + + if (Boolean.TRUE.equals(viewModel.getUseLoyaltyPoints().getValue())) { + dto.setPointsUsed(viewModel.calculatePointsToUse()); + dto.setLoyaltyDiscountAmount(viewModel.calculateLoyaltyDiscount()); + } 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(); + UIUtils.showToast(getContext(), "Sale saved!"); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message); 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 658264c9..68369cce 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 @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.CouponDTO; +import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.SaleDTO; @@ -39,6 +40,8 @@ public class SaleDetailViewModel extends ViewModel { private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> cartItems = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData appliedCoupon = new MutableLiveData<>(null); + private final MutableLiveData selectedCustomerData = new MutableLiveData<>(null); + private final MutableLiveData useLoyaltyPoints = new MutableLiveData<>(false); private final MutableLiveData isLoading = new MutableLiveData<>(false); @Inject @@ -111,6 +114,34 @@ public class SaleDetailViewModel extends ViewModel { appliedCoupon.setValue(coupon); } + public void setUseLoyaltyPoints(boolean use) { + useLoyaltyPoints.setValue(use); + } + + public LiveData getUseLoyaltyPoints() { + return useLoyaltyPoints; + } + + public LiveData getSelectedCustomerData() { + return selectedCustomerData; + } + + public void selectCustomer(Long customerId) { + if (customerId == null) { + selectedCustomerData.setValue(null); + useLoyaltyPoints.setValue(false); + return; + } + customerRepository.getCustomerById(customerId).observeForever(new androidx.lifecycle.Observer>() { + @Override + public void onChanged(Resource resource) { + if (resource != null && resource.status == Resource.Status.SUCCESS) { + selectedCustomerData.setValue(resource.data); + } + } + }); + } + public void clearCoupon() { appliedCoupon.setValue(null); } @@ -125,6 +156,10 @@ public class SaleDetailViewModel extends ViewModel { } public BigDecimal calculateDiscount() { + return calculateCouponDiscount().add(calculateLoyaltyDiscount()); + } + + public BigDecimal calculateCouponDiscount() { CouponDTO coupon = appliedCoupon.getValue(); if (coupon == null || coupon.getDiscountValue() == null) return BigDecimal.ZERO; BigDecimal subtotal = calculateSubtotal(); @@ -135,6 +170,25 @@ public class SaleDetailViewModel extends ViewModel { } } + public BigDecimal calculateLoyaltyDiscount() { + if (Boolean.FALSE.equals(useLoyaltyPoints.getValue())) return BigDecimal.ZERO; + CustomerDTO customer = selectedCustomerData.getValue(); + if (customer == null || customer.getLoyaltyPoints() == null || customer.getLoyaltyPoints() < 20) { + return BigDecimal.ZERO; + } + + BigDecimal subtotalAfterCoupon = calculateSubtotal().subtract(calculateCouponDiscount()); + int maxPointsNeeded = subtotalAfterCoupon.multiply(BigDecimal.valueOf(20)).intValue(); + int pointsToUse = Math.min(customer.getLoyaltyPoints(), maxPointsNeeded); + + return BigDecimal.valueOf(pointsToUse).multiply(BigDecimal.valueOf(0.05)).setScale(2, java.math.RoundingMode.HALF_UP); + } + + public int calculatePointsToUse() { + BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(); + return loyaltyDiscount.divide(BigDecimal.valueOf(0.05), 0, java.math.RoundingMode.HALF_UP).intValue(); + } + public BigDecimal calculateSubtotal() { BigDecimal total = BigDecimal.ZERO; List items = cartItems.getValue(); diff --git a/android/app/src/main/res/layout/fragment_sale_detail.xml b/android/app/src/main/res/layout/fragment_sale_detail.xml index 3e5825e2..e92f83d4 100644 --- a/android/app/src/main/res/layout/fragment_sale_detail.xml +++ b/android/app/src/main/res/layout/fragment_sale_detail.xml @@ -128,7 +128,30 @@ android:id="@+id/spinnerSaleCustomer" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="16dp"/> + android:layout_marginBottom="8dp"/> + + + + + + + + + + Date: Mon, 13 Apr 2026 19:46:28 -0600 Subject: [PATCH 06/12] added points to sale and logic backend --- .../petshop/backend/dto/sale/SaleRequest.java | 21 ++++++++ .../backend/dto/sale/SaleResponse.java | 18 +++++++ .../java/com/petshop/backend/entity/Sale.java | 22 ++++++++ .../petshop/backend/service/SaleService.java | 53 +++++++++++++++++-- .../V5__add_points_columns_to_sale.sql | 3 ++ .../dev/final-target/final_target_schema.sql | 2 + 6 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V5__add_points_columns_to_sale.sql diff --git a/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java index 081ab05d..97248688 100644 --- a/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java @@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.Objects; +import java.math.BigDecimal; public class SaleRequest { @NotNull(message = "Store ID is required") @@ -28,6 +29,10 @@ public class SaleRequest { private Long cartId; + private Integer pointsUsed; + + private BigDecimal pointsDiscountAmount; + public Long getStoreId() { return storeId; } @@ -100,6 +105,22 @@ public class SaleRequest { this.cartId = cartId; } + public Integer getPointsUsed() { + return pointsUsed; + } + + public void setPointsUsed(Integer pointsUsed) { + this.pointsUsed = pointsUsed; + } + + public BigDecimal getPointsDiscountAmount() { + return pointsDiscountAmount; + } + + public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) { + this.pointsDiscountAmount = pointsDiscountAmount; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java index c1f2357f..eaebd4e8 100644 --- a/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java @@ -19,6 +19,8 @@ public class SaleResponse { private BigDecimal couponDiscountAmount; private BigDecimal employeeDiscountAmount; private Integer pointsEarned; + private Integer pointsUsed; + private BigDecimal pointsDiscountAmount; private String channel; private Long couponId; private Long cartId; @@ -135,6 +137,22 @@ public class SaleResponse { this.pointsEarned = pointsEarned; } + public Integer getPointsUsed() { + return pointsUsed; + } + + public void setPointsUsed(Integer pointsUsed) { + this.pointsUsed = pointsUsed; + } + + public BigDecimal getPointsDiscountAmount() { + return pointsDiscountAmount; + } + + public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) { + this.pointsDiscountAmount = pointsDiscountAmount; + } + public String getChannel() { return channel; } diff --git a/backend/src/main/java/com/petshop/backend/entity/Sale.java b/backend/src/main/java/com/petshop/backend/entity/Sale.java index 3bf4d8bf..f994a855 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Sale.java +++ b/backend/src/main/java/com/petshop/backend/entity/Sale.java @@ -69,6 +69,12 @@ public class Sale { @Column(nullable = false) private Integer pointsEarned = 0; + @Column(nullable = false) + private Integer pointsUsed = 0; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal pointsDiscountAmount = BigDecimal.ZERO; + @OneToMany(mappedBy = "sale", cascade = CascadeType.ALL) private List items = new ArrayList<>(); @@ -211,6 +217,22 @@ public class Sale { this.pointsEarned = pointsEarned; } + public Integer getPointsUsed() { + return pointsUsed; + } + + public void setPointsUsed(Integer pointsUsed) { + this.pointsUsed = pointsUsed; + } + + public BigDecimal getPointsDiscountAmount() { + return pointsDiscountAmount; + } + + public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) { + this.pointsDiscountAmount = pointsDiscountAmount; + } + public List getItems() { return items; } diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index 9d49972f..526f5a57 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -152,9 +152,33 @@ public class SaleService { saleItems.add(saleItem); subtotalAmount = subtotalAmount.add(itemTotal); } - subtotalAmount = subtotalAmount.negate(); - sale.setSubtotalAmount(subtotalAmount); - sale.setTotalAmount(subtotalAmount); + + Sale originalSale = sale.getOriginalSale(); + BigDecimal originalSubtotal = originalSale.getSubtotalAmount() != null + ? originalSale.getSubtotalAmount() + : originalSale.getItems().stream() + .map(i -> i.getUnitPrice().multiply(BigDecimal.valueOf(Math.abs(i.getQuantity())))) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + BigDecimal refundRatio = originalSubtotal.compareTo(BigDecimal.ZERO) != 0 + ? subtotalAmount.divide(originalSubtotal, 10, RoundingMode.HALF_UP) + : BigDecimal.ONE; + + BigDecimal refundTotal = originalSale.getTotalAmount().abs() + .multiply(refundRatio).setScale(2, RoundingMode.HALF_UP); + + sale.setSubtotalAmount(subtotalAmount.negate()); + sale.setTotalAmount(refundTotal.negate()); + + User refundCustomer = customer != null ? customer : originalSale.getCustomer(); + if (refundCustomer != null) { + int pointsToRestore = BigDecimal.valueOf(originalSale.getPointsUsed()) + .multiply(refundRatio).setScale(0, RoundingMode.FLOOR).intValue(); + int pointsToDeduct = BigDecimal.valueOf(originalSale.getPointsEarned()) + .multiply(refundRatio).setScale(0, RoundingMode.FLOOR).intValue(); + refundCustomer.setLoyaltyPoints(refundCustomer.getLoyaltyPoints() + pointsToRestore - pointsToDeduct); + userRepository.save(refundCustomer); + } } else { for (var itemRequest : request.getItems()) { Product product = productRepository.findById(itemRequest.getProdId()) @@ -188,10 +212,23 @@ public class SaleService { BigDecimal couponDiscount = calculateCouponDiscount(sale.getCoupon(), subtotalAmount); sale.setCouponDiscountAmount(couponDiscount); - BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount)); + BigDecimal pointsDiscount = BigDecimal.ZERO; + int pointsUsed = 0; + if (customer != null && request.getPointsUsed() != null && request.getPointsUsed() > 0) { + if (customer.getLoyaltyPoints() < request.getPointsUsed()) { + throw new BusinessException("Customer does not have enough loyalty points"); + } + pointsUsed = request.getPointsUsed(); + pointsDiscount = calculatePointsDiscount(pointsUsed); + customer.setLoyaltyPoints(customer.getLoyaltyPoints() - pointsUsed); + } + sale.setPointsUsed(pointsUsed); + sale.setPointsDiscountAmount(pointsDiscount); + + BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount).subtract(pointsDiscount)); sale.setEmployeeDiscountAmount(employeeDiscount); - BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount); + BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(pointsDiscount).subtract(employeeDiscount); sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO)); sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue()); @@ -240,6 +277,10 @@ public class SaleService { return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP); } + private BigDecimal calculatePointsDiscount(int pointsUsed) { + return new BigDecimal(pointsUsed).divide(new BigDecimal("20"), 2, RoundingMode.HALF_UP); + } + private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) { if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) { return BigDecimal.ZERO; @@ -274,6 +315,8 @@ public class SaleService { response.setCouponDiscountAmount(sale.getCouponDiscountAmount()); response.setEmployeeDiscountAmount(sale.getEmployeeDiscountAmount()); response.setPointsEarned(sale.getPointsEarned()); + response.setPointsUsed(sale.getPointsUsed()); + response.setPointsDiscountAmount(sale.getPointsDiscountAmount()); response.setChannel(sale.getChannel()); if (sale.getCoupon() != null) { response.setCouponId(sale.getCoupon().getCouponId()); diff --git a/backend/src/main/resources/db/migration/V5__add_points_columns_to_sale.sql b/backend/src/main/resources/db/migration/V5__add_points_columns_to_sale.sql new file mode 100644 index 00000000..288561ef --- /dev/null +++ b/backend/src/main/resources/db/migration/V5__add_points_columns_to_sale.sql @@ -0,0 +1,3 @@ +ALTER TABLE sale + ADD COLUMN pointsUsed INT NOT NULL DEFAULT 0, + ADD COLUMN pointsDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00; diff --git a/backend/src/main/resources/dev/final-target/final_target_schema.sql b/backend/src/main/resources/dev/final-target/final_target_schema.sql index 9b8d279d..f515888e 100644 --- a/backend/src/main/resources/dev/final-target/final_target_schema.sql +++ b/backend/src/main/resources/dev/final-target/final_target_schema.sql @@ -230,6 +230,8 @@ CREATE TABLE IF NOT EXISTS sale ( couponDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, employeeDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, pointsEarned INT NOT NULL DEFAULT 0, + pointsUsed INT NOT NULL DEFAULT 0, + pointsDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_sale_employee FOREIGN KEY (employeeId) REFERENCES users(id), From 572895efa96f2aa84d15bb803219b99595bbf016 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:46:52 -0600 Subject: [PATCH 07/12] added correct refund logic and points for sales --- .../example/petstoremobile/dtos/SaleDTO.java | 9 ++++++ .../detailfragments/RefundFragment.java | 10 ++----- .../detailfragments/SaleDetailFragment.java | 9 ++++-- .../viewmodels/RefundViewModel.java | 28 +++++++++++++++++++ 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java index 8b2849da..9dc76029 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java @@ -18,6 +18,7 @@ public class SaleDTO { private BigDecimal couponDiscountAmount; private BigDecimal employeeDiscountAmount; private BigDecimal loyaltyDiscountAmount; + private BigDecimal pointsDiscountAmount; private Integer pointsUsed; private String paymentMethod; private String channel; @@ -144,6 +145,14 @@ public class SaleDTO { return customerName; } + public BigDecimal getPointsDiscountAmount() { + return pointsDiscountAmount; + } + + public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) { + this.pointsDiscountAmount = pointsDiscountAmount; + } + // Nested SaleItemDTO public static class SaleItemDTO { private Long saleItemId; 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 f4e58fb5..0dbd985f 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 @@ -302,11 +302,7 @@ public class RefundFragment extends Fragment { } private void updateRefundTotal() { - BigDecimal total = BigDecimal.ZERO; - List cart = viewModel.getRefundCart().getValue(); - if (cart != null) { - for (RefundViewModel.RefundItem item : cart) total = total.add(item.getTotal()); - } + BigDecimal total = viewModel.calculateRefundTotal(); binding.tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP)); } @@ -321,9 +317,7 @@ public class RefundFragment extends Fragment { } String payment = PAYMENT_METHODS[binding.spinnerRefundPayment.getSelectedItemPosition()]; - BigDecimal total = BigDecimal.ZERO; - for (RefundViewModel.RefundItem item : viewModel.getRefundCart().getValue()) total = total.add(item.getTotal()); - final BigDecimal finalTotal = total; + final BigDecimal finalTotal = viewModel.calculateRefundTotal(); DialogUtils.showConfirmDialog(requireContext(), "Confirm Refund", "Process refund for Sale #" + viewModel.getCurrentSale().getSaleId() 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 3acde9a4..8a4dff16 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 @@ -205,7 +205,13 @@ public class SaleDetailFragment extends Fragment { binding.llEmployeeDiscount.setVisibility(View.GONE); } - if (sale.getLoyaltyDiscountAmount() != null && sale.getLoyaltyDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + if (sale.getPointsDiscountAmount() != null && sale.getPointsDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + binding.llLoyaltyDiscount.setVisibility(View.VISIBLE); + binding.tvSaleLoyaltyDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getPointsDiscountAmount())); + if (sale.getPointsUsed() != null) { + binding.tvLoyaltyDiscountLabel.setText("Loyalty Discount (" + sale.getPointsUsed() + " pts):"); + } + } else if (sale.getLoyaltyDiscountAmount() != null && sale.getLoyaltyDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { binding.llLoyaltyDiscount.setVisibility(View.VISIBLE); binding.tvSaleLoyaltyDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getLoyaltyDiscountAmount())); } else { @@ -437,7 +443,6 @@ public class SaleDetailFragment extends Fragment { if (Boolean.TRUE.equals(viewModel.getUseLoyaltyPoints().getValue())) { dto.setPointsUsed(viewModel.calculatePointsToUse()); - dto.setLoyaltyDiscountAmount(viewModel.calculateLoyaltyDiscount()); } viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { 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 222a1fa4..fab83dd9 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 @@ -10,6 +10,7 @@ import com.example.petstoremobile.repositories.SaleRepository; import com.example.petstoremobile.utils.Resource; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -125,6 +126,33 @@ public class RefundViewModel extends ViewModel { refundCart.setValue(cart); } + public BigDecimal calculateRefundTotal() { + SaleDTO sale = currentSale.getValue(); + List cart = refundCart.getValue(); + if (sale == null || cart == null || cart.isEmpty()) return BigDecimal.ZERO; + + BigDecimal cartSubtotal = BigDecimal.ZERO; + for (RefundItem item : cart) cartSubtotal = cartSubtotal.add(item.getTotal()); + + BigDecimal originalSubtotal = sale.getSubtotalAmount(); + if (originalSubtotal == null || originalSubtotal.compareTo(BigDecimal.ZERO) == 0) { + if (sale.getItems() != null) { + originalSubtotal = BigDecimal.ZERO; + for (SaleDTO.SaleItemDTO item : sale.getItems()) { + if (item.getUnitPrice() != null && item.getQuantity() != null) + originalSubtotal = originalSubtotal.add(item.getUnitPrice().multiply(BigDecimal.valueOf(Math.abs(item.getQuantity())))); + } + } + } + if (originalSubtotal == null || originalSubtotal.compareTo(BigDecimal.ZERO) == 0) return cartSubtotal; + + BigDecimal originalTotal = sale.getTotalAmount(); + if (originalTotal == null) return cartSubtotal; + + BigDecimal ratio = cartSubtotal.divide(originalSubtotal, 10, RoundingMode.HALF_UP); + return originalTotal.abs().multiply(ratio).setScale(2, RoundingMode.HALF_UP); + } + public LiveData> submitRefund(String paymentMethod) { SaleDTO sale = currentSale.getValue(); List cart = refundCart.getValue(); From 044e9ba7b2d3e7cd379f2fc53be88156bb0a2be8 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:49:22 -0600 Subject: [PATCH 08/12] made it so staff cannot change the status of pets for desktop for adopted or owned --- .../detailfragments/PetDetailFragment.java | 4 + .../PetProfileFragment.java | 4 +- .../backend/service/AdoptionService.java | 1 + .../api/dto/user/UserResponse.java | 9 + .../controllers/StaffAccountsController.java | 177 +++++++++++++----- .../PetDialogController.java | 80 ++++---- .../modelviews/staff-accounts-view.fxml | 140 ++++++++++---- 7 files changed, 292 insertions(+), 123 deletions(-) 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 69ff8215..a22e2c0d 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 @@ -164,6 +164,10 @@ public class PetDetailFragment extends Fragment { if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return; } + if ("Pending".equalsIgnoreCase(status)) { + if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return; + } PetDTO petDTO = new PetDTO(); petDTO.setPetName(name); 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 73da4613..496304a3 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 @@ -126,7 +126,7 @@ public class PetProfileFragment extends Fragment { String status = pet.getPetStatus(); - if ("Adopted".equalsIgnoreCase(status) || "Owned".equalsIgnoreCase(status)) { + if ("Adopted".equalsIgnoreCase(status) || "Owned".equalsIgnoreCase(status) || "Pending".equalsIgnoreCase(status)) { binding.layoutPetOwner.setVisibility(View.VISIBLE); if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) { binding.tvPetOwner.setText(pet.getCustomerName()); @@ -137,7 +137,7 @@ public class PetProfileFragment extends Fragment { binding.layoutPetOwner.setVisibility(View.GONE); } - if ("Available".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) { + if ("Available".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status) || "Pending".equalsIgnoreCase(status)) { binding.layoutPetStore.setVisibility(View.VISIBLE); if (pet.getStoreName() != null && !pet.getStoreName().isEmpty()) { binding.tvPetStore.setText(pet.getStoreName()); diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 134a78f2..0874aded 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -271,6 +271,7 @@ public class AdoptionService { pet.setStore(null); } else if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus)) { pet.setPetStatus(PET_STATUS_PENDING); + pet.setOwner(customer); } else { pet.setPetStatus(PET_STATUS_AVAILABLE); pet.setOwner(null); diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java index 3a42f128..f9fc4eac 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java @@ -10,6 +10,7 @@ public class UserResponse { private String phone; private String role; private Boolean active; + private Integer loyaltyPoints; private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -72,6 +73,14 @@ public class UserResponse { this.active = active; } + public Integer getLoyaltyPoints() { + return loyaltyPoints; + } + + public void setLoyaltyPoints(Integer loyaltyPoints) { + this.loyaltyPoints = loyaltyPoints; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java index ae749d4c..20dbf6d0 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java @@ -13,21 +13,55 @@ import javafx.scene.control.Label; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; import javafx.stage.Modality; import javafx.stage.Stage; import org.example.petshopdesktop.api.dto.user.UserResponse; -import org.example.petshopdesktop.api.endpoints.UserApi; import org.example.petshopdesktop.api.endpoints.CustomerApi; +import org.example.petshopdesktop.api.endpoints.UserApi; import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.TableViewSupport; -import java.util.List; import java.util.Comparator; +import java.util.List; import java.util.stream.Collectors; public class StaffAccountsController { + @FXML + private VBox staffSection; + + @FXML + private TableView tvCustomers; + + @FXML + private TableColumn colCustomerUsername; + + @FXML + private TableColumn colCustomerName; + + @FXML + private TableColumn colCustomerEmail; + + @FXML + private TableColumn colCustomerPhone; + + @FXML + private TableColumn colCustomerLoyaltyPoints; + + @FXML + private TableColumn colCustomerStatus; + + @FXML + private TableColumn colCustomerCreated; + + @FXML + private TextField txtSearchCustomer; + + @FXML + private Button btnEditCustomer; + @FXML private TableView tvStaff; @@ -55,12 +89,6 @@ public class StaffAccountsController { @FXML private TextField txtSearch; - @FXML - private Label lblError; - - @FXML - private Label lblStatus; - @FXML private Button btnRefresh; @@ -70,11 +98,38 @@ public class StaffAccountsController { @FXML private Button btnEditAccount; + @FXML + private Label lblError; + + @FXML + private Label lblStatus; + + private final ObservableList customerAccounts = FXCollections.observableArrayList(); + private FilteredList filteredCustomers; + private final ObservableList staffAccounts = FXCollections.observableArrayList(); - private FilteredList filtered; + private FilteredList filteredStaff; @FXML public void initialize() { + colCustomerUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername())); + colCustomerName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName())); + colCustomerEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail())); + colCustomerPhone.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getPhone())); + colCustomerLoyaltyPoints.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getLoyaltyPoints())); + colCustomerStatus.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getActive() != null && data.getValue().getActive() ? "Active" : "Inactive")); + colCustomerCreated.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getCreatedAt())); + + filteredCustomers = new FilteredList<>(customerAccounts, a -> true); + TableViewSupport.bindSortedItems(tvCustomers, filteredCustomers); + TableViewSupport.installDoubleClickAction(tvCustomers, this::openEditDialog); + + tvCustomers.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> + btnEditCustomer.setDisable(newVal == null)); + btnEditCustomer.setDisable(true); + + txtSearchCustomer.textProperty().addListener((obs, o, n) -> applyCustomerFilter(n)); + colUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername())); colName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName())); colEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail())); @@ -83,33 +138,39 @@ public class StaffAccountsController { colStatus.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getActive() != null && data.getValue().getActive() ? "Active" : "Inactive")); colCreated.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getCreatedAt())); - filtered = new FilteredList<>(staffAccounts, a -> true); - TableViewSupport.bindSortedItems(tvStaff, filtered); + filteredStaff = new FilteredList<>(staffAccounts, a -> true); + TableViewSupport.bindSortedItems(tvStaff, filteredStaff); TableViewSupport.installDoubleClickAction(tvStaff, this::openEditDialog); - txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); + tvStaff.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> + btnEditAccount.setDisable(newVal == null)); + btnEditAccount.setDisable(true); - tvStaff.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> { - if (btnEditAccount != null) { - btnEditAccount.setDisable(newValue == null); - } - }); + txtSearch.textProperty().addListener((obs, o, n) -> applyStaffFilter(n)); - if (btnEditAccount != null) { - btnEditAccount.setDisable(true); - } + boolean isAdmin = UserSession.getInstance().isAdmin(); + staffSection.setVisible(isAdmin); + staffSection.setManaged(isAdmin); refresh(); } @FXML void btnRefreshClicked(ActionEvent event) { + txtSearchCustomer.clear(); txtSearch.clear(); + TableViewSupport.clearSort(tvCustomers); TableViewSupport.clearSort(tvStaff); refresh(); TableViewSupport.flashStatus(lblStatus, "Refreshed"); } + @FXML + void btnEditCustomerClicked(ActionEvent event) { + lblError.setText(""); + openEditDialog(tvCustomers.getSelectionModel().getSelectedItem()); + } + @FXML void btnCreateAccountClicked(ActionEvent event) { lblError.setText(""); @@ -132,8 +193,7 @@ public class StaffAccountsController { @FXML void btnEditAccountClicked(ActionEvent event) { lblError.setText(""); - UserResponse selected = tvStaff.getSelectionModel().getSelectedItem(); - openEditDialog(selected); + openEditDialog(tvStaff.getSelectionModel().getSelectedItem()); } private void openEditDialog(UserResponse selected) { @@ -154,7 +214,8 @@ public class StaffAccountsController { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml")); Stage dialog = new Stage(); - dialog.initOwner(tvStaff.getScene().getWindow()); + Stage owner = (tvStaff.getScene() != null) ? (Stage) tvStaff.getScene().getWindow() : (Stage) tvCustomers.getScene().getWindow(); + dialog.initOwner(owner); dialog.initModality(Modality.APPLICATION_MODAL); dialog.setTitle("Edit User Account"); dialog.setScene(new Scene(loader.load())); @@ -164,31 +225,45 @@ public class StaffAccountsController { dialog.showAndWait(); refresh(); } catch (Exception e) { - ActivityLogger.getInstance().logException("StaffAccountsController.btnEditAccountClicked", e, "Opening user edit dialog"); + ActivityLogger.getInstance().logException("StaffAccountsController.openEditDialog", e, "Opening user edit dialog"); lblError.setText("Could not open user account editor."); } } private void refresh() { lblError.setText(""); + tvCustomers.setDisable(true); tvStaff.setDisable(true); new Thread(() -> { try { - UserSession session = UserSession.getInstance(); - List users; - if (session.isAdmin()) { - users = UserApi.getInstance().listUsers(null); + Comparator byCreated = Comparator.comparing( + UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder())); + + final List customers; + final List staff; + + if (UserSession.getInstance().isAdmin()) { + List allUsers = UserApi.getInstance().listUsers(null); + customers = allUsers.stream() + .filter(u -> "CUSTOMER".equalsIgnoreCase(u.getRole())) + .sorted(byCreated) + .collect(Collectors.toList()); + staff = allUsers.stream() + .filter(u -> !"CUSTOMER".equalsIgnoreCase(u.getRole())) + .sorted(byCreated) + .collect(Collectors.toList()); } else { - users = CustomerApi.getInstance().listCustomers(null); + customers = CustomerApi.getInstance().listCustomers(null).stream() + .sorted(byCreated) + .collect(Collectors.toList()); + staff = List.of(); } - List sortedUsers = users.stream() - .sorted(Comparator.comparing(UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder()))) - .collect(Collectors.toList()); - Platform.runLater(() -> { - staffAccounts.setAll(sortedUsers); + customerAccounts.setAll(customers); + staffAccounts.setAll(staff); + tvCustomers.setDisable(false); tvStaff.setDisable(false); }); } catch (Exception e) { @@ -196,27 +271,41 @@ public class StaffAccountsController { Platform.runLater(() -> { String message = e.getMessage(); lblError.setText(message == null || message.isBlank() - ? "Could not load user accounts." - : "Could not load user accounts: " + message); + ? "Could not load user accounts." + : "Could not load user accounts: " + message); + tvCustomers.setDisable(false); tvStaff.setDisable(false); }); } }).start(); } - private void applyFilter(String text) { + private void applyCustomerFilter(String text) { String q = text == null ? "" : text.trim().toLowerCase(); if (q.isEmpty()) { - filtered.setPredicate(a -> true); + filteredCustomers.setPredicate(a -> true); return; } + filteredCustomers.setPredicate(a -> + safe(a.getUsername()).contains(q) + || safe(a.getFullName()).contains(q) + || safe(a.getEmail()).contains(q) + || safe(a.getPhone()).contains(q) + ); + } - filtered.setPredicate(a -> - safe(a.getUsername()).contains(q) - || safe(a.getFullName()).contains(q) - || safe(a.getEmail()).contains(q) - || safe(a.getPhone()).contains(q) - || safe(a.getRole()).contains(q) + private void applyStaffFilter(String text) { + String q = text == null ? "" : text.trim().toLowerCase(); + if (q.isEmpty()) { + filteredStaff.setPredicate(a -> true); + return; + } + filteredStaff.setPredicate(a -> + safe(a.getUsername()).contains(q) + || safe(a.getFullName()).contains(q) + || safe(a.getEmail()).contains(q) + || safe(a.getPhone()).contains(q) + || safe(a.getRole()).contains(q) ); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java index bdb46dab..45b2978d 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java @@ -132,9 +132,7 @@ public class PetDialogController { } }); - setFieldVisibility(vbCustomerField, false); - setFieldVisibility(vbStoreField, false); - setFieldVisibility(vbPriceField, true); + updateStatusFieldVisibility(null); loadSpecies(); @@ -174,18 +172,18 @@ public class PetDialogController { String speciesValue = cbPetSpecies.getValue() != null ? cbPetSpecies.getValue().trim() : ""; if (speciesValue.isEmpty()) errorMsg += "Species is required\n"; String selectedStatus = cbPetStatus.getValue(); - boolean needsPrice = !("Owned".equalsIgnoreCase(selectedStatus) || "Adopted".equalsIgnoreCase(selectedStatus)); - if (needsPrice) { - errorMsg += Validator.isPresent(txtPetPrice.getText(), "Price"); - } + errorMsg += Validator.isPresent(txtPetPrice.getText(), "Price"); if (cbPetStatus.getSelectionModel().getSelectedItem() == null){ errorMsg += "Status is required"; } - if ("Owned".equalsIgnoreCase(selectedStatus) && cbCustomer.getValue() == null && UserSession.getInstance().isAdmin()) { - errorMsg += "Customer is required for Owned status\n"; + boolean needsCustomer = "Owned".equalsIgnoreCase(selectedStatus) + || "Adopted".equalsIgnoreCase(selectedStatus) + || "Pending".equalsIgnoreCase(selectedStatus); + boolean needsStore = requiresStore(selectedStatus); + if (needsCustomer && cbCustomer.getValue() == null && UserSession.getInstance().isAdmin()) { + errorMsg += "Customer is required for " + selectedStatus + " status\n"; } - boolean storeRequired = requiresStore(selectedStatus) && !"Adopted".equalsIgnoreCase(selectedStatus); - if (storeRequired && cbStore.getValue() == null) { + if (needsStore && cbStore.getValue() == null) { errorMsg += "Store is required for " + selectedStatus + " status\n"; } @@ -193,15 +191,11 @@ public class PetDialogController { errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50); errorMsg += Validator.isLessThanVarChars(speciesValue, "Species", 50); errorMsg += Validator.isLessThanVarChars(txtPetBreed.getText(), "Breed", 50); - if (needsPrice) { - errorMsg += Validator.isLessThanVarChars(txtPetPrice.getText(), "Price", 12); - } + errorMsg += Validator.isLessThanVarChars(txtPetPrice.getText(), "Price", 12); errorMsg += Validator.isLessThanVarChars(txtPetAge.getText(), "Age", 11); //Check validation (format) - if (needsPrice) { - errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price"); - } + errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price"); errorMsg += Validator.isPositiveInteger(txtPetAge.getText(), "Age"); if(errorMsg.isEmpty()){ @@ -263,9 +257,7 @@ public class PetDialogController { request.setPetSpecies(cbPetSpecies.getValue() != null ? cbPetSpecies.getValue().trim() : ""); request.setPetBreed(txtPetBreed.getText()); request.setPetStatus(cbPetStatus.getValue()); - String buildStatus = cbPetStatus.getValue(); - boolean buildNeedsPrice = !("Owned".equalsIgnoreCase(buildStatus) || "Adopted".equalsIgnoreCase(buildStatus)); - if (buildNeedsPrice && txtPetPrice.getText() != null && !txtPetPrice.getText().isBlank()) { + if (txtPetPrice.getText() != null && !txtPetPrice.getText().isBlank()) { try { request.setPetPrice(new BigDecimal(txtPetPrice.getText())); } catch (NumberFormatException e) { @@ -282,7 +274,10 @@ public class PetDialogController { request.setPetAge(age); String status = cbPetStatus.getValue(); - if (("Owned".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) && cbCustomer.getValue() != null) { + boolean customerApplicable = "Owned".equalsIgnoreCase(status) + || "Adopted".equalsIgnoreCase(status) + || "Pending".equalsIgnoreCase(status); + if (customerApplicable && cbCustomer.getValue() != null) { request.setCustomerId(cbCustomer.getValue().getId()); } if (requiresStore(status) && cbStore.getValue() != null) { @@ -407,9 +402,17 @@ public class PetDialogController { } } updateStatusFieldVisibility(cbPetStatus.getValue()); + applyStatusLock(); } } + private void applyStatusLock() { + String status = cbPetStatus.getValue(); + boolean isRestricted = "Adopted".equalsIgnoreCase(status) || "Owned".equalsIgnoreCase(status); + boolean isStaff = !UserSession.getInstance().isAdmin(); + cbPetStatus.setDisable(isRestricted && isStaff); + } + public void setMode(String mode) { this.mode = mode; lblMode.setText(mode + " Pet"); @@ -489,28 +492,27 @@ public class PetDialogController { } private void updateStatusFieldVisibility(String status) { - boolean statusNeedsCustomer = "Owned".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status); - boolean needsCustomer = statusNeedsCustomer && UserSession.getInstance().isAdmin(); - boolean storeBased = requiresStore(status); - boolean needsPrice = !statusNeedsCustomer; - setFieldVisibility(vbCustomerField, needsCustomer); - setFieldVisibility(vbStoreField, storeBased); - setFieldVisibility(vbPriceField, needsPrice); + if (status == null) { + vbPriceField.setDisable(false); + vbCustomerField.setDisable(true); + vbStoreField.setDisable(false); + return; + } + boolean isAdmin = UserSession.getInstance().isAdmin(); + boolean customerApplicable = "Owned".equalsIgnoreCase(status) + || "Adopted".equalsIgnoreCase(status) + || "Pending".equalsIgnoreCase(status); + boolean isOwned = "Owned".equalsIgnoreCase(status); + + vbPriceField.setDisable(false); + vbCustomerField.setDisable(!customerApplicable || !isAdmin); + vbStoreField.setDisable(isOwned); } private boolean requiresStore(String status) { return "Available".equalsIgnoreCase(status) - || "Pending".equalsIgnoreCase(status) - || "Unadopted".equalsIgnoreCase(status) - || "Adopted".equalsIgnoreCase(status); - } - - private void setFieldVisibility(VBox field, boolean visible) { - if (field == null) { - return; - } - field.setVisible(visible); - field.setManaged(visible); + || "Pending".equalsIgnoreCase(status) + || "Adopted".equalsIgnoreCase(status); } } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml index 7aab7ad4..0e5dddf1 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml @@ -11,7 +11,7 @@ - + @@ -25,22 +25,6 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -105,11 +105,11 @@ - + - + - + From c38bb24e94a6c301179715f559ca8d86c62aa651 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:52:53 -0600 Subject: [PATCH 11/12] fixed phone validation desktop --- .../org/example/petshopdesktop/Validator.java | 8 +-- .../StaffEditDialogController.java | 6 ++ .../StaffRegisterDialogController.java | 6 ++ .../SupplierDialogController.java | 3 + .../util/TextFieldFormatSupport.java | 55 +++++++++++++++++++ 5 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 desktop/src/main/java/org/example/petshopdesktop/util/TextFieldFormatSupport.java diff --git a/desktop/src/main/java/org/example/petshopdesktop/Validator.java b/desktop/src/main/java/org/example/petshopdesktop/Validator.java index 2940cde4..1a453556 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/Validator.java +++ b/desktop/src/main/java/org/example/petshopdesktop/Validator.java @@ -183,7 +183,7 @@ public class Validator { } /** - * Checks if the input is a valid phone number in format XXX-XXX-XXXX + * Checks if the input is a valid phone number in format (XXX) XXX-XXXX * @param value input of string * @param name name of input * @return error msg if input is not in valid phone format, otherwise empty @@ -191,14 +191,14 @@ public class Validator { public static String isValidPhoneNumber(String value, String name){ String msg = ""; if (value == null) { - msg += name + " must be in format XXX-XXX-XXXX. \n"; + msg += name + " must be in format (XXX) XXX-XXXX. \n"; return msg; } - String regex = "^\\d{3}-\\d{3}-\\d{4}$"; + String regex = "^\\(\\d{3}\\) \\d{3}-\\d{4}$"; if (!value.matches(regex)){ - msg += name + " must be in format XXX-XXX-XXXX. \n"; + msg += name + " must be in format (XXX) XXX-XXXX. \n"; } return msg; diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java index 8b88e98f..45dd6dae 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java @@ -15,6 +15,7 @@ import org.example.petshopdesktop.api.endpoints.UserApi; import org.example.petshopdesktop.api.endpoints.CustomerApi; import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TextFieldFormatSupport; public class StaffEditDialogController { @@ -47,6 +48,11 @@ public class StaffEditDialogController { private UserResponse user; + @FXML + void initialize() { + TextFieldFormatSupport.applyPhoneNumberFormat(txtPhone); + } + public void setUser(UserResponse user) { this.user = user; String fullName = user.getFullName() == null ? "" : user.getFullName(); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java index b50d1789..91208a70 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java @@ -15,6 +15,7 @@ import org.example.petshopdesktop.api.endpoints.CustomerApi; import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.Validator; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TextFieldFormatSupport; public class StaffRegisterDialogController { @@ -45,6 +46,11 @@ public class StaffRegisterDialogController { @FXML private Button btnCreate; + @FXML + void initialize() { + TextFieldFormatSupport.applyPhoneNumberFormat(txtPhone); + } + @FXML void btnCreateClicked(ActionEvent event) { lblError.setText(""); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SupplierDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SupplierDialogController.java index 8b61daca..44131a6c 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SupplierDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SupplierDialogController.java @@ -15,6 +15,7 @@ import org.example.petshopdesktop.api.dto.supplier.SupplierResponse; import org.example.petshopdesktop.api.endpoints.SupplierApi; import org.example.petshopdesktop.models.Supplier; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TextFieldFormatSupport; public class SupplierDialogController { @@ -52,6 +53,8 @@ public class SupplierDialogController { */ @FXML void initialize() { + TextFieldFormatSupport.applyPhoneNumberFormat(txtPhone); + //Set up mouse handlers for buttons btnSave.setOnMouseClicked(new EventHandler() { @Override diff --git a/desktop/src/main/java/org/example/petshopdesktop/util/TextFieldFormatSupport.java b/desktop/src/main/java/org/example/petshopdesktop/util/TextFieldFormatSupport.java new file mode 100644 index 00000000..7dba0d9f --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/util/TextFieldFormatSupport.java @@ -0,0 +1,55 @@ +package org.example.petshopdesktop.util; + +import javafx.application.Platform; +import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; + +import java.util.function.UnaryOperator; + +public class TextFieldFormatSupport { + + /** + * Applies a phone number formatter to a TextField. + * The formatter only allows digits and automatically formats the input as (XXX) XXX-XXXX. + * + * @param textField The TextField to apply the formatter to. + */ + public static void applyPhoneNumberFormat(TextField textField) { + textField.textProperty().addListener((observable, oldValue, newValue) -> { + if (newValue == null) return; + + // Remove all non-digit characters + String digits = newValue.replaceAll("\\D", ""); + + // Limit to 10 digits + if (digits.length() > 10) { + digits = digits.substring(0, 10); + } + + StringBuilder formatted = new StringBuilder(); + int len = digits.length(); + + if (len > 0) { + formatted.append("("); + if (len <= 3) { + formatted.append(digits); + } else { + formatted.append(digits, 0, 3).append(") "); + if (len <= 6) { + formatted.append(digits.substring(3)); + } else { + formatted.append(digits, 3, 6).append("-").append(digits.substring(6)); + } + } + } + + String result = formatted.toString(); + if (!result.equals(newValue)) { + Platform.runLater(() -> { + textField.setText(result); + textField.positionCaret(result.length()); + }); + } + }); + } +} From d898732a1705dd8dbe2b9443236c71be85e9606a Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:15:41 -0600 Subject: [PATCH 12/12] seperated staff and customer on desktop --- .../CustomerAccountsController.java | 172 ++++++++++++++++++ .../controllers/MainLayoutController.java | 19 +- .../controllers/SaleController.java | 2 + .../controllers/StaffAccountsController.java | 123 +------------ .../SaleDetailDialogController.java | 12 +- .../petshopdesktop/main-layout-view.fxml | 10 +- .../modelviews/customer-accounts-view.fxml | 89 +++++++++ .../petshopdesktop/modelviews/sale-view.fxml | 4 +- .../modelviews/staff-accounts-view.fxml | 59 +----- 9 files changed, 313 insertions(+), 177 deletions(-) create mode 100644 desktop/src/main/java/org/example/petshopdesktop/controllers/CustomerAccountsController.java create mode 100644 desktop/src/main/resources/org/example/petshopdesktop/modelviews/customer-accounts-view.fxml diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/CustomerAccountsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/CustomerAccountsController.java new file mode 100644 index 00000000..4de53100 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/CustomerAccountsController.java @@ -0,0 +1,172 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.example.petshopdesktop.api.dto.user.UserResponse; +import org.example.petshopdesktop.api.endpoints.CustomerApi; +import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.TableViewSupport; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class CustomerAccountsController { + + @FXML + private TableView tvCustomers; + + @FXML + private TableColumn colCustomerUsername; + + @FXML + private TableColumn colCustomerName; + + @FXML + private TableColumn colCustomerEmail; + + @FXML + private TableColumn colCustomerPhone; + + @FXML + private TableColumn colCustomerLoyaltyPoints; + + @FXML + private TableColumn colCustomerStatus; + + @FXML + private TableColumn colCustomerCreated; + + @FXML + private TextField txtSearchCustomer; + + @FXML + private Button btnEditCustomer; + + @FXML + private Button btnRefresh; + + @FXML + private Label lblError; + + @FXML + private Label lblStatus; + + private final ObservableList customerAccounts = FXCollections.observableArrayList(); + private FilteredList filteredCustomers; + + @FXML + public void initialize() { + colCustomerUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername())); + colCustomerName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName())); + colCustomerEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail())); + colCustomerPhone.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getPhone())); + colCustomerLoyaltyPoints.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getLoyaltyPoints())); + colCustomerStatus.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getActive() != null && data.getValue().getActive() ? "Active" : "Inactive")); + colCustomerCreated.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getCreatedAt())); + + filteredCustomers = new FilteredList<>(customerAccounts, a -> true); + TableViewSupport.bindSortedItems(tvCustomers, filteredCustomers); + TableViewSupport.installDoubleClickAction(tvCustomers, this::openEditDialog); + + tvCustomers.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> + btnEditCustomer.setDisable(newVal == null)); + btnEditCustomer.setDisable(true); + + txtSearchCustomer.textProperty().addListener((obs, o, n) -> applyCustomerFilter(n)); + + refresh(); + } + + @FXML + void btnRefreshClicked(ActionEvent event) { + txtSearchCustomer.clear(); + TableViewSupport.clearSort(tvCustomers); + refresh(); + TableViewSupport.flashStatus(lblStatus, "Refreshed"); + } + + @FXML + void btnEditCustomerClicked(ActionEvent event) { + lblError.setText(""); + openEditDialog(tvCustomers.getSelectionModel().getSelectedItem()); + } + + private void openEditDialog(UserResponse selected) { + if (selected == null) { + lblError.setText("Select a customer to edit."); + return; + } + + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml")); + Stage dialog = new Stage(); + dialog.initOwner(tvCustomers.getScene().getWindow()); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Edit Customer Account"); + dialog.setScene(new Scene(loader.load())); + dialog.setResizable(false); + var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController(); + controller.setUser(selected); + dialog.showAndWait(); + refresh(); + } catch (Exception e) { + ActivityLogger.getInstance().logException("CustomerAccountsController.openEditDialog", e, "Opening customer edit dialog"); + lblError.setText("Could not open customer account editor."); + } + } + + private void refresh() { + lblError.setText(""); + tvCustomers.setDisable(true); + + new Thread(() -> { + try { + Comparator byCreated = Comparator.comparing( + UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder())); + + List customers = CustomerApi.getInstance().listCustomers(null).stream() + .sorted(byCreated) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + customerAccounts.setAll(customers); + tvCustomers.setDisable(false); + }); + } catch (Exception e) { + ActivityLogger.getInstance().logException("CustomerAccountsController.refresh", e, "Loading customer accounts"); + Platform.runLater(() -> { + lblError.setText("Could not load customer accounts."); + tvCustomers.setDisable(false); + }); + } + }).start(); + } + + private void applyCustomerFilter(String text) { + String q = text == null ? "" : text.trim().toLowerCase(); + if (q.isEmpty()) { + filteredCustomers.setPredicate(a -> true); + return; + } + filteredCustomers.setPredicate(a -> + safe(a.getUsername()).contains(q) + || safe(a.getFullName()).contains(q) + || safe(a.getEmail()).contains(q) + || safe(a.getPhone()).contains(q) + ); + } + + private static String safe(String v) { + return v == null ? "" : v.toLowerCase(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java index 61e7fa4e..997899d3 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java @@ -89,6 +89,9 @@ public class MainLayoutController { @FXML private Button btnStaffAccounts; + @FXML + private Button btnCustomers; + @FXML private Button btnAnalytics; @@ -179,6 +182,12 @@ public class MainLayoutController { updateButtons(btnStaffAccounts); } + @FXML + void btnCustomersClicked(ActionEvent event) { + loadView("customer-accounts-view.fxml"); + updateButtons(btnCustomers); + } + @FXML void btnAnalyticsClicked(ActionEvent event) { loadView("analytics-view.fxml"); @@ -415,8 +424,13 @@ public class MainLayoutController { btnPurchaseOrders.setManaged(isAdmin); if (btnStaffAccounts != null) { - btnStaffAccounts.setVisible(true); - btnStaffAccounts.setManaged(true); + btnStaffAccounts.setVisible(isAdmin); + btnStaffAccounts.setManaged(isAdmin); + } + + if (btnCustomers != null) { + btnCustomers.setVisible(true); + btnCustomers.setManaged(true); } if (lblAdminSection != null) { @@ -493,6 +507,7 @@ public class MainLayoutController { btnProducts, btnPurchaseOrders, btnStaffAccounts, + btnCustomers, btnAnalytics, btnActivityLogs, btnCoupons, diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index 3a90b787..9c7c6225 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -255,6 +255,8 @@ public class SaleController { boolean isAdmin = UserSession.getInstance().isAdmin(); vbCreateSale.setVisible(!isAdmin); vbCreateSale.setManaged(!isAdmin); + btnRefund.setVisible(!isAdmin); + btnRefund.setManaged(!isAdmin); lblModeNote.setText(isAdmin ? "(View only)" : "(Staff can create sales)"); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java index 20dbf6d0..76771784 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java @@ -8,16 +8,11 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; -import javafx.scene.control.TextField; +import javafx.scene.control.*; import javafx.scene.layout.VBox; import javafx.stage.Modality; import javafx.stage.Stage; import org.example.petshopdesktop.api.dto.user.UserResponse; -import org.example.petshopdesktop.api.endpoints.CustomerApi; import org.example.petshopdesktop.api.endpoints.UserApi; import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.util.ActivityLogger; @@ -32,36 +27,6 @@ public class StaffAccountsController { @FXML private VBox staffSection; - @FXML - private TableView tvCustomers; - - @FXML - private TableColumn colCustomerUsername; - - @FXML - private TableColumn colCustomerName; - - @FXML - private TableColumn colCustomerEmail; - - @FXML - private TableColumn colCustomerPhone; - - @FXML - private TableColumn colCustomerLoyaltyPoints; - - @FXML - private TableColumn colCustomerStatus; - - @FXML - private TableColumn colCustomerCreated; - - @FXML - private TextField txtSearchCustomer; - - @FXML - private Button btnEditCustomer; - @FXML private TableView tvStaff; @@ -104,32 +69,11 @@ public class StaffAccountsController { @FXML private Label lblStatus; - private final ObservableList customerAccounts = FXCollections.observableArrayList(); - private FilteredList filteredCustomers; - private final ObservableList staffAccounts = FXCollections.observableArrayList(); private FilteredList filteredStaff; @FXML public void initialize() { - colCustomerUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername())); - colCustomerName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName())); - colCustomerEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail())); - colCustomerPhone.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getPhone())); - colCustomerLoyaltyPoints.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getLoyaltyPoints())); - colCustomerStatus.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getActive() != null && data.getValue().getActive() ? "Active" : "Inactive")); - colCustomerCreated.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getCreatedAt())); - - filteredCustomers = new FilteredList<>(customerAccounts, a -> true); - TableViewSupport.bindSortedItems(tvCustomers, filteredCustomers); - TableViewSupport.installDoubleClickAction(tvCustomers, this::openEditDialog); - - tvCustomers.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> - btnEditCustomer.setDisable(newVal == null)); - btnEditCustomer.setDisable(true); - - txtSearchCustomer.textProperty().addListener((obs, o, n) -> applyCustomerFilter(n)); - colUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername())); colName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName())); colEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail())); @@ -148,29 +92,17 @@ public class StaffAccountsController { txtSearch.textProperty().addListener((obs, o, n) -> applyStaffFilter(n)); - boolean isAdmin = UserSession.getInstance().isAdmin(); - staffSection.setVisible(isAdmin); - staffSection.setManaged(isAdmin); - refresh(); } @FXML void btnRefreshClicked(ActionEvent event) { - txtSearchCustomer.clear(); txtSearch.clear(); - TableViewSupport.clearSort(tvCustomers); TableViewSupport.clearSort(tvStaff); refresh(); TableViewSupport.flashStatus(lblStatus, "Refreshed"); } - @FXML - void btnEditCustomerClicked(ActionEvent event) { - lblError.setText(""); - openEditDialog(tvCustomers.getSelectionModel().getSelectedItem()); - } - @FXML void btnCreateAccountClicked(ActionEvent event) { lblError.setText(""); @@ -214,10 +146,9 @@ public class StaffAccountsController { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml")); Stage dialog = new Stage(); - Stage owner = (tvStaff.getScene() != null) ? (Stage) tvStaff.getScene().getWindow() : (Stage) tvCustomers.getScene().getWindow(); - dialog.initOwner(owner); + dialog.initOwner(tvStaff.getScene().getWindow()); dialog.initModality(Modality.APPLICATION_MODAL); - dialog.setTitle("Edit User Account"); + dialog.setTitle("Edit Staff Account"); dialog.setScene(new Scene(loader.load())); dialog.setResizable(false); var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController(); @@ -232,7 +163,6 @@ public class StaffAccountsController { private void refresh() { lblError.setText(""); - tvCustomers.setDisable(true); tvStaff.setDisable(true); new Thread(() -> { @@ -240,60 +170,25 @@ public class StaffAccountsController { Comparator byCreated = Comparator.comparing( UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder())); - final List customers; - final List staff; - - if (UserSession.getInstance().isAdmin()) { - List allUsers = UserApi.getInstance().listUsers(null); - customers = allUsers.stream() - .filter(u -> "CUSTOMER".equalsIgnoreCase(u.getRole())) - .sorted(byCreated) - .collect(Collectors.toList()); - staff = allUsers.stream() - .filter(u -> !"CUSTOMER".equalsIgnoreCase(u.getRole())) - .sorted(byCreated) - .collect(Collectors.toList()); - } else { - customers = CustomerApi.getInstance().listCustomers(null).stream() - .sorted(byCreated) - .collect(Collectors.toList()); - staff = List.of(); - } + List staff = UserApi.getInstance().listUsers(null).stream() + .filter(u -> !"CUSTOMER".equalsIgnoreCase(u.getRole())) + .sorted(byCreated) + .collect(Collectors.toList()); Platform.runLater(() -> { - customerAccounts.setAll(customers); staffAccounts.setAll(staff); - tvCustomers.setDisable(false); tvStaff.setDisable(false); }); } catch (Exception e) { - ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading user accounts"); + ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading staff accounts"); Platform.runLater(() -> { - String message = e.getMessage(); - lblError.setText(message == null || message.isBlank() - ? "Could not load user accounts." - : "Could not load user accounts: " + message); - tvCustomers.setDisable(false); + lblError.setText("Could not load staff accounts."); tvStaff.setDisable(false); }); } }).start(); } - private void applyCustomerFilter(String text) { - String q = text == null ? "" : text.trim().toLowerCase(); - if (q.isEmpty()) { - filteredCustomers.setPredicate(a -> true); - return; - } - filteredCustomers.setPredicate(a -> - safe(a.getUsername()).contains(q) - || safe(a.getFullName()).contains(q) - || safe(a.getEmail()).contains(q) - || safe(a.getPhone()).contains(q) - ); - } - private void applyStaffFilter(String text) { String q = text == null ? "" : text.trim().toLowerCase(); if (q.isEmpty()) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java index abeb3355..6cd3bc28 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java @@ -13,6 +13,7 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.stage.Modality; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.auth.UserSession; import java.util.function.Consumer; @@ -46,6 +47,12 @@ public class SaleDetailDialogController { colQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); colUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); colLineTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + + if (btnRefund != null) { + boolean isAdmin = UserSession.getInstance().isAdmin(); + btnRefund.setVisible(!isAdmin); + btnRefund.setManaged(!isAdmin); + } } public void displaySaleDetails(SaleDetail sale) { @@ -57,7 +64,10 @@ public class SaleDetailDialogController { lblTotal.setText(currency.format(sale.getTotalAmount())); tvItems.setItems(sale.getItems()); if (btnRefund != null) { - btnRefund.setDisable(sale.isRefund()); + boolean isAdmin = UserSession.getInstance().isAdmin(); + if (!isAdmin) { + btnRefund.setDisable(sale.isRefund()); + } } } 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 a9478744..7192ea69 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 @@ -176,6 +176,14 @@ + @@ -220,7 +228,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml index 63569aa2..2138c2ca 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml @@ -48,7 +48,7 @@ - + - + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml index 0e5dddf1..373537f3 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml @@ -19,7 +19,7 @@ - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -