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 96001734..d43304c0 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 @@ -1,10 +1,12 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; -import android.app.DatePickerDialog; import android.os.Bundle; -import android.util.Log; -import android.view.*; -import android.widget.*; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; @@ -12,15 +14,15 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.databinding.FragmentAppointmentDetailBinding; -import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.DropdownDTO; +import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.AppointmentViewModel; - -import java.util.*; +import com.example.petstoremobile.viewmodels.AppointmentDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -38,24 +40,33 @@ public class AppointmentDetailFragment extends Fragment { private long preselectedStoreId = -1; private long preselectedStaffId = -1; - private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; - private final Integer[] MINUTES = {0,15,30,45}; + private final Integer[] HOURS = {9, 10, 11, 12, 13, 14, 15, 16, 17}; + private final Integer[] MINUTES = {0, 15, 30, 45}; - private AppointmentViewModel appointmentViewModel; + private AppointmentDetailViewModel appointmentViewModel; private boolean isUpdatingUI = false; + /** + * Called when the fragment is first created. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); + appointmentViewModel = new ViewModelProvider(this).get(AppointmentDetailViewModel.class); } + /** + * Creates and returns the view hierarchy with the fragment. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentAppointmentDetailBinding.inflate(inflater, container, false); return binding.getRoot(); } + /** + * Called immediately after onCreateView has returned. + */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -70,6 +81,9 @@ public class AppointmentDetailFragment extends Fragment { binding.btnDeleteAppointment.setOnClickListener(v -> confirmDelete()); } + /** + * Called when the view previously created by onCreateView has been detached. + */ @Override public void onDestroyView() { super.onDestroyView(); @@ -77,7 +91,7 @@ public class AppointmentDetailFragment extends Fragment { } /** - * Configures the adapters for spinners. + * Configures the adapters and listeners for all spinners. */ private void setupSpinners() { //Status Spinner is empty by default the date determines whats in here @@ -86,9 +100,9 @@ public class AppointmentDetailFragment extends Fragment { // Set up hour and minute spinners String[] hours = new String[HOURS.length]; for (int i = 0; i < HOURS.length; i++) - hours[i] = String.format("%02d:00", HOURS[i]); + hours[i] = DateTimeUtils.formatTime(HOURS[i], 0); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours); - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00","15","30","45"}); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00", "15", "30", "45"}); // Pet and Staff spinners disabled by until parent selection UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff); @@ -114,42 +128,24 @@ public class AppointmentDetailFragment extends Fragment { }); // Listeners for other selections - binding.spinnerService.setOnItemSelectedListener(new OnIndexSelected(p -> appointmentViewModel.onServiceSelected(p))); - binding.spinnerPet.setOnItemSelectedListener(new OnIndexSelected(p -> appointmentViewModel.onPetSelected(p))); - binding.spinnerStaff.setOnItemSelectedListener(new OnIndexSelected(p -> appointmentViewModel.onStaffSelected(p))); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerService, p -> appointmentViewModel.onServiceSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPet, p -> appointmentViewModel.onPetSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerStaff, p -> appointmentViewModel.onStaffSelected(p)); // Listeners for time changes - binding.spinnerHour.setOnItemSelectedListener(new OnIndexSelected(p -> notifyDateTimeStatusChange())); - binding.spinnerMinute.setOnItemSelectedListener(new OnIndexSelected(p -> notifyDateTimeStatusChange())); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerHour, p -> notifyDateTimeStatusChange()); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerMinute, p -> notifyDateTimeStatusChange()); // Listener to notify ViewModel of status selection - binding.spinnerAppointmentStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - notifyDateTimeStatusChange(); - } - @Override - public void onNothingSelected(AdapterView parent) {} - }); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAppointmentStatus, p -> notifyDateTimeStatusChange()); } /** * Configures the date picker dialog for the appointment date field. */ private void setupDatePicker() { - binding.etAppointmentDate.setOnClickListener(v -> { - Calendar c = Calendar.getInstance(); - DatePickerDialog d = new DatePickerDialog(requireContext(), - (dp,y,m,d1) -> { - String selectedDate = String.format("%04d-%02d-%02d", y, m+1, d1); - binding.etAppointmentDate.setText(selectedDate); - notifyDateTimeStatusChange(); - }, - c.get(Calendar.YEAR), c.get(Calendar.MONTH), - c.get(Calendar.DAY_OF_MONTH)); - d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000); - d.show(); - }); + binding.etAppointmentDate.setOnClickListener(v -> + UIUtils.showDatePicker(requireContext(), binding.etAppointmentDate, this::notifyDateTimeStatusChange)); } /** @@ -178,37 +174,27 @@ public class AppointmentDetailFragment extends Fragment { /** * Applies the ViewState provided by the ViewModel to the UI components. */ - private void applyViewState(AppointmentViewModel.ViewState state) { + private void applyViewState(AppointmentDetailViewModel.ViewState state) { isUpdatingUI = true; // Mode specific UI binding.tvApptMode.setText(state.isEditing ? "Edit Appointment" : "Add Appointment"); - binding.tvAppointmentId.setText("ID: " + appointmentViewModel.getAppointmentId()); + binding.tvAppointmentId.setText(DateTimeUtils.formatId(appointmentViewModel.getAppointmentId())); binding.tvAppointmentId.setVisibility(state.isEditing ? View.VISIBLE : View.GONE); binding.btnDeleteAppointment.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); binding.btnSaveAppointment.setVisibility(state.isSaveVisible ? View.VISIBLE : View.GONE); - // Enabling/Disabling Views - UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer); - UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore); - UIUtils.setViewsEnabled(state.isPetEnabled, binding.spinnerPet); - UIUtils.setViewsEnabled(state.isServiceEnabled, binding.spinnerService); - UIUtils.setViewsEnabled(state.isStaffEnabled, binding.spinnerStaff); - UIUtils.setViewsEnabled(state.isDateEnabled, binding.etAppointmentDate); - UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerHour, binding.spinnerMinute); + // Enabling/Disabling Views and Labels + UIUtils.setFieldEnabled(state.isCustomerEnabled, binding.spinnerCustomer, binding.tvLabelCustomer); + UIUtils.setFieldEnabled(state.isStoreEnabled, binding.spinnerStore, binding.tvLabelStore); + UIUtils.setFieldEnabled(state.isPetEnabled, binding.spinnerPet, binding.tvLabelPet); + UIUtils.setFieldEnabled(state.isServiceEnabled, binding.spinnerService, binding.tvLabelService); + UIUtils.setFieldEnabled(state.isStaffEnabled, binding.spinnerStaff, binding.tvLabelStaff); + UIUtils.setFieldEnabled(state.isDateEnabled, binding.etAppointmentDate, binding.tvLabelDate); + UIUtils.setFieldEnabled(state.isTimeEnabled, binding.spinnerHour, binding.tvLabelTime); + UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerMinute); UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAppointmentStatus); - // Alpha for disabled look - float alpha = 1.0f; - float disabledAlpha = 0.5f; - UIUtils.setViewsAlpha(state.isCustomerEnabled ? alpha : disabledAlpha, binding.tvLabelCustomer); - UIUtils.setViewsAlpha(state.isStoreEnabled ? alpha : disabledAlpha, binding.tvLabelStore); - UIUtils.setViewsAlpha(state.isPetEnabled ? alpha : disabledAlpha, binding.tvLabelPet); - UIUtils.setViewsAlpha(state.isServiceEnabled ? alpha : disabledAlpha, binding.tvLabelService); - UIUtils.setViewsAlpha(state.isStaffEnabled ? alpha : disabledAlpha, binding.tvLabelStaff); - UIUtils.setViewsAlpha(state.isDateEnabled ? alpha : disabledAlpha, binding.tvLabelDate); - UIUtils.setViewsAlpha(state.isTimeEnabled ? alpha : disabledAlpha, binding.tvLabelTime); - // Update status options Object selected = binding.spinnerAppointmentStatus.getSelectedItem(); String current = selected != null ? selected.toString() : ""; @@ -218,6 +204,9 @@ public class AppointmentDetailFragment extends Fragment { isUpdatingUI = false; } + /** + * Notifies the ViewModel that the date, time, or status has changed. + */ private void notifyDateTimeStatusChange() { if (isUpdatingUI) return; @@ -282,7 +271,7 @@ public class AppointmentDetailFragment extends Fragment { appointmentViewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - AppointmentViewModel.ViewState state = appointmentViewModel.getViewState().getValue(); + AppointmentDetailViewModel.ViewState state = appointmentViewModel.getViewState().getValue(); String message = (state != null && state.isEditing) ? "Updated" : "Saved"; Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); navigateBack(); @@ -296,24 +285,19 @@ public class AppointmentDetailFragment extends Fragment { * Validates that all required fields are selected. */ private boolean validateRequiredFields() { - if (binding.spinnerCustomer.getSelectedItemPosition() == 0) return showToast("Select a customer"); - if (binding.spinnerStore.getSelectedItemPosition() == 0) return showToast("Select a store"); - if (binding.spinnerPet.getSelectedItemPosition() == 0) return showToast("Select a pet"); - if (binding.spinnerService.getSelectedItemPosition() == 0) return showToast("Select a service"); - if (binding.etAppointmentDate.getText().toString().trim().isEmpty()) return showToast("Select a date"); + if (binding.spinnerCustomer.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a customer"); + if (binding.spinnerStore.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a store"); + if (binding.spinnerPet.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a pet"); + if (binding.spinnerService.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a service"); + if (binding.etAppointmentDate.getText().toString().trim().isEmpty()) return UIUtils.showToast(getContext(), "Select a date"); return true; } - private boolean showToast(String msg) { - Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); - return false; - } - /** * Builds a time string from the hour and minute spinners. */ private String buildTimeString() { - return String.format("%02d:%02d", HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); + return DateTimeUtils.formatTime(HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); } /** @@ -358,17 +342,7 @@ public class AppointmentDetailFragment extends Fragment { private void parseAndSetTimeSpinners(String time) { int[] parsedTime = DateTimeUtils.parseTimeString(time); if (parsedTime == null) return; - for (int i = 0; i < HOURS.length; i++) if (HOURS[i] == parsedTime[0]) binding.spinnerHour.setSelection(i); - for (int i = 0; i < MINUTES.length; i++) if (MINUTES[i] == parsedTime[1]) binding.spinnerMinute.setSelection(i); - } - - /** - * Helper listener for simple index reporting. - */ - private static class OnIndexSelected implements AdapterView.OnItemSelectedListener { - private final java.util.function.Consumer callback; - public OnIndexSelected(java.util.function.Consumer callback) { this.callback = callback; } - @Override public void onItemSelected(AdapterView p, View v, int pos, long id) { callback.accept(pos); } - @Override public void onNothingSelected(AdapterView p) {} + SpinnerUtils.setSelectionByValueArray(binding.spinnerHour, HOURS, parsedTime[0]); + SpinnerUtils.setSelectionByValueArray(binding.spinnerMinute, MINUTES, parsedTime[1]); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java index 4e79a190..11e8f4fd 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java @@ -3,6 +3,7 @@ package com.example.petstoremobile.utils; import android.util.Log; import java.util.Calendar; +import java.util.Locale; /** * Utility class for date and time operations. @@ -78,7 +79,7 @@ public class DateTimeUtils { } /** - * Parses a time string and returns hour and minute indices for the spinners. + * Parses a time string and returns hour and minute values. */ public static int[] parseTimeString(String time) { if (time == null || time.isEmpty()) return null; @@ -95,4 +96,18 @@ public class DateTimeUtils { return null; } } + + /** + * Formats an hour and minute into an HH:mm string. + */ + public static String formatTime(int hour, int minute) { + return String.format(Locale.getDefault(), "%02d:%02d", hour, minute); + } + + /** + * Formats an ID for display (e.g., "ID: 123"). + */ + public static String formatId(long id) { + return String.format(Locale.getDefault(), "ID: %d", id); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java index b0aae8b8..9c5385c7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java @@ -12,6 +12,7 @@ import com.example.petstoremobile.adapters.WhiteTextArrayAdapter; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Function; /** @@ -96,6 +97,13 @@ public class SpinnerUtils { }); } + /** + * Sets a listener that provides the selected index to a consumer. + */ + public static void setOnIndexSelectedListener(Spinner spinner, Consumer callback) { + spinner.setOnItemSelectedListener(new OnIndexSelected(callback)); + } + /** * Sets the selection of a spinner based on a string value. */ @@ -108,6 +116,19 @@ public class SpinnerUtils { } } + /** + * Sets the selection of a spinner based on a value within an array. + */ + public static void setSelectionByValueArray(Spinner spinner, T[] array, T value) { + if (spinner == null || array == null || value == null) return; + for (int i = 0; i < array.length; i++) { + if (Objects.equals(array[i], value)) { + spinner.setSelection(i); + return; + } + } + } + /** * Configures a simple string array spinner. */ @@ -117,4 +138,23 @@ public class SpinnerUtils { adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); } + + /** + * Helper listener to get selected index from a spinner. + */ + public static class OnIndexSelected implements AdapterView.OnItemSelectedListener { + private final Consumer callback; + + public OnIndexSelected(Consumer callback) { + this.callback = callback; + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + callback.accept(position); + } + + @Override + public void onNothingSelected(AdapterView parent) {} + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java index 40f6c92d..8a70621b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java @@ -1,5 +1,7 @@ package com.example.petstoremobile.utils; +import android.app.DatePickerDialog; +import android.content.Context; import android.telephony.PhoneNumberFormattingTextWatcher; import android.text.Editable; import android.text.InputFilter; @@ -8,10 +10,14 @@ import android.view.View; import android.widget.EditText; import android.widget.ImageButton; import android.widget.Spinner; +import android.widget.Toast; import androidx.fragment.app.Fragment; import com.example.petstoremobile.fragments.ListFragment; +import java.util.Calendar; +import java.util.Locale; + /** * Utility class for shared UI component logic and formatting. */ @@ -32,7 +38,6 @@ public class UIUtils { boolean isVisible = layoutFilter.getVisibility() == View.VISIBLE; layoutFilter.setVisibility(isVisible ? View.GONE : View.VISIBLE); - // Use Android default icons or app-specific ones if available btnToggle.setImageResource(isVisible ? android.R.drawable.ic_menu_search : android.R.drawable.ic_menu_close_clear_cancel); @@ -69,7 +74,6 @@ public class UIUtils { /** * Sets the enabled state and alpha for multiple views. - * Alpha is set to 1.0f when enabled and 0.5f when disabled. */ public static void setViewsEnabled(boolean enabled, View... views) { for (View v : views) { @@ -80,6 +84,19 @@ public class UIUtils { } } + /** + * Sets enabled state for a field and updates alpha for both the field and its label. + */ + public static void setFieldEnabled(boolean enabled, View field, View label) { + if (field != null) { + field.setEnabled(enabled); + field.setAlpha(enabled ? 1.0f : 0.5f); + } + if (label != null) { + label.setAlpha(enabled ? 1.0f : 0.5f); + } + } + /** * Sets the alpha for multiple views. */ @@ -90,4 +107,31 @@ public class UIUtils { } } } + + /** + * Displays a DatePickerDialog and sets the result to an EditText. + */ + public static void showDatePicker(Context context, EditText editText, Runnable onDateSet) { + Calendar c = Calendar.getInstance(); + DatePickerDialog d = new DatePickerDialog(context, + (dp, y, m, d1) -> { + String selectedDate = String.format(Locale.getDefault(), "%04d-%02d-%02d", y, m + 1, d1); + editText.setText(selectedDate); + if (onDateSet != null) onDateSet.run(); + }, + c.get(Calendar.YEAR), c.get(Calendar.MONTH), + c.get(Calendar.DAY_OF_MONTH)); + d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000); + d.show(); + } + + /** + * Displays a toast and returns false. Useful for validation chains. + */ + public static boolean showToast(Context context, String msg) { + if (context != null) { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + } + return false; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java new file mode 100644 index 00000000..685b645a --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java @@ -0,0 +1,402 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.DropdownDTO; +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.repositories.AppointmentRepository; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.repositories.ServiceRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.DateTimeUtils; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +/** + * ViewModel for managing appointment details and form state. + */ +@HiltViewModel +public class AppointmentDetailViewModel extends ViewModel { + private final AppointmentRepository repository; + private final CustomerRepository customerRepository; + private final StoreRepository storeRepository; + private final PetRepository petRepository; + private final ServiceRepository serviceRepository; + + private final MutableLiveData> customers = new MutableLiveData<>(); + private final MutableLiveData> stores = new MutableLiveData<>(); + private final MutableLiveData> services = new MutableLiveData<>(); + private final MutableLiveData> customerPets = new MutableLiveData<>(); + private final MutableLiveData> storeEmployees = new MutableLiveData<>(); + private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); + + private long appointmentId = -1; + private Long currentCustomerId; + private Long currentStoreId; + private Long currentPetId; + private Long currentServiceId; + private Long currentStaffId; + + /** + * Constructor for AppointmentDetailViewModel. + */ + @Inject + public AppointmentDetailViewModel( + AppointmentRepository repository, + CustomerRepository customerRepository, + StoreRepository storeRepository, + PetRepository petRepository, + ServiceRepository serviceRepository) { + this.repository = repository; + this.customerRepository = customerRepository; + this.storeRepository = storeRepository; + this.petRepository = petRepository; + this.serviceRepository = serviceRepository; + } + + // Initial Data Loading + + /** + * Loads initial dropdown data for customers, stores, and services. + */ + public void loadInitialFormData() { + customerRepository.getCustomerDropdowns().observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) customers.setValue(r.data); + }); + storeRepository.getStoreDropdowns().observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) stores.setValue(r.data); + }); + serviceRepository.getAllServices(0, 200, null, "serviceName").observeForever(r -> { + if (r.status == Resource.Status.SUCCESS && r.data != null) services.setValue(r.data.getContent()); + }); + } + + // LiveData Getters + + /** + * Returns the LiveData for the list of customers. + */ + public LiveData> getCustomers() { return customers; } + + /** + * Returns the LiveData for the list of stores. + */ + public LiveData> getStores() { return stores; } + + /** + * Returns the LiveData for the list of services. + */ + public LiveData> getServices() { return services; } + + /** + * Returns the LiveData for the list of pets for the current customer. + */ + public LiveData> getCustomerPets() { return customerPets; } + + /** + * Returns the LiveData for the list of employees for the current store. + */ + public LiveData> getStoreEmployees() { return storeEmployees; } + + /** + * Returns the LiveData for the view state. + */ + public LiveData getViewState() { return viewState; } + + //State Getters + + /** + * Returns the current appointment ID. + */ + public long getAppointmentId() { return appointmentId; } + + /** + * Sets the current appointment ID and updates the mode. + */ + public void setAppointmentId(long id) { + this.appointmentId = id; + initMode(id != -1); + } + + // Selection Handlers for spinners + + /** + * Handles customer selection and loads their pets. + */ + public void onCustomerSelected(int position) { + List list = customers.getValue(); + if (position > 0 && list != null && position <= list.size()) { + currentCustomerId = list.get(position - 1).getId(); + loadPetsForCustomer(currentCustomerId); + updateViewState(s -> { + s.selectedCustomerId = currentCustomerId; + s.isPetEnabled = !s.isEditing; + }); + } else { + currentCustomerId = null; + customerPets.setValue(new ArrayList<>()); + updateViewState(s -> { + s.selectedCustomerId = null; + s.isPetEnabled = false; + }); + } + } + + /** + * Handles store selection and loads its employees. + */ + public void onStoreSelected(int position) { + List list = stores.getValue(); + if (position > 0 && list != null && position <= list.size()) { + currentStoreId = list.get(position - 1).getId(); + loadEmployeesForStore(currentStoreId); + updateViewState(s -> { + s.selectedStoreId = currentStoreId; + s.isStaffEnabled = !s.isPast; + }); + } else { + currentStoreId = null; + storeEmployees.setValue(new ArrayList<>()); + updateViewState(s -> { + s.selectedStoreId = null; + s.isStaffEnabled = false; + }); + } + } + + /** + * Handles service selection. + */ + public void onServiceSelected(int position) { + List list = services.getValue(); + currentServiceId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getServiceId() : null; + updateViewState(s -> s.selectedServiceId = currentServiceId); + } + + /** + * Handles pet selection. + */ + public void onPetSelected(int position) { + List list = customerPets.getValue(); + currentPetId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; + updateViewState(s -> s.selectedPetId = currentPetId); + } + + /** + * Handles staff selection. + */ + public void onStaffSelected(int position) { + List list = storeEmployees.getValue(); + currentStaffId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; + updateViewState(s -> s.selectedStaffId = currentStaffId); + } + + /** + * Loads the list of pets for a specific customer. + */ + private void loadPetsForCustomer(Long customerId) { + petRepository.getCustomerPets(customerId).observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) customerPets.setValue(r.data); + }); + } + + /** + * Loads the list of employees for a specific store. + */ + private void loadEmployeesForStore(Long storeId) { + storeRepository.getStoreEmployees(storeId).observeForever(r -> { + if (r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data); + }); + } + + // Appointment Detail CRUD + + /** + * Fetches appointment details and populates internal state. + */ + public LiveData> loadAppointment() { + MutableLiveData> result = new MutableLiveData<>(); + repository.getAppointmentById(appointmentId).observeForever(resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + AppointmentDTO a = resource.data; + currentCustomerId = a.getCustomerId(); + currentStoreId = a.getStoreId(); + currentPetId = a.getPetId(); + currentServiceId = a.getServiceId(); + currentStaffId = a.getEmployeeId(); + + updateViewState(s -> { + s.selectedCustomerId = currentCustomerId; + s.selectedStoreId = currentStoreId; + s.selectedPetId = currentPetId; + s.selectedServiceId = currentServiceId; + s.selectedStaffId = currentStaffId; + }); + + if (currentCustomerId != null) loadPetsForCustomer(currentCustomerId); + if (currentStoreId != null) loadEmployeesForStore(currentStoreId); + } + result.setValue(resource); + }); + return result; + } + + /** + * Saves the appointment by building the DTO from tracked state. + */ + public LiveData> saveAppointment(String date, String time, String status) { + AppointmentDTO dto = new AppointmentDTO(currentCustomerId, currentStoreId, currentServiceId, currentStaffId, date, time, status, currentPetId); + if (appointmentId != -1) { + return repository.updateAppointment(appointmentId, dto); + } else { + return repository.createAppointment(dto); + } + } + + /** + * Deletes the current appointment. + */ + public LiveData> deleteAppointment() { + return repository.deleteAppointment(appointmentId); + } + + // UI Logic + + /** + * Updates the UI state when date, time, or status changes. + */ + public void onDateOrTimeChanged(String date, String time, String currentStatus) { + updateViewState(s -> { + s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus); + boolean isPast = DateTimeUtils.isDateTimeInPast(date, time); + boolean isCancelled = "Cancelled".equalsIgnoreCase(currentStatus); + + if (isCancelled) { + s.isPast = true; + setAllFieldsEnabled(s, false); + s.isStatusEnabled = false; + s.isSaveVisible = false; + } else if (isPast) { + s.isPast = true; + setAllFieldsEnabled(s, false); + s.isStatusEnabled = true; + } else { + s.isPast = false; + if (!s.isEditing) { + s.isCustomerEnabled = true; + s.isStoreEnabled = true; + s.isServiceEnabled = true; + s.isPetEnabled = currentCustomerId != null; + } + s.isDateEnabled = true; + s.isTimeEnabled = true; + s.isStatusEnabled = true; + } + }); + } + + /** + * Calculates available appointment statuses based on the current context. + */ + private String[] calculateAvailableStatuses(boolean isEditing, String date, String currentTime, String currentStatus) { + if (!isEditing) return new String[]{"Booked"}; + if (date == null || date.isEmpty()) return new String[]{}; + if ("Cancelled".equalsIgnoreCase(currentStatus)) return new String[]{"Cancelled"}; + if (DateTimeUtils.isDateTimeInPast(date, currentTime)) return new String[]{"Completed", "Missed"}; + return new String[]{"Booked", "Cancelled"}; + } + + /** + * Helper method to enable or disable all fields. + */ + private void setAllFieldsEnabled(ViewState s, boolean enabled) { + s.isCustomerEnabled = enabled; + s.isStoreEnabled = enabled; + s.isPetEnabled = enabled; + s.isServiceEnabled = enabled; + s.isStaffEnabled = enabled; + s.isDateEnabled = enabled; + s.isTimeEnabled = enabled; + } + + /** + * Initializes the UI mode (Create vs Edit). + */ + public void initMode(boolean isEditing) { + updateViewState(s -> { + s.isEditing = isEditing; + s.isDeleteVisible = isEditing; + if (isEditing) { + s.isCustomerEnabled = false; + s.isStoreEnabled = false; + s.isPetEnabled = false; + s.isServiceEnabled = false; + } else { + s.isCustomerEnabled = true; + s.isStoreEnabled = true; + s.isServiceEnabled = true; + s.isPetEnabled = false; // until customer selected + s.isStaffEnabled = false; // until store selected + s.availableStatuses = new String[]{"Booked"}; + } + }); + } + + /** + * Validates if a booking is in the future. + */ + public boolean isValidFutureBooking(String status, String date, String time) { + return !"BOOKED".equalsIgnoreCase(status) || !DateTimeUtils.isDateTimeInPast(date, time); + } + + /** + * Helper to update the view state and notify observers. + */ + private void updateViewState(Action action) { + ViewState current = viewState.getValue(); + if (current != null) { + action.run(current); + viewState.setValue(current); + } + } + + private interface Action { + void run(T t); + } + + /** + * A Class to show the states of Appointment Detail Fragment. + */ + public static class ViewState { + public boolean isPast = false; + public boolean isEditing = false; + public boolean isSaveVisible = true; + public boolean isDeleteVisible = false; + public boolean isCustomerEnabled = true; + public boolean isStoreEnabled = true; + public boolean isPetEnabled = false; + public boolean isServiceEnabled = true; + public boolean isStaffEnabled = false; + public boolean isDateEnabled = true; + public boolean isTimeEnabled = true; + public boolean isStatusEnabled = true; + public String[] availableStatuses = new String[]{}; + + // Selected IDs + public Long selectedCustomerId = null; + public Long selectedStoreId = null; + public Long selectedPetId = null; + public Long selectedServiceId = null; + public Long selectedStaffId = null; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java index 32f359e7..5605a8d1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java @@ -1,23 +1,14 @@ package com.example.petstoremobile.viewmodels; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.ServiceDTO; -import com.example.petstoremobile.repositories.AppointmentRepository; -import com.example.petstoremobile.repositories.CustomerRepository; -import com.example.petstoremobile.repositories.PetRepository; -import com.example.petstoremobile.repositories.ServiceRepository; -import com.example.petstoremobile.repositories.StoreRepository; -import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.repositories.AppointmentRepository; -import java.util.ArrayList; import java.util.List; import javax.inject.Inject; @@ -27,38 +18,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class AppointmentViewModel extends ViewModel { private final AppointmentRepository repository; - private final CustomerRepository customerRepository; - private final StoreRepository storeRepository; - private final PetRepository petRepository; - private final ServiceRepository serviceRepository; - - private final MutableLiveData> customers = new MutableLiveData<>(); - private final MutableLiveData> stores = new MutableLiveData<>(); - private final MutableLiveData> services = new MutableLiveData<>(); - private final MutableLiveData> customerPets = new MutableLiveData<>(); - private final MutableLiveData> storeEmployees = new MutableLiveData<>(); - - private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); - - private long appointmentId = -1; - private Long currentCustomerId; - private Long currentStoreId; - private Long currentPetId; - private Long currentServiceId; - private Long currentStaffId; @Inject - public AppointmentViewModel( - AppointmentRepository repository, - CustomerRepository customerRepository, - StoreRepository storeRepository, - PetRepository petRepository, - ServiceRepository serviceRepository) { + public AppointmentViewModel(AppointmentRepository repository) { this.repository = repository; - this.customerRepository = customerRepository; - this.storeRepository = storeRepository; - this.petRepository = petRepository; - this.serviceRepository = serviceRepository; } // API CRUD @@ -70,307 +33,10 @@ public class AppointmentViewModel extends ViewModel { return repository.getAllAppointments(page, size, query, status, storeId, date, employeeId); } - /** - * Retrieves a single appointment by its ID. - */ - public LiveData> getAppointmentById(Long id) { - return repository.getAppointmentById(id); - } - - /** - * Creates a new appointment. - */ - public LiveData> createAppointment(AppointmentDTO appointment) { - return repository.createAppointment(appointment); - } - - /** - * Updates an existing appointment record by ID. - */ - public LiveData> updateAppointment(Long id, AppointmentDTO appointment) { - return repository.updateAppointment(id, appointment); - } - - /** - * Deletes an appointment record by ID. - */ - public LiveData> deleteAppointment(Long id) { - return repository.deleteAppointment(id); - } - /** * Deletes multiple appointment records. */ public LiveData> bulkDeleteAppointments(List ids) { return repository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); } - - // Initial Data Loading - - /** - * Loads initial dropdown data for customers, stores, and services. - */ - public void loadInitialFormData() { - customerRepository.getCustomerDropdowns().observeForever(r -> { - if (r.status == Resource.Status.SUCCESS) customers.setValue(r.data); - }); - storeRepository.getStoreDropdowns().observeForever(r -> { - if (r.status == Resource.Status.SUCCESS) stores.setValue(r.data); - }); - serviceRepository.getAllServices(0, 200, null, "serviceName").observeForever(r -> { - if (r.status == Resource.Status.SUCCESS && r.data != null) services.setValue(r.data.getContent()); - }); - } - - // LiveData Getters - - public LiveData> getCustomers() { return customers; } - public LiveData> getStores() { return stores; } - public LiveData> getServices() { return services; } - public LiveData> getCustomerPets() { return customerPets; } - public LiveData> getStoreEmployees() { return storeEmployees; } - public LiveData getViewState() { return viewState; } - - //State Getters - - public long getAppointmentId() { return appointmentId; } - - /** - * Sets the current appointment ID and updates the mode. - */ - public void setAppointmentId(long id) { - this.appointmentId = id; - initMode(id != -1); - } - - // Selection Handlers for spinners - - public void onCustomerSelected(int position) { - List list = customers.getValue(); - if (position > 0 && list != null && position <= list.size()) { - currentCustomerId = list.get(position - 1).getId(); - loadPetsForCustomer(currentCustomerId); - updateViewState(s -> { - s.selectedCustomerId = currentCustomerId; - s.isPetEnabled = !s.isEditing; - }); - } else { - currentCustomerId = null; - customerPets.setValue(new ArrayList<>()); - updateViewState(s -> { - s.selectedCustomerId = null; - s.isPetEnabled = false; - }); - } - } - - public void onStoreSelected(int position) { - List list = stores.getValue(); - if (position > 0 && list != null && position <= list.size()) { - currentStoreId = list.get(position - 1).getId(); - loadEmployeesForStore(currentStoreId); - updateViewState(s -> { - s.selectedStoreId = currentStoreId; - s.isStaffEnabled = !s.isPast; - }); - } else { - currentStoreId = null; - storeEmployees.setValue(new ArrayList<>()); - updateViewState(s -> { - s.selectedStoreId = null; - s.isStaffEnabled = false; - }); - } - } - - public void onServiceSelected(int position) { - List list = services.getValue(); - currentServiceId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getServiceId() : null; - updateViewState(s -> s.selectedServiceId = currentServiceId); - } - - public void onPetSelected(int position) { - List list = customerPets.getValue(); - currentPetId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; - updateViewState(s -> s.selectedPetId = currentPetId); - } - - public void onStaffSelected(int position) { - List list = storeEmployees.getValue(); - currentStaffId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; - updateViewState(s -> s.selectedStaffId = currentStaffId); - } - - private void loadPetsForCustomer(Long customerId) { - petRepository.getCustomerPets(customerId).observeForever(r -> { - if (r.status == Resource.Status.SUCCESS) customerPets.setValue(r.data); - }); - } - - private void loadEmployeesForStore(Long storeId) { - storeRepository.getStoreEmployees(storeId).observeForever(r -> { - if (r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data); - }); - } - - // Appointment Detail CRUD - - /** - * Fetches appointment details and populates internal state. - */ - public LiveData> loadAppointment() { - MutableLiveData> result = new MutableLiveData<>(); - repository.getAppointmentById(appointmentId).observeForever(resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - AppointmentDTO a = resource.data; - currentCustomerId = a.getCustomerId(); - currentStoreId = a.getStoreId(); - currentPetId = a.getPetId(); - currentServiceId = a.getServiceId(); - currentStaffId = a.getEmployeeId(); - - updateViewState(s -> { - s.selectedCustomerId = currentCustomerId; - s.selectedStoreId = currentStoreId; - s.selectedPetId = currentPetId; - s.selectedServiceId = currentServiceId; - s.selectedStaffId = currentStaffId; - }); - - if (currentCustomerId != null) loadPetsForCustomer(currentCustomerId); - if (currentStoreId != null) loadEmployeesForStore(currentStoreId); - } - result.setValue(resource); - }); - return result; - } - - /** - * Saves the appointment by building the DTO from tracked state. - */ - public LiveData> saveAppointment(String date, String time, String status) { - AppointmentDTO dto = new AppointmentDTO(currentCustomerId, currentStoreId, currentServiceId, currentStaffId, date, time, status, currentPetId); - if (appointmentId != -1) { - return repository.updateAppointment(appointmentId, dto); - } else { - return repository.createAppointment(dto); - } - } - - /** - * Deletes the current appointment. - */ - public LiveData> deleteAppointment() { - return repository.deleteAppointment(appointmentId); - } - - // --- UI Logic --- - - public void onDateOrTimeChanged(String date, String time, String currentStatus) { - updateViewState(s -> { - s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus); - boolean isPast = DateTimeUtils.isDateTimeInPast(date, time); - boolean isCancelled = "Cancelled".equalsIgnoreCase(currentStatus); - - if (isCancelled) { - s.isPast = true; - setAllFieldsEnabled(s, false); - s.isStatusEnabled = false; - s.isSaveVisible = false; - } else if (isPast) { - s.isPast = true; - setAllFieldsEnabled(s, false); - s.isStatusEnabled = true; - } else { - s.isPast = false; - if (!s.isEditing) { - s.isCustomerEnabled = true; - s.isStoreEnabled = true; - s.isServiceEnabled = true; - s.isPetEnabled = currentCustomerId != null; - } - s.isDateEnabled = true; - s.isTimeEnabled = true; - s.isStatusEnabled = true; - } - }); - } - - private String[] calculateAvailableStatuses(boolean isEditing, String date, String currentTime, String currentStatus) { - if (!isEditing) return new String[]{"Booked"}; - if (date == null || date.isEmpty()) return new String[]{}; - if ("Cancelled".equalsIgnoreCase(currentStatus)) return new String[]{"Cancelled"}; - if (DateTimeUtils.isDateTimeInPast(date, currentTime)) return new String[]{"Completed", "Missed"}; - return new String[]{"Booked", "Cancelled"}; - } - - private void setAllFieldsEnabled(ViewState s, boolean enabled) { - s.isCustomerEnabled = enabled; - s.isStoreEnabled = enabled; - s.isPetEnabled = enabled; - s.isServiceEnabled = enabled; - s.isStaffEnabled = enabled; - s.isDateEnabled = enabled; - s.isTimeEnabled = enabled; - } - - public void initMode(boolean isEditing) { - updateViewState(s -> { - s.isEditing = isEditing; - s.isDeleteVisible = isEditing; - if (isEditing) { - s.isCustomerEnabled = false; - s.isStoreEnabled = false; - s.isPetEnabled = false; - s.isServiceEnabled = false; - } else { - s.isCustomerEnabled = true; - s.isStoreEnabled = true; - s.isServiceEnabled = true; - s.isPetEnabled = false; // until customer selected - s.isStaffEnabled = false; // until store selected - s.availableStatuses = new String[]{"Booked"}; - } - }); - } - - public boolean isValidFutureBooking(String status, String date, String time) { - return !"BOOKED".equalsIgnoreCase(status) || !DateTimeUtils.isDateTimeInPast(date, time); - } - - private void updateViewState(Action action) { - ViewState current = viewState.getValue(); - if (current != null) { - action.run(current); - viewState.setValue(current); - } - } - - private interface Action { void run(T t); } - - /** - * A Class to show the states of Appointment Detail Fragment. - */ - public static class ViewState { - public boolean isPast = false; - public boolean isEditing = false; - public boolean isSaveVisible = true; - public boolean isDeleteVisible = false; - public boolean isCustomerEnabled = true; - public boolean isStoreEnabled = true; - public boolean isPetEnabled = false; - public boolean isServiceEnabled = true; - public boolean isStaffEnabled = false; - public boolean isDateEnabled = true; - public boolean isTimeEnabled = true; - public boolean isStatusEnabled = true; - public String[] availableStatuses = new String[]{}; - - // Selected IDs - public Long selectedCustomerId = null; - public Long selectedStoreId = null; - public Long selectedPetId = null; - public Long selectedServiceId = null; - public Long selectedStaffId = null; - } }