Moved appointments businiss logic to modelview andriod

This commit is contained in:
Alex
2026-04-09 00:55:00 -06:00
parent 6ebec31f09
commit f98abf19ef
3 changed files with 584 additions and 394 deletions

View File

@@ -13,16 +13,12 @@ import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.databinding.FragmentAppointmentDetailBinding; import com.example.petstoremobile.databinding.FragmentAppointmentDetailBinding;
import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AppointmentViewModel; 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.*; import java.util.*;
@@ -36,40 +32,22 @@ public class AppointmentDetailFragment extends Fragment {
private FragmentAppointmentDetailBinding binding; private FragmentAppointmentDetailBinding binding;
private long appointmentId = -1;
private boolean isEditing = false;
private boolean isPastAppointment = false;
private long preselectedPetId = -1; private long preselectedPetId = -1;
private long preselectedServiceId = -1; private long preselectedServiceId = -1;
private long preselectedCustomerId = -1; private long preselectedCustomerId = -1;
private long preselectedStoreId = -1; private long preselectedStoreId = -1;
private long preselectedStaffId = -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[] HOURS = {9,10,11,12,13,14,15,16,17};
private final Integer[] MINUTES = {0,15,30,45}; private final Integer[] MINUTES = {0,15,30,45};
private AppointmentViewModel appointmentViewModel; private AppointmentViewModel appointmentViewModel;
private PetViewModel petViewModel; private boolean isUpdatingUI = false;
private ServiceViewModel serviceViewModel;
private StoreViewModel storeViewModel;
private CustomerViewModel customerViewModel;
private UserViewModel userViewModel;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); 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 @Override
@@ -83,7 +61,8 @@ public class AppointmentDetailFragment extends Fragment {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
setupSpinners(); setupSpinners();
setupDatePicker(); setupDatePicker();
loadSpinnersData(); observeViewModel();
appointmentViewModel.loadInitialFormData();
handleArguments(); handleArguments();
binding.btnApptBack.setOnClickListener(v -> navigateBack()); binding.btnApptBack.setOnClickListener(v -> navigateBack());
@@ -101,9 +80,10 @@ public class AppointmentDetailFragment extends Fragment {
* Configures the adapters for spinners. * Configures the adapters for spinners.
*/ */
private void setupSpinners() { private void setupSpinners() {
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, //Status Spinner is empty by default the date determines whats in here
new String[]{"Booked", "Completed", "Cancelled", "Missed"}); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, new String[]{});
// Set up hour and minute spinners
String[] hours = new String[HOURS.length]; String[] hours = new String[HOURS.length];
for (int i = 0; i < HOURS.length; i++) for (int i = 0; i < HOURS.length; i++)
hours[i] = String.format("%02d:00", HOURS[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 // Pet and Staff spinners disabled by until parent selection
UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff); 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() { binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position > 0 && position <= customerList.size()) { appointmentViewModel.onCustomerSelected(position);
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);
}
}
} }
@Override @Override
public void onNothingSelected(AdapterView<?> parent) {} 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() { binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position > 0 && position <= storeList.size()) { appointmentViewModel.onStoreSelected(position);
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);
}
}
} }
@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 @Override
public void onNothingSelected(AdapterView<?> parent) {} public void onNothingSelected(AdapterView<?> parent) {}
}); });
@@ -167,11 +138,13 @@ public class AppointmentDetailFragment extends Fragment {
*/ */
private void setupDatePicker() { private void setupDatePicker() {
binding.etAppointmentDate.setOnClickListener(v -> { binding.etAppointmentDate.setOnClickListener(v -> {
if (isPastAppointment) return;
Calendar c = Calendar.getInstance(); Calendar c = Calendar.getInstance();
DatePickerDialog d = new DatePickerDialog(requireContext(), DatePickerDialog d = new DatePickerDialog(requireContext(),
(dp,y,m,d1) -> binding.etAppointmentDate.setText( (dp,y,m,d1) -> {
String.format("%04d-%02d-%02d", y, m+1, 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.YEAR), c.get(Calendar.MONTH),
c.get(Calendar.DAY_OF_MONTH)); c.get(Calendar.DAY_OF_MONTH));
d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000); 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() { private void observeViewModel() {
loadServices(); appointmentViewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState);
loadCustomers();
loadStores(); // 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) { private void applyViewState(AppointmentViewModel.ViewState state) {
petViewModel.getCustomerPets(customerId).observe(getViewLifecycleOwner(), resource -> { isUpdatingUI = true;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
petList = resource.data; // Mode specific UI
refreshPetSpinner(); 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;
} }
/** private void notifyDateTimeStatusChange() {
* Populates the pet selection spinner. if (isUpdatingUI) return;
*/
private void refreshPetSpinner() {
SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList,
DropdownDTO::getLabel, "-- Select Pet --",
preselectedPetId, DropdownDTO::getId);
}
/** String date = binding.etAppointmentDate.getText().toString();
* Loads the list of services from the API. String time = buildTimeString();
*/ Object selected = binding.spinnerAppointmentStatus.getSelectedItem();
private void loadServices() { String status = selected != null ? selected.toString() : "";
serviceViewModel.getAllServices(0, 200, null, "serviceName").observe(getViewLifecycleOwner(), resource -> { appointmentViewModel.onDateOrTimeChanged(date, time, status);
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);
} }
/** /**
@@ -300,27 +234,10 @@ public class AppointmentDetailFragment extends Fragment {
private void handleArguments() { private void handleArguments() {
Bundle a = getArguments(); Bundle a = getArguments();
if (a != null && a.containsKey("appointmentId")) { if (a != null && a.containsKey("appointmentId")) {
//edit mode appointmentViewModel.setAppointmentId(a.getLong("appointmentId"));
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);
loadAppointmentData(); loadAppointmentData();
} else { } else {
//add mode appointmentViewModel.setAppointmentId(-1);
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);
} }
} }
@@ -328,229 +245,83 @@ public class AppointmentDetailFragment extends Fragment {
* Fetches specific appointment details from the backend using the ID. * Fetches specific appointment details from the backend using the ID.
*/ */
private void loadAppointmentData() { private void loadAppointmentData() {
appointmentViewModel.getAppointmentById(appointmentId).observe(getViewLifecycleOwner(), resource -> { appointmentViewModel.loadAppointment().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return; if (resource == null || resource.status != Resource.Status.SUCCESS || resource.data == null) return;
if (resource.status == Resource.Status.SUCCESS && resource.data != null) { AppointmentDTO a = resource.data;
AppointmentDTO a = resource.data; preselectedPetId = a.getPetId() != null ? a.getPetId() : -1;
preselectedPetId = (a.getPetId() != null) ? a.getPetId() : -1; preselectedServiceId = a.getServiceId() != null ? a.getServiceId() : -1;
preselectedServiceId = (a.getServiceId() != null) ? a.getServiceId() : -1; preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1;
preselectedCustomerId = (a.getCustomerId() != null) ? a.getCustomerId() : -1; preselectedStoreId = a.getStoreId() != null ? a.getStoreId() : -1;
preselectedStoreId = (a.getStoreId() != null) ? a.getStoreId() : -1; preselectedStaffId = a.getEmployeeId() != null ? a.getEmployeeId() : -1;
preselectedStaffId = (a.getEmployeeId() != null) ? a.getEmployeeId() : -1;
binding.etAppointmentDate.setText(a.getAppointmentDate()); binding.etAppointmentDate.setText(a.getAppointmentDate());
parseAndSetTimeSpinners(a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00");
// Pre-fill time spinners
String time = a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00"; String status = a.getAppointmentStatus();
if (time.length() > 5) time = time.substring(0, 5); if (status != null && !status.isEmpty()) {
String[] parts = time.split(":"); SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, DateTimeUtils.formatStatusFromBackend(status));
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();
} }
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. * Validates input and saves the appointment to the backend.
*/ */
private void saveAppointment() { private void saveAppointment() {
if (binding.spinnerCustomer.getSelectedItemPosition() == 0) { if (!validateRequiredFields()) return;
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;
}
String date = binding.etAppointmentDate.getText().toString().trim(); String date = binding.etAppointmentDate.getText().toString().trim();
if (date.isEmpty()) { String time = buildTimeString();
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 status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase(); String status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase();
if (!appointmentViewModel.isValidFutureBooking(status, date, time)) {
// Validate future date+time if status is BOOKED DialogUtils.showInfoDialog(requireContext(), "Invalid Time", "Booked appointments must be in the future.");
if ("BOOKED".equalsIgnoreCase(status)) { return;
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());
}
} }
// Build DTO with all required IDs appointmentViewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> {
AppointmentDTO dto = new AppointmentDTO(
customer.getId(),
store.getId(),
service.getServiceId(),
employeeId,
date,
time,
status,
pet.getId()
);
androidx.lifecycle.Observer<Resource<AppointmentDTO>> observer = resource -> {
if (resource.status == Resource.Status.SUCCESS) { 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(); navigateBack();
} else if (resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
handleSaveError(resource.message); handleSaveError(resource.message);
} }
}; });
}
if (isEditing) { /**
appointmentViewModel.updateAppointment(appointmentId, dto).observe(getViewLifecycleOwner(), observer); * Validates that all required fields are selected.
} else { */
appointmentViewModel.createAppointment(dto).observe(getViewLifecycleOwner(), observer); 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. * Handles errors that occur during the saving process.
*/ */
private void handleSaveError(String errorMessage) { private void handleSaveError(String errorMessage) {
if (errorMessage != null) { if (errorMessage != null && errorMessage.toLowerCase().contains("not available")) showNoAvailabilityDialog();
Log.e("APPT_SAVE", "Error: " + errorMessage); else Toast.makeText(getContext(), errorMessage != null ? errorMessage : "Error saving", Toast.LENGTH_SHORT).show();
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();
}
} }
/** /**
@@ -559,11 +330,9 @@ public class AppointmentDetailFragment extends Fragment {
private void showNoAvailabilityDialog() { private void showNoAvailabilityDialog() {
new androidx.appcompat.app.AlertDialog.Builder(requireContext()) new androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle("No Availability") .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()) .setPositiveButton("Change Time", (d, w) -> d.dismiss())
.setNegativeButton("Cancel Booking", (d, w) -> navigateBack()) .setNegativeButton("Cancel Booking", (d, w) -> navigateBack()).show();
.setCancelable(false)
.show();
} }
/** /**
@@ -571,13 +340,8 @@ public class AppointmentDetailFragment extends Fragment {
*/ */
private void confirmDelete() { private void confirmDelete() {
DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () -> DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () ->
appointmentViewModel.deleteAppointment(appointmentId).observe(getViewLifecycleOwner(), resource -> { appointmentViewModel.deleteAppointment().observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) navigateBack();
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();
}
})); }));
} }
@@ -587,4 +351,24 @@ public class AppointmentDetailFragment extends Fragment {
private void navigateBack() { private void navigateBack() {
NavHostFragment.findNavController(this).popBackStack(); 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) {}
}
} }

View File

@@ -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;
}
}
}

View File

@@ -1,14 +1,23 @@
package com.example.petstoremobile.viewmodels; package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.repositories.AppointmentRepository; 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.utils.Resource;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
@@ -18,12 +27,42 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel @HiltViewModel
public class AppointmentViewModel extends ViewModel { public class AppointmentViewModel extends ViewModel {
private final AppointmentRepository repository; 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 @Inject
public AppointmentViewModel(AppointmentRepository repository) { public AppointmentViewModel(
AppointmentRepository repository,
CustomerRepository customerRepository,
StoreRepository storeRepository,
PetRepository petRepository,
ServiceRepository serviceRepository) {
this.repository = repository; 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. * 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) { public LiveData<Resource<Void>> bulkDeleteAppointments(List<String> ids) {
return repository.bulkDeleteAppointments(new BulkDeleteRequest(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;
}
}