added calendar view to appointments

- NOTE: may have to change appointments abit after backend is updated
This commit is contained in:
Alex
2026-04-03 19:37:43 -06:00
parent 8401d9ef62
commit 5fa9cfd5d6
5 changed files with 149 additions and 20 deletions

View File

@@ -82,6 +82,8 @@ dependencies {
implementation("com.github.bumptech.glide:glide:4.16.0") implementation("com.github.bumptech.glide:glide:4.16.0")
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0") annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
implementation("com.github.prolificinteractive:material-calendarview:2.0.1")
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core) androidTestImplementation(libs.espresso.core)

View File

@@ -1,5 +1,6 @@
package com.example.petstoremobile.fragments.listfragments; package com.example.petstoremobile.fragments.listfragments;
import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -29,10 +30,21 @@ import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.AppointmentDetailFragment; import com.example.petstoremobile.fragments.listfragments.detailfragments.AppointmentDetailFragment;
import com.example.petstoremobile.utils.EventDecorator;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.prolificinteractive.materialcalendarview.CalendarDay;
import com.prolificinteractive.materialcalendarview.CalendarMode;
import com.prolificinteractive.materialcalendarview.MaterialCalendarView;
import com.prolificinteractive.materialcalendarview.OnDateSelectedListener;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
@@ -50,6 +62,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private SwipeRefreshLayout swipeRefreshLayout; private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch; private EditText etSearch;
private ImageButton hamburger; private ImageButton hamburger;
private ImageButton btnToggleCalendarMode;
private MaterialCalendarView calendarView;
private CalendarDay selectedCalendarDay;
private boolean isMonthMode = false;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
@@ -58,10 +75,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
api = RetrofitClient.getAppointmentApi(requireContext()); api = RetrofitClient.getAppointmentApi(requireContext());
hamburger = view.findViewById(R.id.btnHamburger); hamburger = view.findViewById(R.id.btnHamburger);
calendarView = view.findViewById(R.id.calendarView);
btnToggleCalendarMode = view.findViewById(R.id.btnToggleCalendarMode);
setupRecyclerView(view); setupRecyclerView(view);
setupSearch(view); setupSearch(view);
setupSwipeRefresh(view); setupSwipeRefresh(view);
setupCalendar();
loadAppointmentData(); loadAppointmentData();
loadPets(); loadPets();
loadServices(); loadServices();
@@ -76,9 +96,60 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
listFragment.openDrawer(); listFragment.openDrawer();
}); });
btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode());
return view; return view;
} }
// Toggle Calendar Mode from week to month and other way around
private void toggleCalendarMode() {
isMonthMode = !isMonthMode;
calendarView.state().edit()
.setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS)
.commit();
}
private void setupCalendar() {
calendarView.setOnDateChangedListener(new OnDateSelectedListener() {
@Override
public void onDateSelected(@NonNull MaterialCalendarView widget, @NonNull CalendarDay date, boolean selected) {
if (selected) {
if (date.equals(selectedCalendarDay)) {
selectedCalendarDay = null;
calendarView.clearSelection();
} else {
selectedCalendarDay = date;
}
} else {
selectedCalendarDay = null;
}
filterAppointments(etSearch.getText().toString());
}
});
}
//Set indicators for dates with appointments on the calendar
private void updateCalendarDecorators() {
HashSet<CalendarDay> datesWithAppointments = new HashSet<>();
for (AppointmentDTO appointment : appointmentList) {
try {
//Get the appointment date
Date date = dateFormat.parse(appointment.getAppointmentDate());
//if the date is not null, add it to the hashset
if (date != null) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
datesWithAppointments.add(CalendarDay.from(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)));
}
} catch (ParseException e) {
Log.e("AppointmentFragment", "Error parsing date: " + appointment.getAppointmentDate());
}
}
//update the indicators to the calendar
calendarView.removeDecorators();
calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments));
}
private void setupSearch(View view) { private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchAppointment); etSearch = view.findViewById(R.id.etSearchAppointment);
etSearch.addTextChangedListener(new TextWatcher() { etSearch.addTextChangedListener(new TextWatcher() {
@@ -99,16 +170,25 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private void filterAppointments(String query) { private void filterAppointments(String query) {
filteredList.clear(); filteredList.clear();
if (query.isEmpty()) { String lowerQuery = query.toLowerCase();
filteredList.addAll(appointmentList);
} else { String selectedDateString = null;
String lower = query.toLowerCase(); if (selectedCalendarDay != null) {
for (AppointmentDTO a : appointmentList) { selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d",
if ((a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lower)) selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay());
|| (a.getServiceType() != null && a.getServiceType().toLowerCase().contains(lower)) }
|| (a.getPetName() != null && a.getPetName().toLowerCase().contains(lower))) {
filteredList.add(a); 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);
} }
} }
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
@@ -141,17 +221,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
if (lf != null) lf.loadFragment(detailFragment); if (lf != null) lf.loadFragment(detailFragment);
} }
public void onAppointmentSaved(int position, AppointmentDTO appointment) { public void onAppointmentSaved(int position, AppointmentDTO appointment) {
if (position == -1) { loadAppointmentData();
appointmentList.add(appointment);
} else {
appointmentList.set(position, appointment);
}
filterAppointments(etSearch.getText().toString());
} }
public void onAppointmentDeleted(int position) { public void onAppointmentDeleted(int position) {
appointmentList.remove(position); loadAppointmentData();
filterAppointments(etSearch.getText().toString());
} }
@Override @Override
@@ -162,7 +236,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
private void loadAppointmentData() { private void loadAppointmentData() {
if (swipeRefreshLayout != null) if (swipeRefreshLayout != null)
swipeRefreshLayout.setRefreshing(true); swipeRefreshLayout.setRefreshing(true);
api.getAllAppointments(0, 100).enqueue(new Callback<PageResponse<AppointmentDTO>>() { api.getAllAppointments(0, 500).enqueue(new Callback<PageResponse<AppointmentDTO>>() {
@Override @Override
public void onResponse(Call<PageResponse<AppointmentDTO>> call, public void onResponse(Call<PageResponse<AppointmentDTO>> call,
Response<PageResponse<AppointmentDTO>> response) { Response<PageResponse<AppointmentDTO>> response) {
@@ -171,6 +245,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
appointmentList.clear(); appointmentList.clear();
appointmentList.addAll(response.body().getContent()); appointmentList.addAll(response.body().getContent());
updateCalendarDecorators();
filterAppointments(etSearch != null ? etSearch.getText().toString() : ""); filterAppointments(etSearch != null ? etSearch.getText().toString() : "");
} else { } else {
Log.e("AppointmentFragment", "Error: " + response.message()); Log.e("AppointmentFragment", "Error: " + response.message());

View File

@@ -0,0 +1,30 @@
package com.example.petstoremobile.utils;
import com.prolificinteractive.materialcalendarview.CalendarDay;
import com.prolificinteractive.materialcalendarview.DayViewDecorator;
import com.prolificinteractive.materialcalendarview.DayViewFacade;
import com.prolificinteractive.materialcalendarview.spans.DotSpan;
import java.util.Collection;
import java.util.HashSet;
public class EventDecorator implements DayViewDecorator {
private final int color;
private final HashSet<CalendarDay> dates;
public EventDecorator(int color, Collection<CalendarDay> dates) {
this.color = color;
this.dates = new HashSet<>(dates);
}
@Override
public boolean shouldDecorate(CalendarDay day) {
return dates.contains(day);
}
@Override
public void decorate(DayViewFacade view) {
view.addSpan(new DotSpan(8, color));
}
}

View File

@@ -29,15 +29,35 @@
android:contentDescription="Open menu"/> android:contentDescription="Open menu"/>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" android:layout_height="wrap_content"
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"/>
<ImageButton
android:id="@+id/btnToggleCalendarMode"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_today"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="@color/white"
android:contentDescription="Toggle Calendar Mode"/>
</LinearLayout> </LinearLayout>
<com.prolificinteractive.materialcalendarview.MaterialCalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
app:mcv_showOtherDates="all"
app:mcv_selectionColor="@color/accent_blue"
app:mcv_calendarMode="week"
app:mcv_tileHeight="40dp" />
<EditText <EditText
android:id="@+id/etSearchAppointment" android:id="@+id/etSearchAppointment"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -15,6 +15,8 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# Android operating system, and which are packaged with your app's APK # Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Enables namespacing of each library's R class so that its R class includes only the # Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library