Moved appointments businiss logic to modelview andriod
This commit is contained in:
@@ -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) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user