Attachments to Chat #162

Merged
RecentRunner merged 20 commits from AttachmentsToChat into main 2026-04-09 22:34:59 -06:00
6 changed files with 566 additions and 425 deletions
Showing only changes of commit 992da24260 - Show all commits

View File

@@ -1,10 +1,12 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.app.DatePickerDialog;
import android.os.Bundle;
import android.util.Log;
import android.view.*;
import android.widget.*;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
@@ -12,15 +14,15 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import com.example.petstoremobile.databinding.FragmentAppointmentDetailBinding;
import com.example.petstoremobile.dtos.*;
import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.AppointmentViewModel;
import java.util.*;
import com.example.petstoremobile.viewmodels.AppointmentDetailViewModel;
import dagger.hilt.android.AndroidEntryPoint;
@@ -38,24 +40,33 @@ public class AppointmentDetailFragment extends Fragment {
private long preselectedStoreId = -1;
private long preselectedStaffId = -1;
private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17};
private final Integer[] MINUTES = {0,15,30,45};
private final Integer[] HOURS = {9, 10, 11, 12, 13, 14, 15, 16, 17};
private final Integer[] MINUTES = {0, 15, 30, 45};
private AppointmentViewModel appointmentViewModel;
private AppointmentDetailViewModel appointmentViewModel;
private boolean isUpdatingUI = false;
/**
* Called when the fragment is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class);
appointmentViewModel = new ViewModelProvider(this).get(AppointmentDetailViewModel.class);
}
/**
* Creates and returns the view hierarchy with the fragment.
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentAppointmentDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
/**
* Called immediately after onCreateView has returned.
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
@@ -70,6 +81,9 @@ public class AppointmentDetailFragment extends Fragment {
binding.btnDeleteAppointment.setOnClickListener(v -> confirmDelete());
}
/**
* Called when the view previously created by onCreateView has been detached.
*/
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -77,7 +91,7 @@ public class AppointmentDetailFragment extends Fragment {
}
/**
* Configures the adapters for spinners.
* Configures the adapters and listeners for all spinners.
*/
private void setupSpinners() {
//Status Spinner is empty by default the date determines whats in here
@@ -86,9 +100,9 @@ public class AppointmentDetailFragment extends Fragment {
// Set up hour and minute spinners
String[] hours = new String[HOURS.length];
for (int i = 0; i < HOURS.length; i++)
hours[i] = String.format("%02d:00", HOURS[i]);
hours[i] = DateTimeUtils.formatTime(HOURS[i], 0);
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours);
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00","15","30","45"});
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00", "15", "30", "45"});
// Pet and Staff spinners disabled by until parent selection
UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff);
@@ -114,42 +128,24 @@ public class AppointmentDetailFragment extends Fragment {
});
// Listeners for other selections
binding.spinnerService.setOnItemSelectedListener(new OnIndexSelected(p -> appointmentViewModel.onServiceSelected(p)));
binding.spinnerPet.setOnItemSelectedListener(new OnIndexSelected(p -> appointmentViewModel.onPetSelected(p)));
binding.spinnerStaff.setOnItemSelectedListener(new OnIndexSelected(p -> appointmentViewModel.onStaffSelected(p)));
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerService, p -> appointmentViewModel.onServiceSelected(p));
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPet, p -> appointmentViewModel.onPetSelected(p));
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerStaff, p -> appointmentViewModel.onStaffSelected(p));
// Listeners for time changes
binding.spinnerHour.setOnItemSelectedListener(new OnIndexSelected(p -> notifyDateTimeStatusChange()));
binding.spinnerMinute.setOnItemSelectedListener(new OnIndexSelected(p -> notifyDateTimeStatusChange()));
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerHour, p -> notifyDateTimeStatusChange());
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerMinute, p -> notifyDateTimeStatusChange());
// Listener to notify ViewModel of status selection
binding.spinnerAppointmentStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
notifyDateTimeStatusChange();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAppointmentStatus, p -> notifyDateTimeStatusChange());
}
/**
* Configures the date picker dialog for the appointment date field.
*/
private void setupDatePicker() {
binding.etAppointmentDate.setOnClickListener(v -> {
Calendar c = Calendar.getInstance();
DatePickerDialog d = new DatePickerDialog(requireContext(),
(dp,y,m,d1) -> {
String selectedDate = String.format("%04d-%02d-%02d", y, m+1, d1);
binding.etAppointmentDate.setText(selectedDate);
notifyDateTimeStatusChange();
},
c.get(Calendar.YEAR), c.get(Calendar.MONTH),
c.get(Calendar.DAY_OF_MONTH));
d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000);
d.show();
});
binding.etAppointmentDate.setOnClickListener(v ->
UIUtils.showDatePicker(requireContext(), binding.etAppointmentDate, this::notifyDateTimeStatusChange));
}
/**
@@ -178,37 +174,27 @@ public class AppointmentDetailFragment extends Fragment {
/**
* Applies the ViewState provided by the ViewModel to the UI components.
*/
private void applyViewState(AppointmentViewModel.ViewState state) {
private void applyViewState(AppointmentDetailViewModel.ViewState state) {
isUpdatingUI = true;
// Mode specific UI
binding.tvApptMode.setText(state.isEditing ? "Edit Appointment" : "Add Appointment");
binding.tvAppointmentId.setText("ID: " + appointmentViewModel.getAppointmentId());
binding.tvAppointmentId.setText(DateTimeUtils.formatId(appointmentViewModel.getAppointmentId()));
binding.tvAppointmentId.setVisibility(state.isEditing ? View.VISIBLE : View.GONE);
binding.btnDeleteAppointment.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE);
binding.btnSaveAppointment.setVisibility(state.isSaveVisible ? View.VISIBLE : View.GONE);
// Enabling/Disabling Views
UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer);
UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore);
UIUtils.setViewsEnabled(state.isPetEnabled, binding.spinnerPet);
UIUtils.setViewsEnabled(state.isServiceEnabled, binding.spinnerService);
UIUtils.setViewsEnabled(state.isStaffEnabled, binding.spinnerStaff);
UIUtils.setViewsEnabled(state.isDateEnabled, binding.etAppointmentDate);
UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerHour, binding.spinnerMinute);
// Enabling/Disabling Views and Labels
UIUtils.setFieldEnabled(state.isCustomerEnabled, binding.spinnerCustomer, binding.tvLabelCustomer);
UIUtils.setFieldEnabled(state.isStoreEnabled, binding.spinnerStore, binding.tvLabelStore);
UIUtils.setFieldEnabled(state.isPetEnabled, binding.spinnerPet, binding.tvLabelPet);
UIUtils.setFieldEnabled(state.isServiceEnabled, binding.spinnerService, binding.tvLabelService);
UIUtils.setFieldEnabled(state.isStaffEnabled, binding.spinnerStaff, binding.tvLabelStaff);
UIUtils.setFieldEnabled(state.isDateEnabled, binding.etAppointmentDate, binding.tvLabelDate);
UIUtils.setFieldEnabled(state.isTimeEnabled, binding.spinnerHour, binding.tvLabelTime);
UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerMinute);
UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAppointmentStatus);
// Alpha for disabled look
float alpha = 1.0f;
float disabledAlpha = 0.5f;
UIUtils.setViewsAlpha(state.isCustomerEnabled ? alpha : disabledAlpha, binding.tvLabelCustomer);
UIUtils.setViewsAlpha(state.isStoreEnabled ? alpha : disabledAlpha, binding.tvLabelStore);
UIUtils.setViewsAlpha(state.isPetEnabled ? alpha : disabledAlpha, binding.tvLabelPet);
UIUtils.setViewsAlpha(state.isServiceEnabled ? alpha : disabledAlpha, binding.tvLabelService);
UIUtils.setViewsAlpha(state.isStaffEnabled ? alpha : disabledAlpha, binding.tvLabelStaff);
UIUtils.setViewsAlpha(state.isDateEnabled ? alpha : disabledAlpha, binding.tvLabelDate);
UIUtils.setViewsAlpha(state.isTimeEnabled ? alpha : disabledAlpha, binding.tvLabelTime);
// Update status options
Object selected = binding.spinnerAppointmentStatus.getSelectedItem();
String current = selected != null ? selected.toString() : "";
@@ -218,6 +204,9 @@ public class AppointmentDetailFragment extends Fragment {
isUpdatingUI = false;
}
/**
* Notifies the ViewModel that the date, time, or status has changed.
*/
private void notifyDateTimeStatusChange() {
if (isUpdatingUI) return;
@@ -282,7 +271,7 @@ public class AppointmentDetailFragment extends Fragment {
appointmentViewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
AppointmentViewModel.ViewState state = appointmentViewModel.getViewState().getValue();
AppointmentDetailViewModel.ViewState state = appointmentViewModel.getViewState().getValue();
String message = (state != null && state.isEditing) ? "Updated" : "Saved";
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
navigateBack();
@@ -296,24 +285,19 @@ public class AppointmentDetailFragment extends Fragment {
* Validates that all required fields are selected.
*/
private boolean validateRequiredFields() {
if (binding.spinnerCustomer.getSelectedItemPosition() == 0) return showToast("Select a customer");
if (binding.spinnerStore.getSelectedItemPosition() == 0) return showToast("Select a store");
if (binding.spinnerPet.getSelectedItemPosition() == 0) return showToast("Select a pet");
if (binding.spinnerService.getSelectedItemPosition() == 0) return showToast("Select a service");
if (binding.etAppointmentDate.getText().toString().trim().isEmpty()) return showToast("Select a date");
if (binding.spinnerCustomer.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a customer");
if (binding.spinnerStore.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a store");
if (binding.spinnerPet.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a pet");
if (binding.spinnerService.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a service");
if (binding.etAppointmentDate.getText().toString().trim().isEmpty()) return UIUtils.showToast(getContext(), "Select a date");
return true;
}
private boolean showToast(String msg) {
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
return false;
}
/**
* Builds a time string from the hour and minute spinners.
*/
private String buildTimeString() {
return String.format("%02d:%02d", HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]);
return DateTimeUtils.formatTime(HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]);
}
/**
@@ -358,17 +342,7 @@ public class AppointmentDetailFragment extends Fragment {
private void parseAndSetTimeSpinners(String time) {
int[] parsedTime = DateTimeUtils.parseTimeString(time);
if (parsedTime == null) return;
for (int i = 0; i < HOURS.length; i++) if (HOURS[i] == parsedTime[0]) binding.spinnerHour.setSelection(i);
for (int i = 0; i < MINUTES.length; i++) if (MINUTES[i] == parsedTime[1]) binding.spinnerMinute.setSelection(i);
}
/**
* Helper listener for simple index reporting.
*/
private static class OnIndexSelected implements AdapterView.OnItemSelectedListener {
private final java.util.function.Consumer<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) {}
SpinnerUtils.setSelectionByValueArray(binding.spinnerHour, HOURS, parsedTime[0]);
SpinnerUtils.setSelectionByValueArray(binding.spinnerMinute, MINUTES, parsedTime[1]);
}
}

View File

@@ -3,6 +3,7 @@ package com.example.petstoremobile.utils;
import android.util.Log;
import java.util.Calendar;
import java.util.Locale;
/**
* Utility class for date and time operations.
@@ -78,7 +79,7 @@ public class DateTimeUtils {
}
/**
* Parses a time string and returns hour and minute indices for the spinners.
* Parses a time string and returns hour and minute values.
*/
public static int[] parseTimeString(String time) {
if (time == null || time.isEmpty()) return null;
@@ -95,4 +96,18 @@ public class DateTimeUtils {
return null;
}
}
/**
* Formats an hour and minute into an HH:mm string.
*/
public static String formatTime(int hour, int minute) {
return String.format(Locale.getDefault(), "%02d:%02d", hour, minute);
}
/**
* Formats an ID for display (e.g., "ID: 123").
*/
public static String formatId(long id) {
return String.format(Locale.getDefault(), "ID: %d", id);
}
}

View File

@@ -12,6 +12,7 @@ import com.example.petstoremobile.adapters.WhiteTextArrayAdapter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
/**
@@ -96,6 +97,13 @@ public class SpinnerUtils {
});
}
/**
* Sets a listener that provides the selected index to a consumer.
*/
public static void setOnIndexSelectedListener(Spinner spinner, Consumer<Integer> callback) {
spinner.setOnItemSelectedListener(new OnIndexSelected(callback));
}
/**
* Sets the selection of a spinner based on a string value.
*/
@@ -108,6 +116,19 @@ public class SpinnerUtils {
}
}
/**
* Sets the selection of a spinner based on a value within an array.
*/
public static <T> void setSelectionByValueArray(Spinner spinner, T[] array, T value) {
if (spinner == null || array == null || value == null) return;
for (int i = 0; i < array.length; i++) {
if (Objects.equals(array[i], value)) {
spinner.setSelection(i);
return;
}
}
}
/**
* Configures a simple string array spinner.
*/
@@ -117,4 +138,23 @@ public class SpinnerUtils {
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
}
/**
* Helper listener to get selected index from a spinner.
*/
public static class OnIndexSelected implements AdapterView.OnItemSelectedListener {
private final Consumer<Integer> callback;
public OnIndexSelected(Consumer<Integer> callback) {
this.callback = callback;
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
callback.accept(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
}
}

View File

@@ -1,5 +1,7 @@
package com.example.petstoremobile.utils;
import android.app.DatePickerDialog;
import android.content.Context;
import android.telephony.PhoneNumberFormattingTextWatcher;
import android.text.Editable;
import android.text.InputFilter;
@@ -8,10 +10,14 @@ import android.view.View;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.Spinner;
import android.widget.Toast;
import androidx.fragment.app.Fragment;
import com.example.petstoremobile.fragments.ListFragment;
import java.util.Calendar;
import java.util.Locale;
/**
* Utility class for shared UI component logic and formatting.
*/
@@ -32,7 +38,6 @@ public class UIUtils {
boolean isVisible = layoutFilter.getVisibility() == View.VISIBLE;
layoutFilter.setVisibility(isVisible ? View.GONE : View.VISIBLE);
// Use Android default icons or app-specific ones if available
btnToggle.setImageResource(isVisible ?
android.R.drawable.ic_menu_search :
android.R.drawable.ic_menu_close_clear_cancel);
@@ -69,7 +74,6 @@ public class UIUtils {
/**
* Sets the enabled state and alpha for multiple views.
* Alpha is set to 1.0f when enabled and 0.5f when disabled.
*/
public static void setViewsEnabled(boolean enabled, View... views) {
for (View v : views) {
@@ -80,6 +84,19 @@ public class UIUtils {
}
}
/**
* Sets enabled state for a field and updates alpha for both the field and its label.
*/
public static void setFieldEnabled(boolean enabled, View field, View label) {
if (field != null) {
field.setEnabled(enabled);
field.setAlpha(enabled ? 1.0f : 0.5f);
}
if (label != null) {
label.setAlpha(enabled ? 1.0f : 0.5f);
}
}
/**
* Sets the alpha for multiple views.
*/
@@ -90,4 +107,31 @@ public class UIUtils {
}
}
}
/**
* Displays a DatePickerDialog and sets the result to an EditText.
*/
public static void showDatePicker(Context context, EditText editText, Runnable onDateSet) {
Calendar c = Calendar.getInstance();
DatePickerDialog d = new DatePickerDialog(context,
(dp, y, m, d1) -> {
String selectedDate = String.format(Locale.getDefault(), "%04d-%02d-%02d", y, m + 1, d1);
editText.setText(selectedDate);
if (onDateSet != null) onDateSet.run();
},
c.get(Calendar.YEAR), c.get(Calendar.MONTH),
c.get(Calendar.DAY_OF_MONTH));
d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000);
d.show();
}
/**
* Displays a toast and returns false. Useful for validation chains.
*/
public static boolean showToast(Context context, String msg) {
if (context != null) {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
return false;
}
}

View File

@@ -0,0 +1,402 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.repositories.AppointmentRepository;
import com.example.petstoremobile.repositories.CustomerRepository;
import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.repositories.ServiceRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.lifecycle.HiltViewModel;
/**
* ViewModel for managing appointment details and form state.
*/
@HiltViewModel
public class AppointmentDetailViewModel extends ViewModel {
private final AppointmentRepository repository;
private final CustomerRepository customerRepository;
private final StoreRepository storeRepository;
private final PetRepository petRepository;
private final ServiceRepository serviceRepository;
private final MutableLiveData<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;
/**
* Constructor for AppointmentDetailViewModel.
*/
@Inject
public AppointmentDetailViewModel(
AppointmentRepository repository,
CustomerRepository customerRepository,
StoreRepository storeRepository,
PetRepository petRepository,
ServiceRepository serviceRepository) {
this.repository = repository;
this.customerRepository = customerRepository;
this.storeRepository = storeRepository;
this.petRepository = petRepository;
this.serviceRepository = serviceRepository;
}
// Initial Data Loading
/**
* Loads initial dropdown data for customers, stores, and services.
*/
public void loadInitialFormData() {
customerRepository.getCustomerDropdowns().observeForever(r -> {
if (r.status == Resource.Status.SUCCESS) customers.setValue(r.data);
});
storeRepository.getStoreDropdowns().observeForever(r -> {
if (r.status == Resource.Status.SUCCESS) stores.setValue(r.data);
});
serviceRepository.getAllServices(0, 200, null, "serviceName").observeForever(r -> {
if (r.status == Resource.Status.SUCCESS && r.data != null) services.setValue(r.data.getContent());
});
}
// LiveData Getters
/**
* Returns the LiveData for the list of customers.
*/
public LiveData<List<DropdownDTO>> getCustomers() { return customers; }
/**
* Returns the LiveData for the list of stores.
*/
public LiveData<List<DropdownDTO>> getStores() { return stores; }
/**
* Returns the LiveData for the list of services.
*/
public LiveData<List<ServiceDTO>> getServices() { return services; }
/**
* Returns the LiveData for the list of pets for the current customer.
*/
public LiveData<List<DropdownDTO>> getCustomerPets() { return customerPets; }
/**
* Returns the LiveData for the list of employees for the current store.
*/
public LiveData<List<DropdownDTO>> getStoreEmployees() { return storeEmployees; }
/**
* Returns the LiveData for the view state.
*/
public LiveData<ViewState> getViewState() { return viewState; }
//State Getters
/**
* Returns the current appointment ID.
*/
public long getAppointmentId() { return appointmentId; }
/**
* Sets the current appointment ID and updates the mode.
*/
public void setAppointmentId(long id) {
this.appointmentId = id;
initMode(id != -1);
}
// Selection Handlers for spinners
/**
* Handles customer selection and loads their pets.
*/
public void onCustomerSelected(int position) {
List<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;
});
}
}
/**
* Handles store selection and loads its employees.
*/
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;
});
}
}
/**
* Handles service selection.
*/
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);
}
/**
* Handles pet selection.
*/
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);
}
/**
* Handles staff selection.
*/
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);
}
/**
* Loads the list of pets for a specific customer.
*/
private void loadPetsForCustomer(Long customerId) {
petRepository.getCustomerPets(customerId).observeForever(r -> {
if (r.status == Resource.Status.SUCCESS) customerPets.setValue(r.data);
});
}
/**
* Loads the list of employees for a specific store.
*/
private void loadEmployeesForStore(Long storeId) {
storeRepository.getStoreEmployees(storeId).observeForever(r -> {
if (r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data);
});
}
// Appointment Detail CRUD
/**
* Fetches appointment details and populates internal state.
*/
public LiveData<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
/**
* Updates the UI state when date, time, or status changes.
*/
public void onDateOrTimeChanged(String date, String time, String currentStatus) {
updateViewState(s -> {
s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus);
boolean isPast = DateTimeUtils.isDateTimeInPast(date, time);
boolean isCancelled = "Cancelled".equalsIgnoreCase(currentStatus);
if (isCancelled) {
s.isPast = true;
setAllFieldsEnabled(s, false);
s.isStatusEnabled = false;
s.isSaveVisible = false;
} else if (isPast) {
s.isPast = true;
setAllFieldsEnabled(s, false);
s.isStatusEnabled = true;
} else {
s.isPast = false;
if (!s.isEditing) {
s.isCustomerEnabled = true;
s.isStoreEnabled = true;
s.isServiceEnabled = true;
s.isPetEnabled = currentCustomerId != null;
}
s.isDateEnabled = true;
s.isTimeEnabled = true;
s.isStatusEnabled = true;
}
});
}
/**
* Calculates available appointment statuses based on the current context.
*/
private String[] calculateAvailableStatuses(boolean isEditing, String date, String currentTime, String currentStatus) {
if (!isEditing) return new String[]{"Booked"};
if (date == null || date.isEmpty()) return new String[]{};
if ("Cancelled".equalsIgnoreCase(currentStatus)) return new String[]{"Cancelled"};
if (DateTimeUtils.isDateTimeInPast(date, currentTime)) return new String[]{"Completed", "Missed"};
return new String[]{"Booked", "Cancelled"};
}
/**
* Helper method to enable or disable all fields.
*/
private void setAllFieldsEnabled(ViewState s, boolean enabled) {
s.isCustomerEnabled = enabled;
s.isStoreEnabled = enabled;
s.isPetEnabled = enabled;
s.isServiceEnabled = enabled;
s.isStaffEnabled = enabled;
s.isDateEnabled = enabled;
s.isTimeEnabled = enabled;
}
/**
* Initializes the UI mode (Create vs Edit).
*/
public void initMode(boolean isEditing) {
updateViewState(s -> {
s.isEditing = isEditing;
s.isDeleteVisible = isEditing;
if (isEditing) {
s.isCustomerEnabled = false;
s.isStoreEnabled = false;
s.isPetEnabled = false;
s.isServiceEnabled = false;
} else {
s.isCustomerEnabled = true;
s.isStoreEnabled = true;
s.isServiceEnabled = true;
s.isPetEnabled = false; // until customer selected
s.isStaffEnabled = false; // until store selected
s.availableStatuses = new String[]{"Booked"};
}
});
}
/**
* Validates if a booking is in the future.
*/
public boolean isValidFutureBooking(String status, String date, String time) {
return !"BOOKED".equalsIgnoreCase(status) || !DateTimeUtils.isDateTimeInPast(date, time);
}
/**
* Helper to update the view state and notify observers.
*/
private void updateViewState(Action<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;
}
}

View File

@@ -1,23 +1,14 @@
package com.example.petstoremobile.viewmodels;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.petstoremobile.dtos.AppointmentDTO;
import com.example.petstoremobile.dtos.BulkDeleteRequest;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.ServiceDTO;
import com.example.petstoremobile.repositories.AppointmentRepository;
import com.example.petstoremobile.repositories.CustomerRepository;
import com.example.petstoremobile.repositories.PetRepository;
import com.example.petstoremobile.repositories.ServiceRepository;
import com.example.petstoremobile.repositories.StoreRepository;
import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.repositories.AppointmentRepository;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
@@ -27,38 +18,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
@HiltViewModel
public class AppointmentViewModel extends ViewModel {
private final AppointmentRepository repository;
private final CustomerRepository customerRepository;
private final StoreRepository storeRepository;
private final PetRepository petRepository;
private final ServiceRepository serviceRepository;
private final MutableLiveData<List<DropdownDTO>> customers = new MutableLiveData<>();
private final MutableLiveData<List<DropdownDTO>> stores = new MutableLiveData<>();
private final MutableLiveData<List<ServiceDTO>> services = new MutableLiveData<>();
private final MutableLiveData<List<DropdownDTO>> customerPets = new MutableLiveData<>();
private final MutableLiveData<List<DropdownDTO>> storeEmployees = new MutableLiveData<>();
private final MutableLiveData<ViewState> viewState = new MutableLiveData<>(new ViewState());
private long appointmentId = -1;
private Long currentCustomerId;
private Long currentStoreId;
private Long currentPetId;
private Long currentServiceId;
private Long currentStaffId;
@Inject
public AppointmentViewModel(
AppointmentRepository repository,
CustomerRepository customerRepository,
StoreRepository storeRepository,
PetRepository petRepository,
ServiceRepository serviceRepository) {
public AppointmentViewModel(AppointmentRepository repository) {
this.repository = repository;
this.customerRepository = customerRepository;
this.storeRepository = storeRepository;
this.petRepository = petRepository;
this.serviceRepository = serviceRepository;
}
// API CRUD
@@ -70,307 +33,10 @@ public class AppointmentViewModel extends ViewModel {
return repository.getAllAppointments(page, size, query, status, storeId, date, employeeId);
}
/**
* Retrieves a single appointment by its ID.
*/
public LiveData<Resource<AppointmentDTO>> getAppointmentById(Long id) {
return repository.getAppointmentById(id);
}
/**
* Creates a new appointment.
*/
public LiveData<Resource<AppointmentDTO>> createAppointment(AppointmentDTO appointment) {
return repository.createAppointment(appointment);
}
/**
* Updates an existing appointment record by ID.
*/
public LiveData<Resource<AppointmentDTO>> updateAppointment(Long id, AppointmentDTO appointment) {
return repository.updateAppointment(id, appointment);
}
/**
* Deletes an appointment record by ID.
*/
public LiveData<Resource<Void>> deleteAppointment(Long id) {
return repository.deleteAppointment(id);
}
/**
* Deletes multiple appointment records.
*/
public LiveData<Resource<Void>> bulkDeleteAppointments(List<String> ids) {
return repository.bulkDeleteAppointments(new BulkDeleteRequest(ids));
}
// Initial Data Loading
/**
* Loads initial dropdown data for customers, stores, and services.
*/
public void loadInitialFormData() {
customerRepository.getCustomerDropdowns().observeForever(r -> {
if (r.status == Resource.Status.SUCCESS) customers.setValue(r.data);
});
storeRepository.getStoreDropdowns().observeForever(r -> {
if (r.status == Resource.Status.SUCCESS) stores.setValue(r.data);
});
serviceRepository.getAllServices(0, 200, null, "serviceName").observeForever(r -> {
if (r.status == Resource.Status.SUCCESS && r.data != null) services.setValue(r.data.getContent());
});
}
// LiveData Getters
public LiveData<List<DropdownDTO>> getCustomers() { return customers; }
public LiveData<List<DropdownDTO>> getStores() { return stores; }
public LiveData<List<ServiceDTO>> getServices() { return services; }
public LiveData<List<DropdownDTO>> getCustomerPets() { return customerPets; }
public LiveData<List<DropdownDTO>> getStoreEmployees() { return storeEmployees; }
public LiveData<ViewState> getViewState() { return viewState; }
//State Getters
public long getAppointmentId() { return appointmentId; }
/**
* Sets the current appointment ID and updates the mode.
*/
public void setAppointmentId(long id) {
this.appointmentId = id;
initMode(id != -1);
}
// Selection Handlers for spinners
public void onCustomerSelected(int position) {
List<DropdownDTO> list = customers.getValue();
if (position > 0 && list != null && position <= list.size()) {
currentCustomerId = list.get(position - 1).getId();
loadPetsForCustomer(currentCustomerId);
updateViewState(s -> {
s.selectedCustomerId = currentCustomerId;
s.isPetEnabled = !s.isEditing;
});
} else {
currentCustomerId = null;
customerPets.setValue(new ArrayList<>());
updateViewState(s -> {
s.selectedCustomerId = null;
s.isPetEnabled = false;
});
}
}
public void onStoreSelected(int position) {
List<DropdownDTO> list = stores.getValue();
if (position > 0 && list != null && position <= list.size()) {
currentStoreId = list.get(position - 1).getId();
loadEmployeesForStore(currentStoreId);
updateViewState(s -> {
s.selectedStoreId = currentStoreId;
s.isStaffEnabled = !s.isPast;
});
} else {
currentStoreId = null;
storeEmployees.setValue(new ArrayList<>());
updateViewState(s -> {
s.selectedStoreId = null;
s.isStaffEnabled = false;
});
}
}
public void onServiceSelected(int position) {
List<ServiceDTO> list = services.getValue();
currentServiceId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getServiceId() : null;
updateViewState(s -> s.selectedServiceId = currentServiceId);
}
public void onPetSelected(int position) {
List<DropdownDTO> list = customerPets.getValue();
currentPetId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null;
updateViewState(s -> s.selectedPetId = currentPetId);
}
public void onStaffSelected(int position) {
List<DropdownDTO> list = storeEmployees.getValue();
currentStaffId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null;
updateViewState(s -> s.selectedStaffId = currentStaffId);
}
private void loadPetsForCustomer(Long customerId) {
petRepository.getCustomerPets(customerId).observeForever(r -> {
if (r.status == Resource.Status.SUCCESS) customerPets.setValue(r.data);
});
}
private void loadEmployeesForStore(Long storeId) {
storeRepository.getStoreEmployees(storeId).observeForever(r -> {
if (r.status == Resource.Status.SUCCESS) storeEmployees.setValue(r.data);
});
}
// Appointment Detail CRUD
/**
* Fetches appointment details and populates internal state.
*/
public LiveData<Resource<AppointmentDTO>> loadAppointment() {
MutableLiveData<Resource<AppointmentDTO>> result = new MutableLiveData<>();
repository.getAppointmentById(appointmentId).observeForever(resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
AppointmentDTO a = resource.data;
currentCustomerId = a.getCustomerId();
currentStoreId = a.getStoreId();
currentPetId = a.getPetId();
currentServiceId = a.getServiceId();
currentStaffId = a.getEmployeeId();
updateViewState(s -> {
s.selectedCustomerId = currentCustomerId;
s.selectedStoreId = currentStoreId;
s.selectedPetId = currentPetId;
s.selectedServiceId = currentServiceId;
s.selectedStaffId = currentStaffId;
});
if (currentCustomerId != null) loadPetsForCustomer(currentCustomerId);
if (currentStoreId != null) loadEmployeesForStore(currentStoreId);
}
result.setValue(resource);
});
return result;
}
/**
* Saves the appointment by building the DTO from tracked state.
*/
public LiveData<Resource<AppointmentDTO>> saveAppointment(String date, String time, String status) {
AppointmentDTO dto = new AppointmentDTO(currentCustomerId, currentStoreId, currentServiceId, currentStaffId, date, time, status, currentPetId);
if (appointmentId != -1) {
return repository.updateAppointment(appointmentId, dto);
} else {
return repository.createAppointment(dto);
}
}
/**
* Deletes the current appointment.
*/
public LiveData<Resource<Void>> deleteAppointment() {
return repository.deleteAppointment(appointmentId);
}
// --- UI Logic ---
public void onDateOrTimeChanged(String date, String time, String currentStatus) {
updateViewState(s -> {
s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus);
boolean isPast = DateTimeUtils.isDateTimeInPast(date, time);
boolean isCancelled = "Cancelled".equalsIgnoreCase(currentStatus);
if (isCancelled) {
s.isPast = true;
setAllFieldsEnabled(s, false);
s.isStatusEnabled = false;
s.isSaveVisible = false;
} else if (isPast) {
s.isPast = true;
setAllFieldsEnabled(s, false);
s.isStatusEnabled = true;
} else {
s.isPast = false;
if (!s.isEditing) {
s.isCustomerEnabled = true;
s.isStoreEnabled = true;
s.isServiceEnabled = true;
s.isPetEnabled = currentCustomerId != null;
}
s.isDateEnabled = true;
s.isTimeEnabled = true;
s.isStatusEnabled = true;
}
});
}
private String[] calculateAvailableStatuses(boolean isEditing, String date, String currentTime, String currentStatus) {
if (!isEditing) return new String[]{"Booked"};
if (date == null || date.isEmpty()) return new String[]{};
if ("Cancelled".equalsIgnoreCase(currentStatus)) return new String[]{"Cancelled"};
if (DateTimeUtils.isDateTimeInPast(date, currentTime)) return new String[]{"Completed", "Missed"};
return new String[]{"Booked", "Cancelled"};
}
private void setAllFieldsEnabled(ViewState s, boolean enabled) {
s.isCustomerEnabled = enabled;
s.isStoreEnabled = enabled;
s.isPetEnabled = enabled;
s.isServiceEnabled = enabled;
s.isStaffEnabled = enabled;
s.isDateEnabled = enabled;
s.isTimeEnabled = enabled;
}
public void initMode(boolean isEditing) {
updateViewState(s -> {
s.isEditing = isEditing;
s.isDeleteVisible = isEditing;
if (isEditing) {
s.isCustomerEnabled = false;
s.isStoreEnabled = false;
s.isPetEnabled = false;
s.isServiceEnabled = false;
} else {
s.isCustomerEnabled = true;
s.isStoreEnabled = true;
s.isServiceEnabled = true;
s.isPetEnabled = false; // until customer selected
s.isStaffEnabled = false; // until store selected
s.availableStatuses = new String[]{"Booked"};
}
});
}
public boolean isValidFutureBooking(String status, String date, String time) {
return !"BOOKED".equalsIgnoreCase(status) || !DateTimeUtils.isDateTimeInPast(date, time);
}
private void updateViewState(Action<ViewState> action) {
ViewState current = viewState.getValue();
if (current != null) {
action.run(current);
viewState.setValue(current);
}
}
private interface Action<T> { void run(T t); }
/**
* A Class to show the states of Appointment Detail Fragment.
*/
public static class ViewState {
public boolean isPast = false;
public boolean isEditing = false;
public boolean isSaveVisible = true;
public boolean isDeleteVisible = false;
public boolean isCustomerEnabled = true;
public boolean isStoreEnabled = true;
public boolean isPetEnabled = false;
public boolean isServiceEnabled = true;
public boolean isStaffEnabled = false;
public boolean isDateEnabled = true;
public boolean isTimeEnabled = true;
public boolean isStatusEnabled = true;
public String[] availableStatuses = new String[]{};
// Selected IDs
public Long selectedCustomerId = null;
public Long selectedStoreId = null;
public Long selectedPetId = null;
public Long selectedServiceId = null;
public Long selectedStaffId = null;
}
}