added filter options to appointments in the backend and andriod
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
Reference in New Issue
Block a user