added filter options to appointments in the backend and andriod

This commit is contained in:
Alex
2026-04-07 06:34:28 -06:00
parent 9bab45f04b
commit 094c2d4a48
8 changed files with 261 additions and 96 deletions

View File

@@ -17,7 +17,11 @@ public interface AppointmentApi {
@GET("api/v1/appointments") @GET("api/v1/appointments")
Call<PageResponse<AppointmentDTO>> getAllAppointments( Call<PageResponse<AppointmentDTO>> getAllAppointments(
@Query("page") int page, @Query("page") int page,
@Query("size") int size); @Query("size") int size,
@Query("q") String query,
@Query("status") String status,
@Query("storeId") Long storeId,
@Query("date") String date);
@GET("api/v1/appointments/{id}") @GET("api/v1/appointments/{id}")
Call<AppointmentDTO> getAppointmentById(@Path("id") Long id); Call<AppointmentDTO> getAppointmentById(@Path("id") Long id);

View File

@@ -16,15 +16,21 @@ import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Toast; import android.widget.Toast;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.AppointmentAdapter; import com.example.petstoremobile.adapters.AppointmentAdapter;
import com.example.petstoremobile.adapters.WhiteTextArrayAdapter;
import com.example.petstoremobile.databinding.FragmentAppointmentBinding; import com.example.petstoremobile.databinding.FragmentAppointmentBinding;
import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.viewmodels.AppointmentViewModel; import com.example.petstoremobile.viewmodels.AppointmentViewModel;
import com.example.petstoremobile.utils.EventDecorator; import com.example.petstoremobile.utils.EventDecorator;
import com.example.petstoremobile.viewmodels.StoreViewModel;
import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarDay;
import com.prolificinteractive.materialcalendarview.CalendarMode; import com.prolificinteractive.materialcalendarview.CalendarMode;
@@ -44,10 +50,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private FragmentAppointmentBinding binding; private FragmentAppointmentBinding binding;
private List<AppointmentDTO> appointmentList = new ArrayList<>(); private List<AppointmentDTO> appointmentList = new ArrayList<>();
private List<AppointmentDTO> filteredList = new ArrayList<>(); private List<StoreDTO> storeList = new ArrayList<>();
private AppointmentAdapter adapter; private AppointmentAdapter adapter;
private AppointmentViewModel appointmentViewModel; private AppointmentViewModel appointmentViewModel;
private StoreViewModel storeViewModel;
private CalendarDay selectedCalendarDay; private CalendarDay selectedCalendarDay;
private boolean isMonthMode = false; private boolean isMonthMode = false;
@@ -60,6 +67,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class);
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
} }
/** /**
@@ -72,9 +80,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
setupRecyclerView(); setupRecyclerView();
setupSearch(); setupSearch();
setupStatusFilter();
setupStoreFilter();
setupSwipeRefresh(); setupSwipeRefresh();
setupCalendar(); setupCalendar();
loadAppointmentData(); setupFilterToggle();
binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
@@ -99,6 +109,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
binding = null; binding = null;
} }
@Override
public void onResume() {
super.onResume();
loadAppointmentData();
loadStoreData();
}
/** /**
* Toggles the calendar between week and month display modes. * Toggles the calendar between week and month display modes.
*/ */
@@ -109,6 +126,28 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
.commit(); .commit();
} }
/**
* Sets up the filter toggle button to show/hide the filter layout.
*/
private void setupFilterToggle() {
binding.btnToggleFilter.setOnClickListener(v -> {
if (binding.layoutFilter.getVisibility() == View.GONE) {
binding.layoutFilter.setVisibility(View.VISIBLE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
} else {
binding.layoutFilter.setVisibility(View.GONE);
binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search);
// Reset filters when closing
binding.etSearchAppointment.setText("");
binding.spinnerStatus.setSelection(0);
binding.spinnerStore.setSelection(0);
selectedCalendarDay = null;
binding.calendarView.clearSelection();
}
});
}
/** /**
* Sets up the date selection listener for the calendar. * Sets up the date selection listener for the calendar.
*/ */
@@ -124,7 +163,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
} else { } else {
selectedCalendarDay = null; selectedCalendarDay = null;
} }
filterAppointments(binding.etSearchAppointment.getText().toString()); loadAppointmentData();
}); });
} }
@@ -157,48 +196,56 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
*/ */
private void setupSearch() { private void setupSearch() {
binding.etSearchAppointment.addTextChangedListener(new TextWatcher() { binding.etSearchAppointment.addTextChangedListener(new TextWatcher() {
@Override @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
public void beforeTextChanged(CharSequence s, int start, int count, int after) { @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
} loadAppointmentData();
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
filterAppointments(s.toString());
}
@Override
public void afterTextChanged(Editable s) {
} }
@Override public void afterTextChanged(Editable s) {}
}); });
} }
/** /**
* Filters the appointment list based on the search query and selected calendar date. * Configures the status filter spinner.
*/ */
private void filterAppointments(String query) { private void setupStatusFilter() {
filteredList.clear(); String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"};
String lowerQuery = query.toLowerCase(); WhiteTextArrayAdapter<String> adapter = new WhiteTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.spinnerStatus.setAdapter(adapter);
String selectedDateString = null; binding.spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
if (selectedCalendarDay != null) { @Override
selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d", public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay()); loadAppointmentData();
}
for (AppointmentDTO a : appointmentList) {
boolean matchesSearch = query.isEmpty() ||
(a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lowerQuery)) ||
(a.getServiceType() != null && a.getServiceType().toLowerCase().contains(lowerQuery)) ||
(a.getPetName() != null && a.getPetName().toLowerCase().contains(lowerQuery));
boolean matchesDate = (selectedDateString == null) ||
(a.getAppointmentDate() != null && a.getAppointmentDate().equals(selectedDateString));
if (matchesSearch && matchesDate) {
filteredList.add(a);
} }
} @Override public void onNothingSelected(AdapterView<?> parent) {}
adapter.notifyDataSetChanged(); });
}
/**
* Configures the store filter spinner.
*/
private void setupStoreFilter() {
binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
loadAppointmentData();
}
@Override public void onNothingSelected(AdapterView<?> parent) {}
});
}
/**
* Fetches store data to populate the store filter.
*/
private void loadStoreData() {
storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
storeList = resource.data.getContent();
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
}
});
} }
/** /**
@@ -214,7 +261,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private void openAppointmentDetails(int position) { private void openAppointmentDetails(int position) {
Bundle args = new Bundle(); Bundle args = new Bundle();
if (position != -1) { if (position != -1) {
AppointmentDTO a = filteredList.get(position); AppointmentDTO a = appointmentList.get(position);
args.putLong("appointmentId", a.getAppointmentId()); args.putLong("appointmentId", a.getAppointmentId());
} }
NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args); NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args);
@@ -229,11 +276,27 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
} }
/** /**
* Fetches all appointment data from the server. * Fetches appointment data from the server with all active filters.
*/ */
private void loadAppointmentData() { private void loadAppointmentData() {
//Load all appointments from the backend using viewModel String query = binding.etSearchAppointment.getText().toString().trim();
appointmentViewModel.getAllAppointments(0, 500).observe(getViewLifecycleOwner(), resource -> { String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
Long storeId = null;
if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) {
storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
}
String selectedDateString = null;
if (selectedCalendarDay != null) {
selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d",
selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay());
}
if (status.equals("All Statuses")) status = null;
else status = status.toUpperCase();
appointmentViewModel.getAllAppointments(0, 500, query, status, storeId, selectedDateString).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null) return;
// Check the status to see if the resource is loaded and display the data // Check the status to see if the resource is loaded and display the data
@@ -249,7 +312,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
appointmentList.clear(); appointmentList.clear();
appointmentList.addAll(resource.data.getContent()); appointmentList.addAll(resource.data.getContent());
updateCalendarDecorators(); updateCalendarDecorators();
filterAppointments(binding.etSearchAppointment != null ? binding.etSearchAppointment.getText().toString() : ""); adapter.notifyDataSetChanged();
} }
break; break;
case ERROR: case ERROR:
@@ -266,8 +329,8 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
* Initializes the RecyclerView for displaying appointments. * Initializes the RecyclerView for displaying appointments.
*/ */
private void setupRecyclerView() { private void setupRecyclerView() {
adapter = new AppointmentAdapter(filteredList, this); adapter = new AppointmentAdapter(appointmentList, this);
binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewAppointments.setAdapter(adapter); binding.recyclerViewAppointments.setAdapter(adapter);
} }
} }

View File

@@ -21,10 +21,10 @@ public class AppointmentRepository extends BaseRepository {
} }
/** /**
* Retrieves a paginated list of all appointments from the API. * Retrieves a paginated list of all appointments from the API with filtering.
*/ */
public LiveData<Resource<PageResponse<AppointmentDTO>>> getAllAppointments(int page, int size) { public LiveData<Resource<PageResponse<AppointmentDTO>>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date) {
return executeCall(appointmentApi.getAllAppointments(page, size)); return executeCall(appointmentApi.getAllAppointments(page, size, query, status, storeId, date));
} }
/** /**
@@ -54,4 +54,4 @@ public class AppointmentRepository extends BaseRepository {
public LiveData<Resource<Void>> deleteAppointment(Long id) { public LiveData<Resource<Void>> deleteAppointment(Long id) {
return executeCall(appointmentApi.deleteAppointment(id)); return executeCall(appointmentApi.deleteAppointment(id));
} }
} }

View File

@@ -22,10 +22,10 @@ public class AppointmentViewModel extends ViewModel {
} }
/** /**
* Fetches a paginated list of all appointments. * Fetches a paginated list of all appointments with optional filters.
*/ */
public LiveData<Resource<PageResponse<AppointmentDTO>>> getAllAppointments(int page, int size) { public LiveData<Resource<PageResponse<AppointmentDTO>>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date) {
return repository.getAllAppointments(page, size); return repository.getAllAppointments(page, size, query, status, storeId, date);
} }
/** /**
@@ -55,4 +55,4 @@ public class AppointmentViewModel extends ViewModel {
public LiveData<Resource<Void>> deleteAppointment(Long id) { public LiveData<Resource<Void>> deleteAppointment(Long id) {
return repository.deleteAppointment(id); return repository.deleteAppointment(id);
} }
} }

View File

@@ -35,7 +35,8 @@
android:text="Appointments" android:text="Appointments"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold"/> android:textStyle="bold"
android:layout_marginStart="8dp"/>
<ImageButton <ImageButton
android:id="@+id/btnToggleCalendarMode" android:id="@+id/btnToggleCalendarMode"
@@ -46,6 +47,89 @@
app:tint="@color/white" app:tint="@color/white"
android:contentDescription="Toggle Calendar Mode"/> android:contentDescription="Toggle Calendar Mode"/>
<ImageButton
android:id="@+id/btnToggleFilter"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_search"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="@color/white"
android:contentDescription="Toggle filter"/>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutFilter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:visibility="gone"
android:background="@color/primary_dark"
android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="@drawable/bg_search_bar"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@android:drawable/ic_menu_search"
android:alpha="0.6"/>
<EditText
android:id="@+id/etSearchAppointment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="Search by customer, pet or service..."
android:inputType="text"
android:background="@android:color/transparent"
android:textColor="@color/text_dark"
android:textColorHint="#99000000"
android:textSize="14sp"
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<Spinner
android:id="@+id/spinnerStatus"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"/>
<View
android:layout_width="8dp"
android:layout_height="0dp"/>
<Spinner
android:id="@+id/spinnerStore"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_spinner"
android:paddingStart="12dp"
android:paddingEnd="8dp"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<com.prolificinteractive.materialcalendarview.MaterialCalendarView <com.prolificinteractive.materialcalendarview.MaterialCalendarView
@@ -58,19 +142,6 @@
app:mcv_calendarMode="week" app:mcv_calendarMode="week"
app:mcv_tileHeight="40dp" /> app:mcv_tileHeight="40dp" />
<EditText
android:id="@+id/etSearchAppointment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="Search by customer, pet or service..."
android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search"
android:drawablePadding="8dp"
android:background="@android:color/white"
android:padding="12dp"
android:textColor="@color/text_dark"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshAppointment" android:id="@+id/swipeRefreshAppointment"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -36,20 +36,29 @@ public class AppointmentController {
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<Page<AppointmentResponse>> getAllAppointments( public ResponseEntity<Page<AppointmentResponse>> getAllAppointments(
@RequestParam(required = false) String q, @RequestParam(required = false) String q,
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) String status,
@RequestParam(required = false) String date,
@RequestParam(required = false) Long customerId,
@RequestParam(required = false) Long employeeId,
Pageable pageable) { Pageable pageable) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream() String role = authentication.getAuthorities().stream()
.findFirst() .findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", "")) .map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null); .orElse(null);
Long customerId = null; Long effectiveCustomerId = customerId;
if (role != null && role.equals("CUSTOMER")) { if (role != null && role.equals("CUSTOMER")) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository); User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
customerId = user.getId(); effectiveCustomerId = user.getId();
} }
return ResponseEntity.ok(appointmentService.getAllAppointments(q, pageable, customerId)); LocalDate appointmentDate = (date != null && !date.isBlank()) ? LocalDate.parse(date) : null;
return ResponseEntity.ok(appointmentService.getAllAppointments(
q, effectiveCustomerId, employeeId, storeId, status, appointmentDate, pageable));
} }
@GetMapping("/{id}") @GetMapping("/{id}")

View File

@@ -22,20 +22,25 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
List<Appointment> findByStoreAndDate(@Param("storeId") Long storeId, @Param("date") LocalDate date); List<Appointment> findByStoreAndDate(@Param("storeId") Long storeId, @Param("date") LocalDate date);
@Query("SELECT a FROM Appointment a LEFT JOIN a.pet p WHERE " + @Query("SELECT a FROM Appointment a LEFT JOIN a.pet p WHERE " +
"(:q IS NULL OR (" +
"LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%'))") "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%'))" +
Page<Appointment> searchAppointments(@Param("q") String query, Pageable pageable); ")) AND " +
"(:customerId IS NULL OR a.customer.id = :customerId) AND " +
Page<Appointment> findByCustomerId(Long customerId, Pageable pageable); "(:employeeId IS NULL OR a.employee.id = :employeeId) AND " +
"(:storeId IS NULL OR a.store.storeId = :storeId) AND " +
@Query("SELECT a FROM Appointment a LEFT JOIN a.pet p WHERE a.customer.id = :customerId AND (" + "(:status IS NULL OR LOWER(a.appointmentStatus) = LOWER(:status)) AND " +
"LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "(:date IS NULL OR a.appointmentDate = :date)")
"LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + Page<Appointment> searchAppointments(
"LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + @Param("q") String query,
"LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')))") @Param("customerId") Long customerId,
Page<Appointment> searchAppointmentsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); @Param("employeeId") Long employeeId,
@Param("storeId") Long storeId,
@Param("status") String status,
@Param("date") LocalDate date,
Pageable pageable);
@Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.id = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.id = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')")
List<Appointment> findByEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date); List<Appointment> findByEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date);

View File

@@ -45,22 +45,27 @@ public class AppointmentService {
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Page<AppointmentResponse> getAllAppointments(String query, Pageable pageable, Long customerId) { public Page<AppointmentResponse> getAllAppointments(
Page<Appointment> appointments; String query,
Long customerId,
Long employeeId,
Long storeId,
String status,
LocalDate date,
Pageable pageable) {
if (customerId != null) { String normalizedQuery = normalizeFilter(query);
if (query != null && !query.trim().isEmpty()) { String normalizedStatus = normalizeFilter(status);
appointments = appointmentRepository.searchAppointmentsByCustomer(customerId, query, pageable);
} else { Page<Appointment> appointments = appointmentRepository.searchAppointments(
appointments = appointmentRepository.findByCustomerId(customerId, pageable); normalizedQuery,
} customerId,
} else { employeeId,
if (query != null && !query.trim().isEmpty()) { storeId,
appointments = appointmentRepository.searchAppointments(query, pageable); normalizedStatus,
} else { date,
appointments = appointmentRepository.findAll(pageable); pageable
} );
}
return appointments.map(this::mapToResponse); return appointments.map(this::mapToResponse);
} }
@@ -204,6 +209,14 @@ public class AppointmentService {
return availableSlots; return availableSlots;
} }
private String normalizeFilter(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private void validateAppointmentRequest(AppointmentRequest request) { private void validateAppointmentRequest(AppointmentRequest request) {
if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) { if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) {
LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime()); LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime());