diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java index c6bd678c..6dd8eeb4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java @@ -57,7 +57,7 @@ public class AdoptionAdapter extends RecyclerView.Adapter> getUsers(@Query("role") String role, @Query("page") int page, @Query("size") int size); +} diff --git a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java index 0311840d..b5f1ca64 100644 --- a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java +++ b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java @@ -173,4 +173,10 @@ public class NetworkModule { public static CategoryApi provideCategoryApi(Retrofit retrofit) { return retrofit.create(CategoryApi.class); } -} \ No newline at end of file + + @Provides + @Singleton + public static UserApi provideUserApi(Retrofit retrofit) { + return retrofit.create(UserApi.class); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java index daf7d768..d8bf2b6b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java @@ -10,20 +10,27 @@ public class AdoptionDTO { private String customerName; private Long employeeId; private String employeeName; + private Long sourceStoreId; + private String sourceStoreName; private String adoptionDate; private String adoptionStatus; private BigDecimal adoptionFee; private String createdAt; private String updatedAt; - public AdoptionDTO(Long petId, Long customerId, String adoptionDate, String adoptionStatus) { - this(petId, customerId, null, adoptionDate, adoptionStatus); + public AdoptionDTO(Long petId, Long customerId, Long sourceStoreId, String adoptionDate, String adoptionStatus) { + this.petId = petId; + this.customerId = customerId; + this.sourceStoreId = sourceStoreId; + this.adoptionDate = adoptionDate; + this.adoptionStatus = adoptionStatus; } - public AdoptionDTO(Long petId, Long customerId, Long employeeId, String adoptionDate, String adoptionStatus) { + public AdoptionDTO(Long petId, Long customerId, Long employeeId, Long sourceStoreId, String adoptionDate, String adoptionStatus) { this.petId = petId; this.customerId = customerId; this.employeeId = employeeId; + this.sourceStoreId = sourceStoreId; this.adoptionDate = adoptionDate; this.adoptionStatus = adoptionStatus; } @@ -56,6 +63,14 @@ public class AdoptionDTO { return employeeName; } + public Long getSourceStoreId() { + return sourceStoreId; + } + + public String getSourceStoreName() { + return sourceStoreName; + } + public String getAdoptionDate() { return adoptionDate; } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index d38622f5..8789433d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -18,6 +18,7 @@ import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.AdoptionViewModel; import com.example.petstoremobile.viewmodels.CustomerViewModel; import com.example.petstoremobile.viewmodels.PetViewModel; +import com.example.petstoremobile.viewmodels.StoreViewModel; import java.util.*; @@ -35,15 +36,18 @@ public class AdoptionDetailFragment extends Fragment { private boolean isEditing = false; private long preselectedPetId = -1; private long preselectedCustomerId = -1; + private long preselectedStoreId = -1; private List petList = new ArrayList<>(); private List customerList = new ArrayList<>(); + private List storeList = new ArrayList<>(); - private final String[] STATUSES = {"Pending", "Approved", "Rejected"}; + private final String[] STATUSES = {"Pending", "Completed", "Cancelled"}; private AdoptionViewModel adoptionViewModel; private PetViewModel petViewModel; private CustomerViewModel customerViewModel; + private StoreViewModel storeViewModel; @Override public void onCreate(Bundle savedInstanceState) { @@ -51,6 +55,7 @@ public class AdoptionDetailFragment extends Fragment { adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); petViewModel = new ViewModelProvider(this).get(PetViewModel.class); customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); + storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); } @Override @@ -107,6 +112,7 @@ public class AdoptionDetailFragment extends Fragment { private void loadSpinnersData() { loadPets(); loadCustomers(); + loadStores(); } /** @@ -152,6 +158,27 @@ public class AdoptionDetailFragment extends Fragment { preselectedCustomerId, CustomerDTO::getCustomerId); } + /** + * Loads the list of stores from the API. + */ + private void loadStores() { + storeViewModel.getAllStores(0, 200).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList = resource.data.getContent(); + refreshStoreSpinner(); + } + }); + } + + /** + * Populates the store selection spinner with data. + */ + private void refreshStoreSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, storeList, + StoreDTO::getStoreName, "-- Select Store --", + preselectedStoreId, StoreDTO::getStoreId); + } + /** * Handles arguments to determine if the fragment is in edit or add mode. */ @@ -182,11 +209,13 @@ public class AdoptionDetailFragment extends Fragment { AdoptionDTO a = resource.data; preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; + preselectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1; binding.etAdoptionDate.setText(a.getAdoptionDate()); SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getAdoptionStatus()); refreshPetSpinner(); refreshCustomerSpinner(); + refreshStoreSpinner(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -203,6 +232,9 @@ public class AdoptionDetailFragment extends Fragment { if (binding.spinnerAdoptionPet.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return; } + if (binding.spinnerAdoptionStore.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return; + } String date = binding.etAdoptionDate.getText().toString().trim(); if (date.isEmpty()) { Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; @@ -210,11 +242,13 @@ public class AdoptionDetailFragment extends Fragment { CustomerDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); PetDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); + StoreDTO store = storeList.get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; AdoptionDTO dto = new AdoptionDTO( pet.getPetId(), customer.getCustomerId(), + store.getStoreId(), date, status ); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index cf9bb837..7757f156 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -21,6 +21,7 @@ import com.example.petstoremobile.viewmodels.CustomerViewModel; import com.example.petstoremobile.viewmodels.PetViewModel; import com.example.petstoremobile.viewmodels.ServiceViewModel; import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.UserViewModel; import java.util.*; @@ -40,11 +41,13 @@ public class AppointmentDetailFragment extends Fragment { private long preselectedServiceId = -1; private long preselectedCustomerId = -1; private long preselectedStoreId = -1; + private long preselectedStaffId = -1; private List petList = new ArrayList<>(); private List serviceList = new ArrayList<>(); private List customerList = new ArrayList<>(); private List storeList = new ArrayList<>(); + private List staffList = new ArrayList<>(); private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; private final Integer[] MINUTES = {0,15,30,45}; @@ -54,6 +57,7 @@ public class AppointmentDetailFragment extends Fragment { private ServiceViewModel serviceViewModel; private StoreViewModel storeViewModel; private CustomerViewModel customerViewModel; + private UserViewModel userViewModel; @Override public void onCreate(Bundle savedInstanceState) { @@ -63,6 +67,7 @@ public class AppointmentDetailFragment extends Fragment { serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class); storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); + userViewModel = new ViewModelProvider(this).get(UserViewModel.class); } @Override @@ -128,6 +133,7 @@ public class AppointmentDetailFragment extends Fragment { loadServices(); loadCustomers(); loadStores(); + loadStaff(); } /** @@ -215,6 +221,27 @@ public class AppointmentDetailFragment extends Fragment { preselectedStoreId, StoreDTO::getStoreId); } + /** + * Loads the list of staff from the API. + */ + private void loadStaff() { + userViewModel.getUsers("STAFF", 0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + staffList = resource.data.getContent(); + refreshStaffSpinner(); + } + }); + } + + /** + * Populates the staff selection spinner. + */ + private void refreshStaffSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, staffList, + UserDTO::getFullName, "-- Select Staff --", + preselectedStaffId, UserDTO::getId); + } + /** * Handles arguments to determine if the fragment is in edit or add mode. */ @@ -247,6 +274,7 @@ public class AppointmentDetailFragment extends Fragment { preselectedServiceId = (a.getServiceId() != null) ? a.getServiceId() : -1; preselectedCustomerId = (a.getCustomerId() != null) ? a.getCustomerId() : -1; preselectedStoreId = (a.getStoreId() != null) ? a.getStoreId() : -1; + preselectedStaffId = (a.getEmployeeId() != null) ? a.getEmployeeId() : -1; binding.etAppointmentDate.setText(a.getAppointmentDate()); @@ -276,6 +304,7 @@ public class AppointmentDetailFragment extends Fragment { refreshServiceSpinner(); refreshCustomerSpinner(); refreshStoreSpinner(); + refreshStaffSpinner(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load appointment: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -307,6 +336,11 @@ public class AppointmentDetailFragment extends Fragment { StoreDTO store = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1); PetDTO pet = petList.get(binding.spinnerPet.getSelectedItemPosition() - 1); ServiceDTO service = serviceList.get(binding.spinnerService.getSelectedItemPosition() - 1); + + Long employeeId = null; + if (binding.spinnerStaff.getSelectedItemPosition() > 0) { + employeeId = staffList.get(binding.spinnerStaff.getSelectedItemPosition() - 1).getId(); + } String time = String.format("%02d:%02d", HOURS[binding.spinnerHour.getSelectedItemPosition()], @@ -346,6 +380,7 @@ public class AppointmentDetailFragment extends Fragment { customer.getCustomerId(), store.getStoreId(), service.getServiceId(), + employeeId, date, time, status, diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/UserRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/UserRepository.java new file mode 100644 index 00000000..0ed9ced9 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/UserRepository.java @@ -0,0 +1,26 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; + +import com.example.petstoremobile.api.UserApi; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.UserDTO; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class UserRepository extends BaseRepository { + private final UserApi userApi; + + @Inject + public UserRepository(UserApi userApi) { + super("UserRepository"); + this.userApi = userApi; + } + + public LiveData>> getUsers(String role, int page, int size) { + return executeCall(userApi.getUsers(role, page, size)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java new file mode 100644 index 00000000..d839f6c4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java @@ -0,0 +1,27 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.UserDTO; +import com.example.petstoremobile.repositories.UserRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class UserViewModel extends ViewModel { + private final UserRepository userRepository; + + @Inject + public UserViewModel(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public LiveData>> getUsers(String role, int page, int size) { + return userRepository.getUsers(role, page, size); + } +} diff --git a/android/app/src/main/res/layout/fragment_adoption_detail.xml b/android/app/src/main/res/layout/fragment_adoption_detail.xml index 2f6b153f..0ad14fcf 100644 --- a/android/app/src/main/res/layout/fragment_adoption_detail.xml +++ b/android/app/src/main/res/layout/fragment_adoption_detail.xml @@ -95,6 +95,20 @@ android:layout_height="wrap_content" android:layout_marginBottom="16dp"/> + + + + - \ No newline at end of file + diff --git a/android/app/src/main/res/layout/fragment_appointment_detail.xml b/android/app/src/main/res/layout/fragment_appointment_detail.xml index ef1d8b33..1f31bc54 100644 --- a/android/app/src/main/res/layout/fragment_appointment_detail.xml +++ b/android/app/src/main/res/layout/fragment_appointment_detail.xml @@ -124,15 +124,27 @@ android:textSize="12sp" android:layout_marginBottom="4dp"/> - - - + + + + + > getAllAdoptions( @RequestParam(required = false) String q, + @RequestParam(required = false) Long customerId, + @RequestParam(required = false) String status, Pageable pageable) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String role = authentication.getAuthorities().stream() @@ -40,13 +42,13 @@ public class AdoptionController { .map(authority -> authority.getAuthority().replace("ROLE_", "")) .orElse(null); - Long customerId = null; + Long effectiveCustomerId = customerId; if (role != null && role.equals("CUSTOMER")) { User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); + effectiveCustomerId = user.getId(); } - return ResponseEntity.ok(adoptionService.getAllAdoptions(q, pageable, customerId)); + return ResponseEntity.ok(adoptionService.getAllAdoptions(q, effectiveCustomerId, status, pageable)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java index 23ccc543..42f1d578 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java @@ -14,18 +14,18 @@ import java.util.Optional; public interface AdoptionRepository extends JpaRepository { @Query("SELECT a FROM Adoption a WHERE " + + "(:q IS NULL OR (" + "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%'))") - Page searchAdoptions(@Param("q") String query, Pageable pageable); - - Page findByCustomerId(Long customerId, Pageable pageable); - - @Query("SELECT a FROM Adoption a WHERE a.customer.id = :customerId AND (" + - "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%')))") - Page searchAdoptionsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); + "LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%'))" + + ")) AND " + + "(:customerId IS NULL OR a.customer.id = :customerId) AND " + + "(:status IS NULL OR LOWER(a.adoptionStatus) = LOWER(:status))") + Page searchAdoptions( + @Param("q") String query, + @Param("customerId") Long customerId, + @Param("status") String status, + Pageable pageable); Optional findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(Long petId, String adoptionStatus); 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 c7ef4eb2..5f76947d 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -38,22 +38,16 @@ public class AdoptionService { this.storeRepository = storeRepository; } - public Page getAllAdoptions(String query, Pageable pageable, Long customerId) { - Page adoptions; + public Page getAllAdoptions(String query, Long customerId, String status, Pageable pageable) { + String normalizedQuery = normalizeFilter(query); + String normalizedStatus = normalizeFilter(status); - if (customerId != null) { - if (query != null && !query.trim().isEmpty()) { - adoptions = adoptionRepository.searchAdoptionsByCustomer(customerId, query, pageable); - } else { - adoptions = adoptionRepository.findByCustomerId(customerId, pageable); - } - } else { - if (query != null && !query.trim().isEmpty()) { - adoptions = adoptionRepository.searchAdoptions(query, pageable); - } else { - adoptions = adoptionRepository.findAll(pageable); - } - } + Page adoptions = adoptionRepository.searchAdoptions( + normalizedQuery, + customerId, + normalizedStatus, + pageable + ); return adoptions.map(this::mapToResponse); } @@ -140,6 +134,14 @@ public class AdoptionService { adoptionRepository.deleteAllById(request.getIds()); } + private String normalizeFilter(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + private AdoptionResponse mapToResponse(Adoption adoption) { StoreLocation sourceStore = adoption.getSourceStore(); return new AdoptionResponse(