Close chat #169
@@ -13,16 +13,12 @@ import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.example.petstoremobile.databinding.FragmentAppointmentDetailBinding;
|
||||
import com.example.petstoremobile.dtos.*;
|
||||
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 com.example.petstoremobile.viewmodels.CustomerViewModel;
|
||||
import com.example.petstoremobile.viewmodels.PetViewModel;
|
||||
import com.example.petstoremobile.viewmodels.ServiceViewModel;
|
||||
import com.example.petstoremobile.viewmodels.StoreViewModel;
|
||||
import com.example.petstoremobile.viewmodels.UserViewModel;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -36,40 +32,22 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
|
||||
private FragmentAppointmentDetailBinding binding;
|
||||
|
||||
private long appointmentId = -1;
|
||||
private boolean isEditing = false;
|
||||
private boolean isPastAppointment = false;
|
||||
private long preselectedPetId = -1;
|
||||
private long preselectedServiceId = -1;
|
||||
private long preselectedCustomerId = -1;
|
||||
private long preselectedStoreId = -1;
|
||||
private long preselectedStaffId = -1;
|
||||
|
||||
private List<DropdownDTO> petList = new ArrayList<>();
|
||||
private List<ServiceDTO> serviceList = new ArrayList<>();
|
||||
private List<DropdownDTO> customerList = new ArrayList<>();
|
||||
private List<DropdownDTO> storeList = new ArrayList<>();
|
||||
private List<DropdownDTO> staffList = new ArrayList<>();
|
||||
|
||||
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 PetViewModel petViewModel;
|
||||
private ServiceViewModel serviceViewModel;
|
||||
private StoreViewModel storeViewModel;
|
||||
private CustomerViewModel customerViewModel;
|
||||
private UserViewModel userViewModel;
|
||||
private boolean isUpdatingUI = false;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class);
|
||||
petViewModel = new ViewModelProvider(this).get(PetViewModel.class);
|
||||
serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class);
|
||||
storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class);
|
||||
customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class);
|
||||
userViewModel = new ViewModelProvider(this).get(UserViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -83,7 +61,8 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setupSpinners();
|
||||
setupDatePicker();
|
||||
loadSpinnersData();
|
||||
observeViewModel();
|
||||
appointmentViewModel.loadInitialFormData();
|
||||
handleArguments();
|
||||
|
||||
binding.btnApptBack.setOnClickListener(v -> navigateBack());
|
||||
@@ -101,9 +80,10 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
* Configures the adapters for spinners.
|
||||
*/
|
||||
private void setupSpinners() {
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus,
|
||||
new String[]{"Booked", "Completed", "Cancelled", "Missed"});
|
||||
//Status Spinner is empty by default the date determines whats in here
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, new String[]{});
|
||||
|
||||
// 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]);
|
||||
@@ -113,50 +93,41 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
// Pet and Staff spinners disabled by until parent selection
|
||||
UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff);
|
||||
|
||||
// Listener to load pets based on selected customer
|
||||
// Listener to notify ViewModel of customer selection
|
||||
binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (position > 0 && position <= customerList.size()) {
|
||||
DropdownDTO selectedCustomer = customerList.get(position - 1);
|
||||
loadPets(selectedCustomer.getId());
|
||||
if (!isEditing) {
|
||||
UIUtils.setViewsEnabled(true, binding.spinnerPet);
|
||||
}
|
||||
} else {
|
||||
petList.clear();
|
||||
refreshPetSpinner();
|
||||
if (!isEditing) {
|
||||
binding.spinnerPet.setSelection(0);
|
||||
UIUtils.setViewsEnabled(false, binding.spinnerPet);
|
||||
}
|
||||
}
|
||||
appointmentViewModel.onCustomerSelected(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {}
|
||||
});
|
||||
|
||||
// Listener to load staff based on selected store
|
||||
// Listener to notify ViewModel of store selection
|
||||
binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (position > 0 && position <= storeList.size()) {
|
||||
DropdownDTO selectedStore = storeList.get(position - 1);
|
||||
loadStaff(selectedStore.getId());
|
||||
if (!isPastAppointment) {
|
||||
UIUtils.setViewsEnabled(true, binding.spinnerStaff);
|
||||
}
|
||||
} else {
|
||||
staffList.clear();
|
||||
refreshStaffSpinner();
|
||||
if (!isEditing) {
|
||||
binding.spinnerStaff.setSelection(0);
|
||||
UIUtils.setViewsEnabled(false, binding.spinnerStaff);
|
||||
}
|
||||
}
|
||||
appointmentViewModel.onStoreSelected(position);
|
||||
}
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {}
|
||||
});
|
||||
|
||||
// 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)));
|
||||
|
||||
// Listeners for time changes
|
||||
binding.spinnerHour.setOnItemSelectedListener(new OnIndexSelected(p -> notifyDateTimeStatusChange()));
|
||||
binding.spinnerMinute.setOnItemSelectedListener(new OnIndexSelected(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) {}
|
||||
});
|
||||
@@ -167,11 +138,13 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
*/
|
||||
private void setupDatePicker() {
|
||||
binding.etAppointmentDate.setOnClickListener(v -> {
|
||||
if (isPastAppointment) return;
|
||||
Calendar c = Calendar.getInstance();
|
||||
DatePickerDialog d = new DatePickerDialog(requireContext(),
|
||||
(dp,y,m,d1) -> binding.etAppointmentDate.setText(
|
||||
String.format("%04d-%02d-%02d", y, m+1, d1)),
|
||||
(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);
|
||||
@@ -180,118 +153,79 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all required data for spinners from the backend.
|
||||
* Observes the ViewModel for UI state and list updates.
|
||||
*/
|
||||
private void loadSpinnersData() {
|
||||
loadServices();
|
||||
loadCustomers();
|
||||
loadStores();
|
||||
private void observeViewModel() {
|
||||
appointmentViewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState);
|
||||
|
||||
// Populate spinners when data arrives
|
||||
appointmentViewModel.getCustomers().observe(getViewLifecycleOwner(), list ->
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, list, DropdownDTO::getLabel, "-- Select Customer --", preselectedCustomerId, DropdownDTO::getId));
|
||||
|
||||
appointmentViewModel.getStores().observe(getViewLifecycleOwner(), list ->
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, list, DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId));
|
||||
|
||||
appointmentViewModel.getServices().observe(getViewLifecycleOwner(), list ->
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, list, ServiceDTO::getServiceName, "-- Select Service --", preselectedServiceId, ServiceDTO::getServiceId));
|
||||
|
||||
appointmentViewModel.getCustomerPets().observe(getViewLifecycleOwner(), list ->
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, list, DropdownDTO::getLabel, "-- Select Pet --", preselectedPetId, DropdownDTO::getId));
|
||||
|
||||
appointmentViewModel.getStoreEmployees().observe(getViewLifecycleOwner(), list ->
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, list, DropdownDTO::getLabel, "-- Select Staff --", preselectedStaffId, DropdownDTO::getId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of pets from the ViewModel, filtered by customerId.
|
||||
* Applies the ViewState provided by the ViewModel to the UI components.
|
||||
*/
|
||||
private void loadPets(Long customerId) {
|
||||
petViewModel.getCustomerPets(customerId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
petList = resource.data;
|
||||
refreshPetSpinner();
|
||||
}
|
||||
});
|
||||
private void applyViewState(AppointmentViewModel.ViewState state) {
|
||||
isUpdatingUI = true;
|
||||
|
||||
// Mode specific UI
|
||||
binding.tvApptMode.setText(state.isEditing ? "Edit Appointment" : "Add Appointment");
|
||||
binding.tvAppointmentId.setText("ID: " + 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);
|
||||
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() : "";
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, state.availableStatuses);
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, current);
|
||||
|
||||
isUpdatingUI = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the pet selection spinner.
|
||||
*/
|
||||
private void refreshPetSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList,
|
||||
DropdownDTO::getLabel, "-- Select Pet --",
|
||||
preselectedPetId, DropdownDTO::getId);
|
||||
}
|
||||
private void notifyDateTimeStatusChange() {
|
||||
if (isUpdatingUI) return;
|
||||
|
||||
/**
|
||||
* Loads the list of services from the API.
|
||||
*/
|
||||
private void loadServices() {
|
||||
serviceViewModel.getAllServices(0, 200, null, "serviceName").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
serviceList = resource.data.getContent();
|
||||
refreshServiceSpinner();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the service selection spinner.
|
||||
*/
|
||||
private void refreshServiceSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, serviceList,
|
||||
ServiceDTO::getServiceName, "-- Select Service --",
|
||||
preselectedServiceId, ServiceDTO::getServiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of customers from the API.
|
||||
*/
|
||||
private void loadCustomers() {
|
||||
customerViewModel.getCustomerDropdowns().observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
customerList = resource.data;
|
||||
refreshCustomerSpinner();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the customer selection spinner.
|
||||
*/
|
||||
private void refreshCustomerSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, customerList,
|
||||
DropdownDTO::getLabel,
|
||||
"-- Select Customer --",
|
||||
preselectedCustomerId, DropdownDTO::getId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of stores from the API.
|
||||
*/
|
||||
private void loadStores() {
|
||||
storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
storeList = resource.data;
|
||||
refreshStoreSpinner();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the store selection spinner.
|
||||
*/
|
||||
private void refreshStoreSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList,
|
||||
DropdownDTO::getLabel, "-- Select Store --",
|
||||
preselectedStoreId, DropdownDTO::getId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list of staff for a specific store.
|
||||
*/
|
||||
private void loadStaff(Long storeId) {
|
||||
storeViewModel.getStoreEmployees(storeId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
staffList = resource.data;
|
||||
refreshStaffSpinner();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the staff selection spinner.
|
||||
*/
|
||||
private void refreshStaffSpinner() {
|
||||
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, staffList,
|
||||
DropdownDTO::getLabel, "-- Select Staff --",
|
||||
preselectedStaffId, DropdownDTO::getId);
|
||||
String date = binding.etAppointmentDate.getText().toString();
|
||||
String time = buildTimeString();
|
||||
Object selected = binding.spinnerAppointmentStatus.getSelectedItem();
|
||||
String status = selected != null ? selected.toString() : "";
|
||||
appointmentViewModel.onDateOrTimeChanged(date, time, status);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,27 +234,10 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
private void handleArguments() {
|
||||
Bundle a = getArguments();
|
||||
if (a != null && a.containsKey("appointmentId")) {
|
||||
//edit mode
|
||||
isEditing = true;
|
||||
appointmentId = a.getLong("appointmentId");
|
||||
binding.tvApptMode.setText("Edit Appointment");
|
||||
binding.tvAppointmentId.setText("ID: " + appointmentId);
|
||||
binding.tvAppointmentId.setVisibility(View.VISIBLE);
|
||||
binding.btnDeleteAppointment.setVisibility(View.VISIBLE);
|
||||
|
||||
UIUtils.setViewsEnabled(false, binding.spinnerCustomer, binding.spinnerStore, binding.spinnerPet, binding.spinnerService);
|
||||
UIUtils.setViewsAlpha(0.5f, binding.tvLabelCustomer, binding.tvLabelStore, binding.tvLabelPet, binding.tvLabelService);
|
||||
|
||||
appointmentViewModel.setAppointmentId(a.getLong("appointmentId"));
|
||||
loadAppointmentData();
|
||||
} else {
|
||||
//add mode
|
||||
binding.tvApptMode.setText("Add Appointment");
|
||||
binding.btnDeleteAppointment.setVisibility(View.GONE);
|
||||
binding.tvAppointmentId.setVisibility(View.GONE);
|
||||
|
||||
UIUtils.setViewsEnabled(true, binding.spinnerCustomer, binding.spinnerStore, binding.spinnerService);
|
||||
UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff, binding.spinnerAppointmentStatus);
|
||||
UIUtils.setViewsAlpha(1.0f, binding.tvLabelCustomer, binding.tvLabelStore, binding.tvLabelPet, binding.tvLabelService, binding.tvLabelStaff);
|
||||
appointmentViewModel.setAppointmentId(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,229 +245,83 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
* Fetches specific appointment details from the backend using the ID.
|
||||
*/
|
||||
private void loadAppointmentData() {
|
||||
appointmentViewModel.getAppointmentById(appointmentId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
AppointmentDTO a = resource.data;
|
||||
preselectedPetId = (a.getPetId() != null) ? a.getPetId() : -1;
|
||||
preselectedServiceId = (a.getServiceId() != null) ? a.getServiceId() : -1;
|
||||
preselectedCustomerId = (a.getCustomerId() != null) ? a.getCustomerId() : -1;
|
||||
preselectedStoreId = (a.getStoreId() != null) ? a.getStoreId() : -1;
|
||||
preselectedStaffId = (a.getEmployeeId() != null) ? a.getEmployeeId() : -1;
|
||||
appointmentViewModel.loadAppointment().observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null || resource.status != Resource.Status.SUCCESS || resource.data == null) return;
|
||||
AppointmentDTO a = resource.data;
|
||||
preselectedPetId = a.getPetId() != null ? a.getPetId() : -1;
|
||||
preselectedServiceId = a.getServiceId() != null ? a.getServiceId() : -1;
|
||||
preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1;
|
||||
preselectedStoreId = a.getStoreId() != null ? a.getStoreId() : -1;
|
||||
preselectedStaffId = a.getEmployeeId() != null ? a.getEmployeeId() : -1;
|
||||
|
||||
binding.etAppointmentDate.setText(a.getAppointmentDate());
|
||||
|
||||
// Pre-fill time spinners
|
||||
String time = a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00";
|
||||
if (time.length() > 5) time = time.substring(0, 5);
|
||||
String[] parts = time.split(":");
|
||||
if (parts.length == 2) {
|
||||
try {
|
||||
int hour = Integer.parseInt(parts[0]);
|
||||
int min = Integer.parseInt(parts[1]);
|
||||
for (int i = 0; i < HOURS.length; i++)
|
||||
if (HOURS[i] == hour) { binding.spinnerHour.setSelection(i); break; }
|
||||
for (int i = 0; i < MINUTES.length; i++)
|
||||
if (MINUTES[i] == min) { binding.spinnerMinute.setSelection(i); break; }
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
// Match Title labels with backend values
|
||||
String status = a.getAppointmentStatus();
|
||||
if (status != null && !status.isEmpty()) {
|
||||
String formattedStatus = status.substring(0, 1).toUpperCase() + status.substring(1).toLowerCase();
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, formattedStatus);
|
||||
}
|
||||
|
||||
checkIfPastAndDisable(a.getAppointmentDate(), time);
|
||||
|
||||
refreshPetSpinner();
|
||||
refreshServiceSpinner();
|
||||
refreshCustomerSpinner();
|
||||
refreshStoreSpinner();
|
||||
refreshStaffSpinner();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Failed to load appointment: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
binding.etAppointmentDate.setText(a.getAppointmentDate());
|
||||
parseAndSetTimeSpinners(a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00");
|
||||
|
||||
String status = a.getAppointmentStatus();
|
||||
if (status != null && !status.isEmpty()) {
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, DateTimeUtils.formatStatusFromBackend(status));
|
||||
}
|
||||
notifyDateTimeStatusChange();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the appointment is in the past and disables fields.
|
||||
*/
|
||||
private void checkIfPastAndDisable(String date, String time) {
|
||||
if (date == null || time == null) return;
|
||||
try {
|
||||
Calendar selected = Calendar.getInstance();
|
||||
String[] dateParts = date.split("-");
|
||||
String[] timeParts = time.split(":");
|
||||
selected.set(
|
||||
Integer.parseInt(dateParts[0]),
|
||||
Integer.parseInt(dateParts[1]) - 1,
|
||||
Integer.parseInt(dateParts[2]),
|
||||
Integer.parseInt(timeParts[0]),
|
||||
Integer.parseInt(timeParts[1]),
|
||||
0
|
||||
);
|
||||
|
||||
Object selectedItem = binding.spinnerAppointmentStatus.getSelectedItem();
|
||||
String currentStatus = selectedItem != null ? selectedItem.toString() : "";
|
||||
|
||||
// If the appointment is already Cancelled, disable all fields
|
||||
if ("Cancelled".equalsIgnoreCase(currentStatus)) {
|
||||
isPastAppointment = true;
|
||||
disableAllExceptStatus();
|
||||
UIUtils.setViewsEnabled(false, binding.spinnerAppointmentStatus);
|
||||
binding.btnSaveAppointment.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the appointment date/time is in the past
|
||||
if (selected.before(Calendar.getInstance())) {
|
||||
isPastAppointment = true;
|
||||
disableAllExceptStatus();
|
||||
|
||||
// Make status spinner only have Completed or Missed
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus,
|
||||
new String[]{"Completed", "Missed"});
|
||||
|
||||
// Restore selection if it's already one of the valid options
|
||||
if (currentStatus.equals("Completed") || currentStatus.equals("Missed")) {
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, currentStatus);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("APPT_DETAIL", "Error parsing date/time for past check: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables all input fields except the status spinner
|
||||
*/
|
||||
private void disableAllExceptStatus() {
|
||||
UIUtils.setViewsEnabled(false,
|
||||
binding.spinnerCustomer, binding.spinnerStore, binding.spinnerPet,
|
||||
binding.spinnerService, binding.spinnerStaff, binding.etAppointmentDate,
|
||||
binding.spinnerHour, binding.spinnerMinute);
|
||||
|
||||
UIUtils.setViewsAlpha(0.5f,
|
||||
binding.tvLabelCustomer, binding.tvLabelStore, binding.tvLabelPet,
|
||||
binding.tvLabelService, binding.tvLabelStaff, binding.tvLabelDate,
|
||||
binding.tvLabelTime);
|
||||
|
||||
// Keep status enabled
|
||||
UIUtils.setViewsEnabled(true, binding.spinnerAppointmentStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input and saves the appointment to the backend.
|
||||
*/
|
||||
private void saveAppointment() {
|
||||
if (binding.spinnerCustomer.getSelectedItemPosition() == 0) {
|
||||
Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return;
|
||||
}
|
||||
if (binding.spinnerStore.getSelectedItemPosition() == 0) {
|
||||
Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return;
|
||||
}
|
||||
if (binding.spinnerPet.getSelectedItemPosition() == 0) {
|
||||
Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return;
|
||||
}
|
||||
if (binding.spinnerService.getSelectedItemPosition() == 0) {
|
||||
Toast.makeText(getContext(), "Select a service", Toast.LENGTH_SHORT).show(); return;
|
||||
}
|
||||
if (!validateRequiredFields()) return;
|
||||
|
||||
String date = binding.etAppointmentDate.getText().toString().trim();
|
||||
if (date.isEmpty()) {
|
||||
Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return;
|
||||
}
|
||||
|
||||
DropdownDTO customer = customerList.get(binding.spinnerCustomer.getSelectedItemPosition() - 1);
|
||||
DropdownDTO store = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1);
|
||||
DropdownDTO pet = petList.get(binding.spinnerPet.getSelectedItemPosition() - 1);
|
||||
ServiceDTO service = serviceList.get(binding.spinnerService.getSelectedItemPosition() - 1);
|
||||
|
||||
Long employeeId = null;
|
||||
if (binding.spinnerStaff.getSelectedItemPosition() > 0) {
|
||||
employeeId = staffList.get(binding.spinnerStaff.getSelectedItemPosition() - 1).getId();
|
||||
}
|
||||
|
||||
String time = String.format("%02d:%02d",
|
||||
HOURS[binding.spinnerHour.getSelectedItemPosition()],
|
||||
MINUTES[binding.spinnerMinute.getSelectedItemPosition()]);
|
||||
|
||||
// Get status and convert to uppercase for backend
|
||||
String time = buildTimeString();
|
||||
String status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase();
|
||||
|
||||
|
||||
// Validate future date+time if status is BOOKED
|
||||
if ("BOOKED".equalsIgnoreCase(status)) {
|
||||
try {
|
||||
String[] dateParts = date.split("-");
|
||||
String[] timeParts = time.split(":");
|
||||
Calendar selected = Calendar.getInstance();
|
||||
selected.set(
|
||||
Integer.parseInt(dateParts[0]),
|
||||
Integer.parseInt(dateParts[1]) - 1,
|
||||
Integer.parseInt(dateParts[2]),
|
||||
Integer.parseInt(timeParts[0]),
|
||||
Integer.parseInt(timeParts[1]),
|
||||
0
|
||||
);
|
||||
if (selected.before(Calendar.getInstance())) {
|
||||
DialogUtils.showInfoDialog(requireContext(), "Invalid Time",
|
||||
"Booked appointments must be in the future. " +
|
||||
"Please select a future date and time.");
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("APPT_SAVE", "Date parse error: " + e.getMessage());
|
||||
}
|
||||
if (!appointmentViewModel.isValidFutureBooking(status, date, time)) {
|
||||
DialogUtils.showInfoDialog(requireContext(), "Invalid Time", "Booked appointments must be in the future.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build DTO with all required IDs
|
||||
AppointmentDTO dto = new AppointmentDTO(
|
||||
customer.getId(),
|
||||
store.getId(),
|
||||
service.getServiceId(),
|
||||
employeeId,
|
||||
date,
|
||||
time,
|
||||
status,
|
||||
pet.getId()
|
||||
);
|
||||
|
||||
androidx.lifecycle.Observer<Resource<AppointmentDTO>> observer = resource -> {
|
||||
appointmentViewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), isEditing ? "Updated" : "Saved", Toast.LENGTH_SHORT).show();
|
||||
AppointmentViewModel.ViewState state = appointmentViewModel.getViewState().getValue();
|
||||
String message = (state != null && state.isEditing) ? "Updated" : "Saved";
|
||||
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
handleSaveError(resource.message);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
appointmentViewModel.updateAppointment(appointmentId, dto).observe(getViewLifecycleOwner(), observer);
|
||||
} else {
|
||||
appointmentViewModel.createAppointment(dto).observe(getViewLifecycleOwner(), observer);
|
||||
}
|
||||
/**
|
||||
* 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");
|
||||
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()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors that occur during the saving process.
|
||||
*/
|
||||
private void handleSaveError(String errorMessage) {
|
||||
if (errorMessage != null) {
|
||||
Log.e("APPT_SAVE", "Error: " + errorMessage);
|
||||
if (errorMessage.toLowerCase().contains("future")) {
|
||||
DialogUtils.showInfoDialog(requireContext(), "Invalid Date/Time",
|
||||
"Booked appointments must be scheduled in the future.");
|
||||
} else if (errorMessage.toLowerCase().contains("not available")) {
|
||||
showNoAvailabilityDialog();
|
||||
} else {
|
||||
Toast.makeText(getContext(), errorMessage, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Something went wrong", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
if (errorMessage != null && errorMessage.toLowerCase().contains("not available")) showNoAvailabilityDialog();
|
||||
else Toast.makeText(getContext(), errorMessage != null ? errorMessage : "Error saving", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -559,11 +330,9 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
private void showNoAvailabilityDialog() {
|
||||
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
|
||||
.setTitle("No Availability")
|
||||
.setMessage("This time slot is already booked. Please choose a different time or date.")
|
||||
.setMessage("This time slot is already booked.")
|
||||
.setPositiveButton("Change Time", (d, w) -> d.dismiss())
|
||||
.setNegativeButton("Cancel Booking", (d, w) -> navigateBack())
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
.setNegativeButton("Cancel Booking", (d, w) -> navigateBack()).show();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,13 +340,8 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
*/
|
||||
private void confirmDelete() {
|
||||
DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () ->
|
||||
appointmentViewModel.deleteAppointment(appointmentId).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
appointmentViewModel.deleteAppointment().observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) navigateBack();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -587,4 +351,24 @@ public class AppointmentDetailFragment extends Fragment {
|
||||
private void navigateBack() {
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a time string and sets the hour and minute spinners.
|
||||
*/
|
||||
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<Integer> callback;
|
||||
public OnIndexSelected(java.util.function.Consumer<Integer> 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) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.example.petstoremobile.utils;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
/**
|
||||
* Utility class for date and time operations.
|
||||
*/
|
||||
public class DateTimeUtils {
|
||||
|
||||
private static final String TAG = "DateTimeUtils";
|
||||
|
||||
/**
|
||||
* Formats status from backend format to UI format
|
||||
* (backend is using all caps so we lower case them with this function)
|
||||
*/
|
||||
public static String formatStatusFromBackend(String status) {
|
||||
if (status == null || status.isEmpty()) return status;
|
||||
return status.substring(0, 1).toUpperCase() + status.substring(1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a date and time string to a Calendar object.
|
||||
* format: date = "YYYY-MM-DD", time = "HH:MM"
|
||||
*/
|
||||
public static Calendar parseDateTimeToCalendar(String date, String time) throws Exception {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
String[] dateParts = date.split("-");
|
||||
String[] timeParts = time.split(":");
|
||||
|
||||
calendar.set(
|
||||
Integer.parseInt(dateParts[0]),
|
||||
Integer.parseInt(dateParts[1]) - 1,
|
||||
Integer.parseInt(dateParts[2]),
|
||||
Integer.parseInt(timeParts[0]),
|
||||
Integer.parseInt(timeParts[1]),
|
||||
0
|
||||
);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given date is in the past.
|
||||
* format: date = "YYYY-MM-DD"
|
||||
*/
|
||||
public static boolean isDateInPast(String date) {
|
||||
if (date == null || date.isEmpty()) return false;
|
||||
try {
|
||||
Calendar selected = Calendar.getInstance();
|
||||
String[] dateParts = date.split("-");
|
||||
selected.set(
|
||||
Integer.parseInt(dateParts[0]),
|
||||
Integer.parseInt(dateParts[1]) - 1,
|
||||
Integer.parseInt(dateParts[2]),
|
||||
0, 0, 0
|
||||
);
|
||||
return selected.before(Calendar.getInstance());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error parsing date: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given date and time are in the past.
|
||||
* format: date = "YYYY-MM-DD", time = "HH:MM"
|
||||
*/
|
||||
public static boolean isDateTimeInPast(String date, String time) {
|
||||
if (date == null || date.isEmpty() || time == null || time.isEmpty()) return false;
|
||||
try {
|
||||
Calendar selected = parseDateTimeToCalendar(date, time);
|
||||
return selected.before(Calendar.getInstance());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error parsing date/time: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a time string and returns hour and minute indices for the spinners.
|
||||
*/
|
||||
public static int[] parseTimeString(String time) {
|
||||
if (time == null || time.isEmpty()) return null;
|
||||
if (time.length() > 5) time = time.substring(0, 5);
|
||||
|
||||
String[] parts = time.split(":");
|
||||
if (parts.length != 2) return null;
|
||||
|
||||
try {
|
||||
int hour = Integer.parseInt(parts[0]);
|
||||
int min = Integer.parseInt(parts[1]);
|
||||
return new int[]{hour, min};
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -18,12 +27,42 @@ 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<List<DropdownDTO>> customers = new MutableLiveData<>();
|
||||
private final MutableLiveData<List<DropdownDTO>> stores = new MutableLiveData<>();
|
||||
private final MutableLiveData<List<ServiceDTO>> services = new MutableLiveData<>();
|
||||
private final MutableLiveData<List<DropdownDTO>> customerPets = new MutableLiveData<>();
|
||||
private final MutableLiveData<List<DropdownDTO>> storeEmployees = new MutableLiveData<>();
|
||||
|
||||
private final MutableLiveData<ViewState> 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) {
|
||||
public AppointmentViewModel(
|
||||
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;
|
||||
}
|
||||
|
||||
// API CRUD
|
||||
|
||||
/**
|
||||
* Fetches a paginated list of all appointments with optional filters.
|
||||
*/
|
||||
@@ -65,4 +104,273 @@ public class AppointmentViewModel extends ViewModel {
|
||||
public LiveData<Resource<Void>> bulkDeleteAppointments(List<String> 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<List<DropdownDTO>> getCustomers() { return customers; }
|
||||
public LiveData<List<DropdownDTO>> getStores() { return stores; }
|
||||
public LiveData<List<ServiceDTO>> getServices() { return services; }
|
||||
public LiveData<List<DropdownDTO>> getCustomerPets() { return customerPets; }
|
||||
public LiveData<List<DropdownDTO>> getStoreEmployees() { return storeEmployees; }
|
||||
public LiveData<ViewState> 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<DropdownDTO> 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<DropdownDTO> 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<ServiceDTO> 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<DropdownDTO> 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<DropdownDTO> 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<Resource<AppointmentDTO>> loadAppointment() {
|
||||
MutableLiveData<Resource<AppointmentDTO>> 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<Resource<AppointmentDTO>> 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<Resource<Void>> 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<ViewState> action) {
|
||||
ViewState current = viewState.getValue();
|
||||
if (current != null) {
|
||||
action.run(current);
|
||||
viewState.setValue(current);
|
||||
}
|
||||
}
|
||||
|
||||
private interface Action<T> { 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user