From 0ee097e82d5d8f6c70964fe4bc0e8a31ea196558 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 23:39:34 -0600 Subject: [PATCH 1/8] Fixed profile issue with camera and added viewstate to pet and service --- .../listfragments/ServiceFragment.java | 7 +- .../detailfragments/PetDetailFragment.java | 147 ++++++++------ .../ServiceDetailFragment.java | 60 ++++-- .../petstoremobile/utils/FileUtils.java | 24 ++- .../utils/ImagePickerHelper.java | 15 +- .../viewmodels/PetDetailViewModel.java | 189 +++++++++++++++--- .../viewmodels/ServiceDetailViewModel.java | 81 +++++++- 7 files changed, 398 insertions(+), 125 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index 3a1b45a1..1aaf625d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -56,11 +56,14 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic setupFilterToggle(); setupBulkDelete(); observeViewModel(); - + loadServices(true); UIUtils.setupHamburgerMenu(binding.btnHamburger, this); + binding.fabAddService.setOnClickListener(v -> + NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail)); + return binding.getRoot(); } @@ -156,4 +159,4 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic bulkDeleteHandler.onSelectionChanged(count); } } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index ee1b34fc..d56ba6a6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -41,9 +41,7 @@ public class PetDetailFragment extends Fragment { private FragmentPetDetailBinding binding; private PetDetailViewModel viewModel; - - private Long selectedCustomerId = null; - private Long selectedStoreId = null; + private boolean isUpdatingUI = false; @Override public void onCreate(Bundle savedInstanceState) { @@ -65,6 +63,7 @@ public class PetDetailFragment extends Fragment { setupSpinner(); observeViewModel(); handleArguments(); + viewModel.loadInitialFormData(); binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSavePet.setOnClickListener(v -> savePet()); @@ -72,23 +71,18 @@ public class PetDetailFragment extends Fragment { } private void observeViewModel() { - viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> updateCustomerSpinnerSelection()); - viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> updateStoreSpinnerSelection()); - - viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - setLoading(resource.status == Resource.Status.LOADING); - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - viewModel.setCustomerList(resource.data); - } + viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); + + viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> { + PetDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + Long selectedCustomerId = state != null ? state.selectedCustomerId : null; + updateCustomerSpinnerSelection(selectedCustomerId); }); - viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - setLoading(resource.status == Resource.Status.LOADING); - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - viewModel.setStoreList(resource.data); - } + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> { + PetDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + Long selectedStoreId = state != null ? state.selectedStoreId : null; + updateStoreSpinnerSelection(selectedStoreId); }); } @@ -119,12 +113,12 @@ public class PetDetailFragment extends Fragment { String status = binding.spinnerPetStatus.getSelectedItem().toString(); Long customerId = null; - if (binding.spinnerCustomer.getSelectedItemPosition() > 0) { + if (binding.spinnerCustomer.getSelectedItemPosition() > 0 && viewModel.getCustomerList().getValue() != null) { customerId = viewModel.getCustomerList().getValue().get(binding.spinnerCustomer.getSelectedItemPosition() - 1).getId(); } Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0) { + if (binding.spinnerStore.getSelectedItemPosition() > 0 && viewModel.getStoreList().getValue() != null) { storeId = viewModel.getStoreList().getValue().get(binding.spinnerStore.getSelectedItemPosition() - 1).getId(); } @@ -193,24 +187,12 @@ public class PetDetailFragment extends Fragment { private void handleArguments() { if (getArguments() != null && getArguments().containsKey("petId")) { - long petId = getArguments().getLong("petId"); - viewModel.setPetId(petId); - binding.tvMode.setText("Edit Pet"); - binding.tvPetId.setText(DateTimeUtils.formatId(petId)); - binding.tvPetId.setVisibility(View.VISIBLE); - binding.btnDeletePet.setVisibility(View.VISIBLE); - - UIUtils.setViewsEnabled(false, binding.etPetSpecies, binding.etPetBreed); + viewModel.setPetId(getArguments().getLong("petId")); loadPetData(); - } else { - viewModel.setPetId(-1); - binding.tvMode.setText("Add Pet"); - binding.tvPetId.setVisibility(View.GONE); - binding.btnDeletePet.setVisibility(View.GONE); - binding.btnSavePet.setText("Add"); - - UIUtils.setViewsEnabled(true, binding.etPetSpecies, binding.etPetBreed); + return; } + + viewModel.setPetId(-1); } private void loadPetData() { @@ -226,20 +208,13 @@ public class PetDetailFragment extends Fragment { if (p.getPetPrice() != null) { binding.etPetPrice.setText(String.format(Locale.getDefault(), "%.2f", p.getPetPrice())); } - SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, p.getPetStatus()); - - selectedCustomerId = p.getCustomerId(); - updateCustomerSpinnerSelection(); - - selectedStoreId = p.getStoreId(); - updateStoreSpinnerSelection(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load pet: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } - private void updateCustomerSpinnerSelection() { + private void updateCustomerSpinnerSelection(Long selectedCustomerId) { SpinnerUtils.populateSpinner( requireContext(), binding.spinnerCustomer, @@ -251,7 +226,7 @@ public class PetDetailFragment extends Fragment { ); } - private void updateStoreSpinnerSelection() { + private void updateStoreSpinnerSelection(Long selectedStoreId) { SpinnerUtils.populateSpinner( requireContext(), binding.spinnerStore, @@ -264,36 +239,76 @@ public class PetDetailFragment extends Fragment { } private void setupSpinner() { - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, - new String[]{"Available", "Adopted", "Owned"}); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, new String[]{}); - binding.spinnerPetStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - String status = parent.getItemAtPosition(position).toString(); - - clearSpinnerError(binding.spinnerCustomer); - clearSpinnerError(binding.spinnerStore); - - if ("Available".equalsIgnoreCase(status)) { - binding.spinnerCustomer.setSelection(0); - UIUtils.setViewsEnabled(false, binding.spinnerCustomer); - } else { - UIUtils.setViewsEnabled(true, binding.spinnerCustomer); - } - - if ("Owned".equalsIgnoreCase(status)) { - binding.spinnerStore.setSelection(0); - UIUtils.setViewsEnabled(false, binding.spinnerStore); - } else { - UIUtils.setViewsEnabled(true, binding.spinnerStore); - } + if (isUpdatingUI) return; + viewModel.onCustomerSelected(position); } @Override public void onNothingSelected(AdapterView parent) { } }); + + binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (isUpdatingUI) return; + viewModel.onStoreSelected(position); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + + binding.spinnerPetStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (isUpdatingUI) return; + String status = parent.getItemAtPosition(position).toString(); + clearSpinnerError(binding.spinnerCustomer); + clearSpinnerError(binding.spinnerStore); + viewModel.onStatusSelected(status); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + } + + private void applyViewState(PetDetailViewModel.ViewState state) { + isUpdatingUI = true; + + binding.tvMode.setText(state.modeTitle); + binding.tvPetId.setText(DateTimeUtils.formatId(viewModel.getPetId())); + binding.tvPetId.setVisibility(state.isPetIdVisible ? View.VISIBLE : View.GONE); + binding.btnDeletePet.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); + binding.btnSavePet.setText(state.saveButtonText); + + UIUtils.setViewsEnabled(state.isSpeciesEnabled, binding.etPetSpecies); + UIUtils.setViewsEnabled(state.isBreedEnabled, binding.etPetBreed); + UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerCustomer); + UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerStore); + + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, state.availableStatuses); + SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, state.selectedStatus); + + updateCustomerSpinnerSelection(state.selectedCustomerId); + updateStoreSpinnerSelection(state.selectedStoreId); + + if (!state.isCustomerEnabled && binding.spinnerCustomer.getSelectedItemPosition() != 0) { + binding.spinnerCustomer.setSelection(0); + } + if (!state.isStoreEnabled && binding.spinnerStore.getSelectedItemPosition() != 0) { + binding.spinnerStore.setSelection(0); + } + + isUpdatingUI = false; } private void clearSpinnerError(Spinner spinner) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index 2374fc4c..d7ee6e4c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -11,9 +11,9 @@ import androidx.navigation.fragment.NavHostFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; import android.widget.Toast; -import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentServiceDetailBinding; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.ActivityLogger; @@ -21,6 +21,7 @@ import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.ServiceDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -51,6 +52,7 @@ public class ServiceDetailFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + observeViewModel(); handleArguments(); binding.btnBack.setOnClickListener(v -> navigateBack()); @@ -58,8 +60,12 @@ public class ServiceDetailFragment extends Fragment { binding.btnDeleteService.setOnClickListener(v -> deleteService()); } + private void observeViewModel() { + viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); + } + private void setLoading(boolean loading) { - if (binding != null && binding.progressBar != null) { + if (binding != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } @@ -126,34 +132,48 @@ public class ServiceDetailFragment extends Fragment { private void handleArguments() { if (getArguments() != null && getArguments().containsKey("serviceId")) { - long serviceId = getArguments().getLong("serviceId"); - viewModel.setServiceId(serviceId); - binding.tvMode.setText("Edit Service"); - binding.tvServiceId.setText(DateTimeUtils.formatId(serviceId)); - binding.btnDeleteService.setVisibility(View.VISIBLE); + viewModel.setServiceId(getArguments().getLong("serviceId")); loadServiceData(); - } else { - viewModel.setServiceId(-1); - binding.tvMode.setText("Add Service"); - binding.tvServiceId.setVisibility(View.GONE); - binding.btnDeleteService.setVisibility(View.GONE); - binding.btnSaveService.setText("Add"); + return; } + + viewModel.setServiceId(-1); } private void loadServiceData() { viewModel.loadService().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; setLoading(resource.status == Resource.Status.LOADING); - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - ServiceDTO s = resource.data; - binding.etServiceName.setText(s.getServiceName()); - binding.etServiceDesc.setText(s.getServiceDesc()); - binding.etServiceDuration.setText(String.valueOf(s.getServiceDuration())); - binding.etServicePrice.setText(String.valueOf(s.getServicePrice())); - } else if (resource.status == Resource.Status.ERROR) { + if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load service: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } + + private void applyViewState(ServiceDetailViewModel.ViewState state) { + binding.tvMode.setText(state.modeTitle); + binding.tvServiceId.setText(DateTimeUtils.formatId(viewModel.getServiceId())); + binding.tvServiceId.setVisibility(state.isServiceIdVisible ? View.VISIBLE : View.GONE); + binding.btnDeleteService.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); + binding.btnSaveService.setText(state.saveButtonText); + + UIUtils.setViewsEnabled(state.isFieldsEnabled, + binding.etServiceName, + binding.etServiceDesc, + binding.etServiceDuration, + binding.etServicePrice); + + updateIfDifferent(binding.etServiceName, state.serviceName); + updateIfDifferent(binding.etServiceDesc, state.serviceDesc); + updateIfDifferent(binding.etServiceDuration, state.serviceDuration); + updateIfDifferent(binding.etServicePrice, state.servicePrice); + } + + private void updateIfDifferent(EditText field, String value) { + String current = field.getText() != null ? field.getText().toString() : ""; + String next = value != null ? value : ""; + if (!current.equals(next)) { + field.setText(next); + } + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java index bf8c8770..bf45f4f8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java @@ -11,13 +11,31 @@ import java.io.InputStream; public class FileUtils { public static File getFileFromUri(Context context, Uri uri) { try { + if ("content".equals(uri.getScheme())) { + String authority = uri.getAuthority(); + if (authority != null && authority.equals(context.getPackageName() + ".fileprovider")) { + String lastSegment = uri.getLastPathSegment(); + if (lastSegment != null) { + String fileName = lastSegment.contains("/") + ? lastSegment.substring(lastSegment.lastIndexOf('/') + 1) + : lastSegment; + File cachedFile = new File(context.getCacheDir(), fileName); + if (cachedFile.exists() && cachedFile.length() > 0) { + return cachedFile; + } + } + } + } + String fileName = getFileName(context, uri); if (fileName == null) fileName = "upload_" + System.currentTimeMillis(); - + InputStream inputStream = context.getContentResolver().openInputStream(uri); + if (inputStream == null) return null; + File tempFile = new File(context.getCacheDir(), fileName); FileOutputStream outputStream = new FileOutputStream(tempFile); - byte[] buffer = new byte[1024]; + byte[] buffer = new byte[4096]; int length; while ((length = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, length); @@ -47,4 +65,4 @@ public class FileUtils { } return result; } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java b/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java index 4d1e9bf9..2bcbb52a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java @@ -129,9 +129,16 @@ public class ImagePickerHelper { * Prepares a temporary file and launches the camera app. */ private void launchCamera() { - File photoFile = new File(fragment.requireContext().getCacheDir(), tempFileName); - photoUri = FileProvider.getUriForFile(fragment.requireContext(), fragment.requireContext().getPackageName() + ".fileprovider", photoFile); - cameraLauncher.launch(photoUri); + try { + File photoFile = new File(fragment.requireContext().getCacheDir(), tempFileName); + if (!photoFile.exists()) photoFile.createNewFile(); + photoUri = FileProvider.getUriForFile(fragment.requireContext(), + fragment.requireContext().getPackageName() + ".fileprovider", photoFile); + cameraLauncher.launch(photoUri); + } catch (Exception e) { + android.widget.Toast.makeText(fragment.requireContext(), + "Could not prepare camera", android.widget.Toast.LENGTH_SHORT).show(); + } } /** @@ -157,4 +164,4 @@ public class ImagePickerHelper { .setNegativeButton("Cancel", null) .show(); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java index 68506f44..44f2680a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java @@ -20,17 +20,22 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class PetDetailViewModel extends ViewModel { + private static final String STATUS_AVAILABLE = "Available"; + private static final String STATUS_ADOPTED = "Adopted"; + private static final String STATUS_OWNED = "Owned"; + private final PetRepository petRepository; private final CustomerRepository customerRepository; private final StoreRepository storeRepository; - private final MutableLiveData petState = new MutableLiveData<>(); private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); - + private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); + private long petId = -1; - private boolean isEditing = false; + private Long selectedCustomerId = null; + private Long selectedStoreId = null; @Inject public PetDetailViewModel(PetRepository petRepository, CustomerRepository customerRepository, StoreRepository storeRepository) { @@ -39,9 +44,23 @@ public class PetDetailViewModel extends ViewModel { this.storeRepository = storeRepository; } + public void loadInitialFormData() { + customerRepository.getCustomerDropdowns().observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + customerList.setValue(resource.data); + } + }); + + storeRepository.getStoreDropdowns().observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList.setValue(resource.data); + } + }); + } + public void setPetId(long id) { this.petId = id; - this.isEditing = id != -1; + initMode(id != -1); } public long getPetId() { @@ -49,46 +68,108 @@ public class PetDetailViewModel extends ViewModel { } public boolean isEditing() { - return isEditing; + ViewState current = viewState.getValue(); + return current != null && current.isEditing; + } + + public LiveData getViewState() { + return viewState; + } + + public void onCustomerSelected(int position) { + List list = customerList.getValue(); + if (position > 0 && list != null && position <= list.size()) { + selectedCustomerId = list.get(position - 1).getId(); + } else { + selectedCustomerId = null; + } + + updateViewState(state -> state.selectedCustomerId = selectedCustomerId); + } + + public void onStoreSelected(int position) { + List list = storeList.getValue(); + if (position > 0 && list != null && position <= list.size()) { + selectedStoreId = list.get(position - 1).getId(); + } else { + selectedStoreId = null; + } + + updateViewState(state -> state.selectedStoreId = selectedStoreId); + } + + public void onStatusSelected(String status) { + updateViewState(state -> { + state.selectedStatus = normalizeStatus(status); + applyStatusRules(state, true); + }); + } + + public void initMode(boolean isEditing) { + updateViewState(state -> { + state.isEditing = isEditing; + state.modeTitle = isEditing ? "Edit Pet" : "Add Pet"; + state.saveButtonText = isEditing ? "Save" : "Add"; + state.isPetIdVisible = isEditing; + state.isDeleteVisible = isEditing; + state.isSpeciesEnabled = !isEditing; + state.isBreedEnabled = !isEditing; + + if (isEditing) { + state.isCustomerEnabled = true; + state.isStoreEnabled = true; + } + + if (!isEditing) { + selectedCustomerId = null; + selectedStoreId = null; + state.selectedCustomerId = null; + state.selectedStoreId = null; + state.selectedStatus = STATUS_AVAILABLE; + state.isCustomerEnabled = false; + state.isStoreEnabled = true; + } + }); } public LiveData> loadPet() { - return petRepository.getPetById(petId); - } + MutableLiveData> result = new MutableLiveData<>(); + petRepository.getPetById(petId).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + PetDTO pet = resource.data; + selectedCustomerId = pet.getCustomerId(); + selectedStoreId = pet.getStoreId(); - public LiveData>> loadCustomers() { - return customerRepository.getCustomerDropdowns(); - } + updateViewState(state -> { + state.selectedCustomerId = selectedCustomerId; + state.selectedStoreId = selectedStoreId; + state.selectedStatus = normalizeStatus(pet.getPetStatus()); + applyStatusRules(state, false); + }); + } - public LiveData>> loadStores() { - return storeRepository.getStoreDropdowns(); + result.setValue(resource); + }); + return result; } public LiveData> savePet(PetDTO petDTO) { - if (isEditing) { + if (isEditing()) { petDTO.setPetId(petId); return petRepository.updatePet(petId, petDTO); - } else { - return petRepository.createPet(petDTO); } + + return petRepository.createPet(petDTO); } public LiveData> deletePet() { return petRepository.deletePet(petId); } - public void setCustomerList(List list) { - customerList.setValue(list); - } - public LiveData> getCustomerList() { return customerList; } - public void setStoreList(List list) { - storeList.setValue(list); - } - public LiveData> getStoreList() { return storeList; } @@ -100,4 +181,66 @@ public class PetDetailViewModel extends ViewModel { public void setLoading(boolean loading) { isLoading.setValue(loading); } + + private void applyStatusRules(ViewState state, boolean clearInvalidSelections) { + if (STATUS_AVAILABLE.equalsIgnoreCase(state.selectedStatus)) { + state.isCustomerEnabled = false; + state.isStoreEnabled = true; + if (clearInvalidSelections) { + selectedCustomerId = null; + state.selectedCustomerId = null; + } + return; + } + + if (STATUS_OWNED.equalsIgnoreCase(state.selectedStatus)) { + state.isCustomerEnabled = true; + state.isStoreEnabled = false; + if (clearInvalidSelections) { + selectedStoreId = null; + state.selectedStoreId = null; + } + return; + } + + state.isCustomerEnabled = true; + state.isStoreEnabled = true; + } + + private String normalizeStatus(String status) { + if (status == null) return STATUS_AVAILABLE; + String normalized = status.trim(); + if (STATUS_ADOPTED.equalsIgnoreCase(normalized)) return STATUS_ADOPTED; + if (STATUS_OWNED.equalsIgnoreCase(normalized)) return STATUS_OWNED; + return STATUS_AVAILABLE; + } + + private void updateViewState(Action action) { + ViewState current = viewState.getValue(); + if (current != null) { + action.run(current); + viewState.setValue(current); + } + } + + private interface Action { + void run(T target); + } + + + public static class ViewState { + public boolean isEditing = false; + public boolean isDeleteVisible = false; + public boolean isPetIdVisible = false; + public boolean isSpeciesEnabled = true; + public boolean isBreedEnabled = true; + public boolean isCustomerEnabled = false; + public boolean isStoreEnabled = true; + public String modeTitle = "Add Pet"; + public String saveButtonText = "Add"; + public String[] availableStatuses = new String[]{STATUS_AVAILABLE, STATUS_ADOPTED, STATUS_OWNED}; + public String selectedStatus = STATUS_AVAILABLE; + public Long selectedCustomerId = null; + public Long selectedStoreId = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java index fca74229..aa465a08 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java @@ -1,6 +1,7 @@ package com.example.petstoremobile.viewmodels; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.ServiceDTO; @@ -14,8 +15,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class ServiceDetailViewModel extends ViewModel { private final ServiceRepository repository; + private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); + private long serviceId = -1; - private boolean isEditing = false; @Inject public ServiceDetailViewModel(ServiceRepository repository) { @@ -24,7 +26,7 @@ public class ServiceDetailViewModel extends ViewModel { public void setServiceId(long id) { this.serviceId = id; - this.isEditing = id != -1; + initMode(id != -1); } public long getServiceId() { @@ -32,23 +34,88 @@ public class ServiceDetailViewModel extends ViewModel { } public boolean isEditing() { - return isEditing; + ViewState current = viewState.getValue(); + return current != null && current.isEditing; + } + + public LiveData getViewState() { + return viewState; + } + + public void initMode(boolean isEditing) { + updateViewState(state -> { + state.isEditing = isEditing; + state.modeTitle = isEditing ? "Edit Service" : "Add Service"; + state.saveButtonText = isEditing ? "Save" : "Add"; + state.isServiceIdVisible = isEditing; + state.isDeleteVisible = isEditing; + state.isFieldsEnabled = true; + }); } public LiveData> loadService() { - return repository.getServiceById(serviceId); + MutableLiveData> result = new MutableLiveData<>(); + repository.getServiceById(serviceId).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + ServiceDTO service = resource.data; + updateViewState(state -> { + state.serviceName = safeText(service.getServiceName()); + state.serviceDesc = safeText(service.getServiceDesc()); + state.serviceDuration = service.getServiceDuration() != null ? String.valueOf(service.getServiceDuration()) : ""; + state.servicePrice = service.getServicePrice() != null ? String.valueOf(service.getServicePrice()) : ""; + }); + } + result.setValue(resource); + }); + return result; } public LiveData> saveService(ServiceDTO dto) { - if (isEditing) { + updateViewState(state -> { + state.serviceName = safeText(dto.getServiceName()); + state.serviceDesc = safeText(dto.getServiceDesc()); + state.serviceDuration = dto.getServiceDuration() != null ? String.valueOf(dto.getServiceDuration()) : ""; + state.servicePrice = dto.getServicePrice() != null ? String.valueOf(dto.getServicePrice()) : ""; + }); + + if (isEditing()) { dto.setServiceId(serviceId); return repository.updateService(serviceId, dto); - } else { - return repository.createService(dto); } + + return repository.createService(dto); } public LiveData> deleteService() { return repository.deleteService(serviceId); } + + private String safeText(String value) { + return value == null ? "" : value.trim(); + } + + private void updateViewState(Action action) { + ViewState current = viewState.getValue(); + if (current != null) { + action.run(current); + viewState.setValue(current); + } + } + + private interface Action { + void run(T target); + } + + public static class ViewState { + public boolean isEditing = false; + public boolean isDeleteVisible = false; + public boolean isServiceIdVisible = false; + public boolean isFieldsEnabled = true; + public String modeTitle = "Add Service"; + public String saveButtonText = "Add"; + public String serviceName = ""; + public String serviceDesc = ""; + public String serviceDuration = ""; + public String servicePrice = ""; + } } From 5340ddf98bbbb2639d1180a36c263372176beb57 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Fri, 10 Apr 2026 00:28:01 -0600 Subject: [PATCH 2/8] added viewstates to Supplier and Product --- .../AdoptionDetailFragment.java | 208 +++++++----------- .../ProductDetailFragment.java | 4 +- .../SupplierDetailFragment.java | 63 ++++-- .../viewmodels/AdoptionDetailViewModel.java | 163 +++++++++++--- .../viewmodels/ProductDetailViewModel.java | 1 + .../viewmodels/SupplierDetailViewModel.java | 75 ++++++- .../res/layout/fragment_adoption_detail.xml | 29 +-- 7 files changed, 343 insertions(+), 200 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 9b0f022b..0e936edf 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -33,11 +33,6 @@ public class AdoptionDetailFragment extends Fragment { private FragmentAdoptionDetailBinding binding; private AdoptionDetailViewModel viewModel; - private long preselectedPetId = -1; - private long preselectedCustomerId = -1; - private long preselectedStoreId = -1; - private long preselectedEmployeeId = -1; - private final String[] STATUSES = {"Pending", "Completed", "Cancelled"}; @Override @@ -59,7 +54,7 @@ public class AdoptionDetailFragment extends Fragment { setupSpinners(); setupDatePicker(); observeViewModel(); - loadSpinnersData(); + viewModel.loadInitialFormData(); handleArguments(); binding.btnAdoptionBack.setOnClickListener(v -> navigateBack()); @@ -68,14 +63,39 @@ public class AdoptionDetailFragment extends Fragment { } private void observeViewModel() { - viewModel.getPetList().observe(getViewLifecycleOwner(), list -> refreshPetSpinner()); - viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> refreshCustomerSpinner()); - viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner()); - viewModel.getEmployeeList().observe(getViewLifecycleOwner(), list -> refreshEmployeeSpinner()); + viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); + + viewModel.getPetList().observe(getViewLifecycleOwner(), list -> { + AdoptionDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + Long petId = state != null ? state.selectedPetId : null; + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, list, + DropdownDTO::getLabel, "-- Select Pet --", petId, DropdownDTO::getId); + }); + + viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> { + AdoptionDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + Long customerId = state != null ? state.selectedCustomerId : null; + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, list, + DropdownDTO::getLabel, "-- Select Customer --", customerId, DropdownDTO::getId); + }); + + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> { + AdoptionDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + Long storeId = state != null ? state.selectedStoreId : null; + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, list, + DropdownDTO::getLabel, "-- Select Store --", storeId, DropdownDTO::getId); + }); + + viewModel.getEmployeeList().observe(getViewLifecycleOwner(), list -> { + AdoptionDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + Long employeeId = state != null ? state.selectedEmployeeId : null; + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, list, + DropdownDTO::getLabel, "-- Select Staff --", employeeId, DropdownDTO::getId); + }); } private void setLoading(boolean loading) { - if (binding != null && binding.progressBar != null) { + if (binding != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } @@ -94,14 +114,7 @@ public class AdoptionDetailFragment extends Fragment { binding.spinnerAdoptionCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (position > 0) { - UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet); - } else { - if (!viewModel.isEditing()) { - binding.spinnerAdoptionPet.setSelection(0); - UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); - } - } + viewModel.onCustomerSelected(position); } @Override public void onNothingSelected(AdapterView parent) {} @@ -110,12 +123,7 @@ public class AdoptionDetailFragment extends Fragment { binding.spinnerAdoptionStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (position > 0 && viewModel.getStoreList().getValue() != null && position <= viewModel.getStoreList().getValue().size()) { - DropdownDTO selectedStore = viewModel.getStoreList().getValue().get(position - 1); - loadEmployees(selectedStore.getId()); - } else { - viewModel.setEmployeeList(new ArrayList<>()); - } + viewModel.onStoreSelected(position); } @Override public void onNothingSelected(AdapterView parent) {} @@ -126,111 +134,64 @@ public class AdoptionDetailFragment extends Fragment { binding.etAdoptionDate.setOnClickListener(v -> UIUtils.showDatePicker(requireContext(), binding.etAdoptionDate, null)); } - private void loadSpinnersData() { - viewModel.loadPets().observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - setLoading(resource.status == Resource.Status.LOADING); - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - viewModel.setPetList(resource.data); - } - }); - viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - setLoading(resource.status == Resource.Status.LOADING); - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - viewModel.setCustomerList(resource.data); - } - }); - viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - setLoading(resource.status == Resource.Status.LOADING); - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - viewModel.setStoreList(resource.data); - } - }); - } - - private void refreshPetSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, viewModel.getPetList().getValue(), - DropdownDTO::getLabel, "-- Select Pet --", - preselectedPetId, DropdownDTO::getId); - } - - private void refreshCustomerSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, viewModel.getCustomerList().getValue(), - DropdownDTO::getLabel, "-- Select Customer --", - preselectedCustomerId, DropdownDTO::getId); - } - - private void refreshStoreSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, viewModel.getStoreList().getValue(), - DropdownDTO::getLabel, "-- Select Store --", - preselectedStoreId, DropdownDTO::getId); - } - - private void loadEmployees(Long storeId) { - viewModel.loadEmployees(storeId).observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - setLoading(resource.status == Resource.Status.LOADING); - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - viewModel.setEmployeeList(resource.data); - } - }); - } - - private void refreshEmployeeSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, viewModel.getEmployeeList().getValue(), - DropdownDTO::getLabel, "-- Select Staff --", - preselectedEmployeeId, DropdownDTO::getId); - } - private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("adoptionId")) { - long adoptionId = a.getLong("adoptionId"); - viewModel.setAdoptionId(adoptionId); - binding.tvAdoptionMode.setText("Edit Adoption"); - binding.tvAdoptionId.setText(DateTimeUtils.formatId(adoptionId)); - binding.tvAdoptionId.setVisibility(View.VISIBLE); - binding.btnDeleteAdoption.setVisibility(View.VISIBLE); + viewModel.setAdoptionId(a.getLong("adoptionId")); loadAdoptionData(); - } else { - viewModel.setAdoptionId(-1); - binding.tvAdoptionMode.setText("Add Adoption"); - binding.btnDeleteAdoption.setVisibility(View.GONE); - binding.tvAdoptionId.setVisibility(View.GONE); - UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); + return; } + viewModel.setAdoptionId(-1); } private void loadAdoptionData() { viewModel.loadAdoption().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; setLoading(resource.status == Resource.Status.LOADING); - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - AdoptionDTO a = resource.data; - preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; - preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; - preselectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1; - preselectedEmployeeId = a.getEmployeeId() != null ? a.getEmployeeId() : -1; - - binding.etAdoptionDate.setText(a.getAdoptionDate()); - binding.etAdoptionFee.setText(a.getAdoptionFee() != null ? a.getAdoptionFee().toString() : ""); - SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getAdoptionStatus()); - - refreshPetSpinner(); - refreshCustomerSpinner(); - refreshStoreSpinner(); - - if (preselectedCustomerId != -1) { - UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet); - } - } else if (resource.status == Resource.Status.ERROR) { + if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } + private void applyViewState(AdoptionDetailViewModel.ViewState state) { + binding.tvAdoptionMode.setText(state.modeTitle); + binding.tvAdoptionId.setText(DateTimeUtils.formatId(viewModel.getAdoptionId())); + binding.tvAdoptionId.setVisibility(state.isAdoptionIdVisible ? View.VISIBLE : View.GONE); + binding.btnDeleteAdoption.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); + binding.btnSaveAdoption.setText(state.saveButtonText); + + UIUtils.setViewsEnabled(state.isPetEnabled, binding.spinnerAdoptionPet); + UIUtils.setViewsEnabled(state.isEmployeeEnabled, binding.spinnerAdoptionEmployee); + + if (!state.adoptionDate.isEmpty()) { + binding.etAdoptionDate.setText(state.adoptionDate); + } + if (!state.adoptionFee.isEmpty()) { + binding.etAdoptionFee.setText(state.adoptionFee); + } + if (!state.adoptionStatus.isEmpty()) { + SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, state.adoptionStatus); + } + + // Re-populate spinners with updated preselected IDs + List pets = viewModel.getPetList().getValue(); + if (pets != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, + pets, DropdownDTO::getLabel, "-- Select Pet --", state.selectedPetId, DropdownDTO::getId); + + List customers = viewModel.getCustomerList().getValue(); + if (customers != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, + customers, DropdownDTO::getLabel, "-- Select Customer --", state.selectedCustomerId, DropdownDTO::getId); + + List stores = viewModel.getStoreList().getValue(); + if (stores != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, + stores, DropdownDTO::getLabel, "-- Select Store --", state.selectedStoreId, DropdownDTO::getId); + + List employees = viewModel.getEmployeeList().getValue(); + if (employees != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, + employees, DropdownDTO::getLabel, "-- Select Staff --", state.selectedEmployeeId, DropdownDTO::getId); + } + private void saveAdoption() { if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionCustomer, "Customer")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionPet, "Pet")) return; @@ -247,24 +208,17 @@ public class AdoptionDetailFragment extends Fragment { DropdownDTO customer = viewModel.getCustomerList().getValue().get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); DropdownDTO pet = viewModel.getPetList().getValue().get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); - + Long employeeId = null; - if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0) { + if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0 && viewModel.getEmployeeList().getValue() != null) { employeeId = viewModel.getEmployeeList().getValue().get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId(); } - - String adoptionDate = binding.etAdoptionDate.getText().toString().trim(); - String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; + + String adoptionDate = binding.etAdoptionDate.getText().toString().trim(); + String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; AdoptionDTO dto = new AdoptionDTO( - pet.getId(), - customer.getId(), - employeeId, - store.getId(), - adoptionDate, - status, - fee - ); + pet.getId(), customer.getId(), employeeId, store.getId(), adoptionDate, status, fee); viewModel.saveAdoption(dto).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 9f072b51..c580905b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -110,7 +110,7 @@ public class ProductDetailFragment extends Fragment { private void observeViewModel() { viewModel.getCategoryList().observe(getViewLifecycleOwner(), list -> updateCategorySpinner()); - + viewModel.loadCategories().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; setLoading(resource.status == Resource.Status.LOADING); @@ -268,7 +268,7 @@ public class ProductDetailFragment extends Fragment { } private void confirmDelete() { - DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () -> + DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () -> viewModel.deleteProduct().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; setLoading(resource.status == Resource.Status.LOADING); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index a7c64079..5eb1f43b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -11,11 +11,13 @@ import androidx.navigation.fragment.NavHostFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; import android.widget.Toast; import com.example.petstoremobile.databinding.FragmentSupplierDetailBinding; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; @@ -51,6 +53,7 @@ public class SupplierDetailFragment extends Fragment { super.onViewCreated(view, savedInstanceState); UIUtils.formatPhoneInput(binding.etSupPhone); + observeViewModel(); handleArguments(); binding.btnBack.setOnClickListener(v -> navigateBack()); @@ -58,8 +61,12 @@ public class SupplierDetailFragment extends Fragment { binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); } + private void observeViewModel() { + viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); + } + private void setLoading(boolean loading) { - if (binding != null && binding.progressBar != null) { + if (binding != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } @@ -129,36 +136,50 @@ public class SupplierDetailFragment extends Fragment { private void handleArguments() { if (getArguments() != null && getArguments().containsKey("supId")) { - long supId = getArguments().getLong("supId"); - viewModel.setSupId(supId); - binding.tvMode.setText("Edit Supplier"); - binding.tvSupId.setText("ID: " + supId); - binding.tvSupId.setVisibility(View.VISIBLE); - binding.btnDeleteSupplier.setVisibility(View.VISIBLE); + viewModel.setSupId(getArguments().getLong("supId")); loadSupplierData(); - } else { - viewModel.setSupId(-1); - binding.tvMode.setText("Add Supplier"); - binding.tvSupId.setVisibility(View.GONE); - binding.btnDeleteSupplier.setVisibility(View.GONE); - binding.btnSaveSupplier.setText("Add"); + return; } + + viewModel.setSupId(-1); } private void loadSupplierData() { viewModel.loadSupplier().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; setLoading(resource.status == Resource.Status.LOADING); - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - SupplierDTO s = resource.data; - binding.etSupCompany.setText(s.getSupCompany()); - binding.etSupContactFirstName.setText(s.getSupContactFirstName()); - binding.etSupContactLastName.setText(s.getSupContactLastName()); - binding.etSupEmail.setText(s.getSupEmail()); - binding.etSupPhone.setText(s.getSupPhone()); - } else if (resource.status == Resource.Status.ERROR) { + if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load supplier: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } + + private void applyViewState(SupplierDetailViewModel.ViewState state) { + binding.tvMode.setText(state.modeTitle); + binding.tvSupId.setText(DateTimeUtils.formatId(viewModel.getSupId())); + binding.tvSupId.setVisibility(state.isSupIdVisible ? View.VISIBLE : View.GONE); + binding.btnDeleteSupplier.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); + binding.btnSaveSupplier.setText(state.saveButtonText); + + UIUtils.setViewsEnabled(state.isFieldsEnabled, + binding.etSupCompany, + binding.etSupContactFirstName, + binding.etSupContactLastName, + binding.etSupEmail, + binding.etSupPhone); + + updateIfDifferent(binding.etSupCompany, state.supCompany); + updateIfDifferent(binding.etSupContactFirstName, state.supFirstName); + updateIfDifferent(binding.etSupContactLastName, state.supLastName); + updateIfDifferent(binding.etSupEmail, state.supEmail); + updateIfDifferent(binding.etSupPhone, state.supPhone); + } + + private void updateIfDifferent(EditText field, String value) { + String current = field.getText() != null ? field.getText().toString() : ""; + String next = value != null ? value : ""; + if (!current.equals(next)) { + field.setText(next); + } + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java index f6e24cd6..6a8ae492 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java @@ -27,12 +27,12 @@ public class AdoptionDetailViewModel extends ViewModel { private final StoreRepository storeRepository; private long adoptionId = -1; - private boolean isEditing = false; private final MutableLiveData> petList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> employeeList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); @Inject public AdoptionDetailViewModel(AdoptionRepository adoptionRepository, PetRepository petRepository, @@ -45,7 +45,7 @@ public class AdoptionDetailViewModel extends ViewModel { public void setAdoptionId(long id) { this.adoptionId = id; - this.isEditing = id != -1; + initMode(id != -1); } public long getAdoptionId() { @@ -53,50 +53,155 @@ public class AdoptionDetailViewModel extends ViewModel { } public boolean isEditing() { - return isEditing; + ViewState current = viewState.getValue(); + return current != null && current.isEditing; + } + + public LiveData getViewState() { + return viewState; + } + + public void initMode(boolean isEditing) { + updateViewState(state -> { + state.isEditing = isEditing; + state.modeTitle = isEditing ? "Edit Adoption" : "Add Adoption"; + state.saveButtonText = isEditing ? "Save" : "Add"; + state.isAdoptionIdVisible = isEditing; + state.isDeleteVisible = isEditing; + state.isPetEnabled = isEditing; + state.isEmployeeEnabled = isEditing; + }); + } + + public void loadInitialFormData() { + petRepository.getAdoptionPets().observeForever(r -> { + if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { + petList.setValue(r.data); + } + }); + customerRepository.getCustomerDropdowns().observeForever(r -> { + if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { + customerList.setValue(r.data); + } + }); + storeRepository.getStoreDropdowns().observeForever(r -> { + if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { + storeList.setValue(r.data); + } + }); + } + + public void onCustomerSelected(int position) { + List list = customerList.getValue(); + Long customerId = (position > 0 && list != null && position <= list.size()) + ? list.get(position - 1).getId() : null; + updateViewState(state -> { + state.selectedCustomerId = customerId; + state.isPetEnabled = customerId != null; + if (customerId == null && !state.isEditing) { + state.selectedPetId = null; + } + }); + } + + public void onStoreSelected(int position) { + List list = storeList.getValue(); + if (position > 0 && list != null && position <= list.size()) { + Long storeId = list.get(position - 1).getId(); + updateViewState(state -> { + state.selectedStoreId = storeId; + state.isEmployeeEnabled = true; + }); + loadEmployeesForStore(storeId); + } else { + employeeList.setValue(new ArrayList<>()); + updateViewState(state -> { + state.selectedStoreId = null; + state.selectedEmployeeId = null; + state.isEmployeeEnabled = false; + }); + } + } + + private void loadEmployeesForStore(Long storeId) { + storeRepository.getStoreEmployees(storeId).observeForever(r -> { + if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { + employeeList.setValue(r.data); + } + }); } public LiveData> loadAdoption() { - return adoptionRepository.getAdoptionById(adoptionId); - } + MutableLiveData> result = new MutableLiveData<>(); + adoptionRepository.getAdoptionById(adoptionId).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + AdoptionDTO a = resource.data; + updateViewState(state -> { + state.selectedPetId = a.getPetId() != null ? a.getPetId() : -1; + state.selectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; + state.selectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1; + state.selectedEmployeeId = a.getEmployeeId() != null ? a.getEmployeeId() : -1; + state.adoptionDate = a.getAdoptionDate() != null ? a.getAdoptionDate() : ""; + state.adoptionFee = a.getAdoptionFee() != null ? a.getAdoptionFee().toString() : ""; + state.adoptionStatus = a.getAdoptionStatus() != null ? a.getAdoptionStatus() : ""; + state.isPetEnabled = state.selectedCustomerId != null && state.selectedCustomerId != -1; + state.isEmployeeEnabled = state.selectedStoreId != null && state.selectedStoreId != -1; + }); - public LiveData>> loadPets() { - return petRepository.getAdoptionPets(); - } - - public LiveData>> loadCustomers() { - return customerRepository.getCustomerDropdowns(); - } - - public LiveData>> loadStores() { - return storeRepository.getStoreDropdowns(); - } - - public LiveData>> loadEmployees(Long storeId) { - return storeRepository.getStoreEmployees(storeId); + if (a.getSourceStoreId() != null) { + loadEmployeesForStore(a.getSourceStoreId()); + } + } + result.setValue(resource); + }); + return result; } public LiveData> saveAdoption(AdoptionDTO dto) { - if (isEditing) { + if (isEditing()) { return adoptionRepository.updateAdoption(adoptionId, dto); - } else { - return adoptionRepository.createAdoption(dto); } + return adoptionRepository.createAdoption(dto); } public LiveData> deleteAdoption() { return adoptionRepository.deleteAdoption(adoptionId); } - public void setPetList(List list) { petList.setValue(list); } public LiveData> getPetList() { return petList; } - - public void setCustomerList(List list) { customerList.setValue(list); } public LiveData> getCustomerList() { return customerList; } - - public void setStoreList(List list) { storeList.setValue(list); } public LiveData> getStoreList() { return storeList; } - - public void setEmployeeList(List list) { employeeList.setValue(list); } public LiveData> getEmployeeList() { return employeeList; } + + // Kept for backward-compatibility with any remaining direct calls + public void setEmployeeList(List list) { employeeList.setValue(list); } + + private void updateViewState(Action action) { + ViewState current = viewState.getValue(); + if (current != null) { + action.run(current); + viewState.setValue(current); + } + } + + private interface Action { + void run(T target); + } + + public static class ViewState { + public boolean isEditing = false; + public boolean isAdoptionIdVisible = false; + public boolean isDeleteVisible = false; + public boolean isPetEnabled = false; + public boolean isEmployeeEnabled = false; + public String modeTitle = "Add Adoption"; + public String saveButtonText = "Add"; + public Long selectedPetId = null; + public Long selectedCustomerId = null; + public Long selectedStoreId = null; + public Long selectedEmployeeId = null; + public String adoptionDate = ""; + public String adoptionFee = ""; + public String adoptionStatus = ""; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java index 9ec0628a..54ff65b5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java @@ -83,3 +83,4 @@ public class ProductDetailViewModel extends ViewModel { return categoryList; } } + diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java index 591beb52..88078102 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java @@ -1,6 +1,7 @@ package com.example.petstoremobile.viewmodels; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.SupplierDTO; @@ -14,8 +15,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class SupplierDetailViewModel extends ViewModel { private final SupplierRepository repository; + private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); + private long supId = -1; - private boolean isEditing = false; @Inject public SupplierDetailViewModel(SupplierRepository repository) { @@ -24,7 +26,7 @@ public class SupplierDetailViewModel extends ViewModel { public void setSupId(long id) { this.supId = id; - this.isEditing = id != -1; + initMode(id != -1); } public long getSupId() { @@ -32,23 +34,82 @@ public class SupplierDetailViewModel extends ViewModel { } public boolean isEditing() { - return isEditing; + ViewState current = viewState.getValue(); + return current != null && current.isEditing; + } + + public LiveData getViewState() { + return viewState; + } + + public void initMode(boolean isEditing) { + updateViewState(state -> { + state.isEditing = isEditing; + state.modeTitle = isEditing ? "Edit Supplier" : "Add Supplier"; + state.saveButtonText = isEditing ? "Save" : "Add"; + state.isSupIdVisible = isEditing; + state.isDeleteVisible = isEditing; + state.isFieldsEnabled = true; + }); } public LiveData> loadSupplier() { - return repository.getSupplierById(supId); + MutableLiveData> result = new MutableLiveData<>(); + repository.getSupplierById(supId).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + SupplierDTO s = resource.data; + updateViewState(state -> { + state.supCompany = safeText(s.getSupCompany()); + state.supFirstName = safeText(s.getSupContactFirstName()); + state.supLastName = safeText(s.getSupContactLastName()); + state.supEmail = safeText(s.getSupEmail()); + state.supPhone = safeText(s.getSupPhone()); + }); + } + result.setValue(resource); + }); + return result; } public LiveData> saveSupplier(SupplierDTO dto) { - if (isEditing) { + if (isEditing()) { dto.setSupId(supId); return repository.updateSupplier(supId, dto); - } else { - return repository.createSupplier(dto); } + return repository.createSupplier(dto); } public LiveData> deleteSupplier() { return repository.deleteSupplier(supId); } + + private String safeText(String value) { + return value == null ? "" : value.trim(); + } + + private void updateViewState(Action action) { + ViewState current = viewState.getValue(); + if (current != null) { + action.run(current); + viewState.setValue(current); + } + } + + private interface Action { + void run(T target); + } + + public static class ViewState { + public boolean isEditing = false; + public boolean isDeleteVisible = false; + public boolean isSupIdVisible = false; + public boolean isFieldsEnabled = true; + public String modeTitle = "Add Supplier"; + public String saveButtonText = "Add"; + public String supCompany = ""; + public String supFirstName = ""; + public String supLastName = ""; + public String supEmail = ""; + public String supPhone = ""; + } } diff --git a/android/app/src/main/res/layout/fragment_adoption_detail.xml b/android/app/src/main/res/layout/fragment_adoption_detail.xml index 0608f96f..a5170113 100644 --- a/android/app/src/main/res/layout/fragment_adoption_detail.xml +++ b/android/app/src/main/res/layout/fragment_adoption_detail.xml @@ -99,6 +99,21 @@ android:layout_height="wrap_content" android:layout_marginBottom="16dp"/> + + + + + - - - - Date: Fri, 10 Apr 2026 02:58:14 -0600 Subject: [PATCH 3/8] fixed spinners to populate the correct pets in edit mode for adoptions --- .../example/petstoremobile/api/PetApi.java | 3 + .../AdoptionDetailFragment.java | 4 +- .../AppointmentDetailFragment.java | 83 ++++++++++++------- .../repositories/PetRepository.java | 7 ++ .../viewmodels/AdoptionDetailViewModel.java | 6 +- .../AppointmentDetailViewModel.java | 7 +- 6 files changed, 75 insertions(+), 35 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java index 24250c4c..d13a5ed4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -44,6 +44,9 @@ public interface PetApi { @GET("api/v1/dropdowns/adoption-pets") Call> getAdoptionPets(); + @GET("api/v1/dropdowns/pets") + Call> getPetDropdowns(); + // Get pet by id @GET("api/v1/pets/{id}") Call getPetById(@Path("id") Long id); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 0e936edf..3c63819d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -54,7 +54,9 @@ public class AdoptionDetailFragment extends Fragment { setupSpinners(); setupDatePicker(); observeViewModel(); - viewModel.loadInitialFormData(); + Bundle args = getArguments(); + boolean isEditing = args != null && args.containsKey("adoptionId"); + viewModel.loadInitialFormData(isEditing); handleArguments(); binding.btnAdoptionBack.setOnClickListener(v -> navigateBack()); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index bb2b9d58..adfea86c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -25,6 +25,8 @@ import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.AppointmentDetailViewModel; +import java.util.List; + import dagger.hilt.android.AndroidEntryPoint; /** @@ -35,12 +37,6 @@ public class AppointmentDetailFragment extends Fragment { private FragmentAppointmentDetailBinding binding; - private long preselectedPetId = -1; - private long preselectedServiceId = -1; - private long preselectedCustomerId = -1; - 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}; @@ -118,27 +114,42 @@ public class AppointmentDetailFragment extends Fragment { } private void setupDatePicker() { - binding.etAppointmentDate.setOnClickListener(v -> + binding.etAppointmentDate.setOnClickListener(v -> UIUtils.showDatePicker(requireContext(), binding.etAppointmentDate, this::notifyDateTimeStatusChange)); } private void observeViewModel() { viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); - - viewModel.getCustomers().observe(getViewLifecycleOwner(), list -> - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, list, DropdownDTO::getLabel, "-- Select Customer --", preselectedCustomerId, DropdownDTO::getId)); - - viewModel.getStores().observe(getViewLifecycleOwner(), list -> - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, list, DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId)); - - viewModel.getServices().observe(getViewLifecycleOwner(), list -> - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, list, ServiceDTO::getServiceName, "-- Select Service --", preselectedServiceId, ServiceDTO::getServiceId)); - viewModel.getCustomerPets().observe(getViewLifecycleOwner(), list -> - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, list, DropdownDTO::getLabel, "-- Select Pet --", preselectedPetId, DropdownDTO::getId)); + viewModel.getCustomers().observe(getViewLifecycleOwner(), list -> { + AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + Long id = state != null ? state.selectedCustomerId : null; + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, list, DropdownDTO::getLabel, "-- Select Customer --", id, DropdownDTO::getId); + }); - viewModel.getStoreEmployees().observe(getViewLifecycleOwner(), list -> - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, list, DropdownDTO::getLabel, "-- Select Staff --", preselectedStaffId, DropdownDTO::getId)); + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + Long id = state != null ? state.selectedStoreId : null; + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, list, DropdownDTO::getLabel, "-- Select Store --", id, DropdownDTO::getId); + }); + + viewModel.getServices().observe(getViewLifecycleOwner(), list -> { + AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + Long id = state != null ? state.selectedServiceId : null; + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, list, ServiceDTO::getServiceName, "-- Select Service --", id, ServiceDTO::getServiceId); + }); + + viewModel.getCustomerPets().observe(getViewLifecycleOwner(), list -> { + AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + Long id = state != null ? state.selectedPetId : null; + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, list, DropdownDTO::getLabel, "-- Select Pet --", id, DropdownDTO::getId); + }); + + viewModel.getStoreEmployees().observe(getViewLifecycleOwner(), list -> { + AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue(); + Long id = state != null ? state.selectedStaffId : null; + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, list, DropdownDTO::getLabel, "-- Select Staff --", id, DropdownDTO::getId); + }); } private void setLoading(boolean loading) { @@ -155,7 +166,7 @@ public class AppointmentDetailFragment extends Fragment { 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); - + UIUtils.setFieldEnabled(state.isCustomerEnabled, binding.spinnerCustomer, binding.tvLabelCustomer); UIUtils.setFieldEnabled(state.isStoreEnabled, binding.spinnerStore, binding.tvLabelStore); UIUtils.setFieldEnabled(state.isPetEnabled, binding.spinnerPet, binding.tvLabelPet); @@ -171,6 +182,27 @@ public class AppointmentDetailFragment extends Fragment { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, state.availableStatuses); SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, current); + // Re-populate dropdown spinners with current selected IDs from ViewState + List customers = viewModel.getCustomers().getValue(); + if (customers != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, + customers, DropdownDTO::getLabel, "-- Select Customer --", state.selectedCustomerId, DropdownDTO::getId); + + List stores = viewModel.getStores().getValue(); + if (stores != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, + stores, DropdownDTO::getLabel, "-- Select Store --", state.selectedStoreId, DropdownDTO::getId); + + List services = viewModel.getServices().getValue(); + if (services != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, + services, ServiceDTO::getServiceName, "-- Select Service --", state.selectedServiceId, ServiceDTO::getServiceId); + + List pets = viewModel.getCustomerPets().getValue(); + if (pets != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, + pets, DropdownDTO::getLabel, "-- Select Pet --", state.selectedPetId, DropdownDTO::getId); + + List staff = viewModel.getStoreEmployees().getValue(); + if (staff != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, + staff, DropdownDTO::getLabel, "-- Select Staff --", state.selectedStaffId, DropdownDTO::getId); + isUpdatingUI = false; } @@ -200,20 +232,15 @@ public class AppointmentDetailFragment extends Fragment { setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { AppointmentDTO a = resource.data; - preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; - preselectedServiceId = a.getServiceId() != null ? a.getServiceId() : -1; - preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; - preselectedStoreId = a.getStoreId() != null ? a.getStoreId() : -1; - preselectedStaffId = a.getEmployeeId() != null ? a.getEmployeeId() : -1; - binding.etAppointmentDate.setText(a.getAppointmentDate()); parseAndSetTimeSpinners(a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00"); - String status = a.getAppointmentStatus(); if (status != null && !status.isEmpty()) { SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, DateTimeUtils.formatStatusFromBackend(status)); } notifyDateTimeStatusChange(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load appointment", Toast.LENGTH_SHORT).show(); } }); } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index 623a4daa..df7e4e5d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -47,6 +47,13 @@ public class PetRepository extends BaseRepository { return executeCall(petApi.getAdoptionPets()); } + /** + * Retrieves all pets from the dropdowns API. + */ + public LiveData>> getPetDropdowns() { + return executeCall(petApi.getPetDropdowns()); + } + /** * Retrieves a specific pet by its ID from the API. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java index 6a8ae492..a744f92c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java @@ -69,12 +69,12 @@ public class AdoptionDetailViewModel extends ViewModel { state.isAdoptionIdVisible = isEditing; state.isDeleteVisible = isEditing; state.isPetEnabled = isEditing; - state.isEmployeeEnabled = isEditing; + state.isEmployeeEnabled = false; }); } - public void loadInitialFormData() { - petRepository.getAdoptionPets().observeForever(r -> { + public void loadInitialFormData(boolean isEditing) { + (isEditing ? petRepository.getPetDropdowns() : petRepository.getAdoptionPets()).observeForever(r -> { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { petList.setValue(r.data); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java index 685b645a..e05d261a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java @@ -41,6 +41,7 @@ public class AppointmentDetailViewModel extends ViewModel { private final MutableLiveData viewState = new MutableLiveData<>(new ViewState()); private long appointmentId = -1; + private boolean isOriginallyCancel = false; private Long currentCustomerId; private Long currentStoreId; private Long currentPetId; @@ -229,6 +230,7 @@ public class AppointmentDetailViewModel extends ViewModel { repository.getAppointmentById(appointmentId).observeForever(resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { AppointmentDTO a = resource.data; + isOriginallyCancel = "CANCELLED".equalsIgnoreCase(a.getAppointmentStatus()); currentCustomerId = a.getCustomerId(); currentStoreId = a.getStoreId(); currentPetId = a.getPetId(); @@ -279,9 +281,8 @@ public class AppointmentDetailViewModel extends ViewModel { updateViewState(s -> { s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus); boolean isPast = DateTimeUtils.isDateTimeInPast(date, time); - boolean isCancelled = "Cancelled".equalsIgnoreCase(currentStatus); - if (isCancelled) { + if (isOriginallyCancel) { s.isPast = true; setAllFieldsEnabled(s, false); s.isStatusEnabled = false; @@ -311,7 +312,7 @@ public class AppointmentDetailViewModel extends ViewModel { 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 (isOriginallyCancel) return new String[]{"Cancelled"}; if (DateTimeUtils.isDateTimeInPast(date, currentTime)) return new String[]{"Completed", "Missed"}; return new String[]{"Booked", "Cancelled"}; } From 49ee40b91283f9f442b80d77310338830e653b27 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Fri, 10 Apr 2026 04:31:10 -0600 Subject: [PATCH 4/8] Added so adoption status can be missed and fixed adoption bugs for andriod --- .../petstoremobile/api/ProductApi.java | 9 + .../listfragments/ProductFragment.java | 8 +- .../AdoptionDetailFragment.java | 43 ++-- .../AppointmentDetailFragment.java | 4 +- .../InventoryDetailFragment.java | 10 +- .../ProductDetailFragment.java | 10 +- .../repositories/PetRepository.java | 7 + .../repositories/ProductRepository.java | 17 ++ .../petstoremobile/utils/DateTimeUtils.java | 25 +++ .../viewmodels/AdoptionDetailViewModel.java | 190 +++++++++++++++--- .../AppointmentDetailViewModel.java | 11 + .../viewmodels/InventoryDetailViewModel.java | 10 +- .../viewmodels/ProductDetailViewModel.java | 11 +- .../viewmodels/ProductListViewModel.java | 10 +- .../res/layout/fragment_adoption_detail.xml | 30 +-- .../backend/service/AdoptionService.java | 6 +- 16 files changed, 317 insertions(+), 84 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java index 1d46107b..8aeb596e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java @@ -1,11 +1,14 @@ package com.example.petstoremobile.api; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import okhttp3.MultipartBody; import retrofit2.Call; import retrofit2.http.*; +import java.util.List; + public interface ProductApi { String PRODUCT_IMAGE_PATH = "api/v1/products/%d/image"; @@ -35,4 +38,10 @@ public interface ProductApi { @DELETE("api/v1/products/{id}/image") Call deleteProductImage(@Path("id") Long id); + + @GET("api/v1/dropdowns/products") + Call> getProductDropdowns(); + + @GET("api/v1/dropdowns/categories") + Call> getCategoryDropdowns(); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 84f076a2..566f4ee7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -16,7 +16,7 @@ import android.view.ViewGroup; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductAdapter; import com.example.petstoremobile.databinding.FragmentProductBinding; -import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; @@ -74,7 +74,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc viewModel.getCategories().observe(getViewLifecycleOwner(), list -> { SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, list, - CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId); + DropdownDTO::getLabel, "All Categories", -1L, DropdownDTO::getId); }); viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { @@ -111,9 +111,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc if (query.isEmpty()) query = null; Long categoryId = null; - List categories = viewModel.getCategories().getValue(); + List categories = viewModel.getCategories().getValue(); if (binding.spinnerCategory.getSelectedItemPosition() > 0 && categories != null && !categories.isEmpty()) { - categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId(); + categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getId(); } viewModel.loadProducts(query, categoryId); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 3c63819d..1e109213 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -32,8 +32,7 @@ public class AdoptionDetailFragment extends Fragment { private FragmentAdoptionDetailBinding binding; private AdoptionDetailViewModel viewModel; - - private final String[] STATUSES = {"Pending", "Completed", "Cancelled"}; + private boolean isUpdatingUI = false; @Override public void onCreate(Bundle savedInstanceState) { @@ -109,9 +108,7 @@ public class AdoptionDetailFragment extends Fragment { } private void setupSpinners() { - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES); - - UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, new String[]{}); binding.spinnerAdoptionCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override @@ -130,10 +127,22 @@ public class AdoptionDetailFragment extends Fragment { @Override public void onNothingSelected(AdapterView parent) {} }); + + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAdoptionPet, p -> viewModel.onPetSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAdoptionStatus, p -> notifyDateStatusChange()); } private void setupDatePicker() { - binding.etAdoptionDate.setOnClickListener(v -> UIUtils.showDatePicker(requireContext(), binding.etAdoptionDate, null)); + binding.etAdoptionDate.setOnClickListener(v -> + UIUtils.showDatePicker(requireContext(), binding.etAdoptionDate, this::notifyDateStatusChange)); + } + + private void notifyDateStatusChange() { + if (isUpdatingUI) return; + String date = binding.etAdoptionDate.getText().toString(); + Object selected = binding.spinnerAdoptionStatus.getSelectedItem(); + String status = selected != null ? selected.toString() : ""; + viewModel.onDateChanged(date, status); } private void handleArguments() { @@ -157,14 +166,25 @@ public class AdoptionDetailFragment extends Fragment { } private void applyViewState(AdoptionDetailViewModel.ViewState state) { + isUpdatingUI = true; + binding.tvAdoptionMode.setText(state.modeTitle); binding.tvAdoptionId.setText(DateTimeUtils.formatId(viewModel.getAdoptionId())); binding.tvAdoptionId.setVisibility(state.isAdoptionIdVisible ? View.VISIBLE : View.GONE); binding.btnDeleteAdoption.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); binding.btnSaveAdoption.setText(state.saveButtonText); + binding.btnSaveAdoption.setVisibility(state.isSaveVisible ? View.VISIBLE : View.GONE); + UIUtils.setViewsEnabled(state.isCustomerEnabled, binding.spinnerAdoptionCustomer); UIUtils.setViewsEnabled(state.isPetEnabled, binding.spinnerAdoptionPet); + UIUtils.setViewsEnabled(state.isStoreEnabled, binding.spinnerAdoptionStore); UIUtils.setViewsEnabled(state.isEmployeeEnabled, binding.spinnerAdoptionEmployee); + UIUtils.setViewsEnabled(state.isDateEnabled, binding.etAdoptionDate); + UIUtils.setViewsEnabled(state.isFeeEnabled, binding.etAdoptionFee); + UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAdoptionStatus); + + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, state.availableStatuses); + SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, state.selectedStatus); if (!state.adoptionDate.isEmpty()) { binding.etAdoptionDate.setText(state.adoptionDate); @@ -172,9 +192,6 @@ public class AdoptionDetailFragment extends Fragment { if (!state.adoptionFee.isEmpty()) { binding.etAdoptionFee.setText(state.adoptionFee); } - if (!state.adoptionStatus.isEmpty()) { - SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, state.adoptionStatus); - } // Re-populate spinners with updated preselected IDs List pets = viewModel.getPetList().getValue(); @@ -192,6 +209,8 @@ public class AdoptionDetailFragment extends Fragment { List employees = viewModel.getEmployeeList().getValue(); if (employees != null) SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, employees, DropdownDTO::getLabel, "-- Select Staff --", state.selectedEmployeeId, DropdownDTO::getId); + + isUpdatingUI = false; } private void saveAdoption() { @@ -203,8 +222,7 @@ public class AdoptionDetailFragment extends Fragment { BigDecimal fee = BigDecimal.ZERO; String feeStr = binding.etAdoptionFee.getText().toString().trim(); if (!feeStr.isEmpty()) { - if (!InputValidator.isPositiveDecimal(binding.etAdoptionFee, "Adoption Fee")) return; - fee = new BigDecimal(feeStr); + try { fee = new BigDecimal(feeStr); } catch (NumberFormatException ignored) {} } DropdownDTO customer = viewModel.getCustomerList().getValue().get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); @@ -217,7 +235,8 @@ public class AdoptionDetailFragment extends Fragment { } String adoptionDate = binding.etAdoptionDate.getText().toString().trim(); - String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; + Object selectedStatus = binding.spinnerAdoptionStatus.getSelectedItem(); + String status = selectedStatus != null ? selectedStatus.toString().toUpperCase() : ""; AdoptionDTO dto = new AdoptionDTO( pet.getId(), customer.getId(), employeeId, store.getId(), adoptionDate, status, fee); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index adfea86c..6c524d75 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -177,10 +177,8 @@ public class AppointmentDetailFragment extends Fragment { UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerMinute); UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAppointmentStatus); - Object selected = binding.spinnerAppointmentStatus.getSelectedItem(); - String current = selected != null ? selected.toString() : ""; SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, state.availableStatuses); - SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, current); + SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, state.selectedStatus); // Re-populate dropdown spinners with current selected IDs from ViewState List customers = viewModel.getCustomers().getValue(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index ee52c960..75f1a6b3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -92,7 +92,7 @@ public class InventoryDetailFragment extends Fragment { if (resource == null) return; setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - viewModel.setProductList(resource.data.getContent()); + viewModel.setProductList(resource.data); } }); } @@ -105,8 +105,8 @@ public class InventoryDetailFragment extends Fragment { private void refreshProductSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, viewModel.getProductList().getValue(), - ProductDTO::getProdName, "-- Select Product --", - preselectedProductId, ProductDTO::getProdId); + DropdownDTO::getLabel, "-- Select Product --", + preselectedProductId, DropdownDTO::getId); } private void handleArguments() { @@ -156,9 +156,9 @@ public class InventoryDetailFragment extends Fragment { int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim()); DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); - ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); + DropdownDTO product = viewModel.getProductList().getValue().get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); - InventoryDTO request = new InventoryDTO(product.getProdId(), store.getId(), quantity); + InventoryDTO request = new InventoryDTO(product.getId(), store.getId(), quantity); setButtonsEnabled(false); viewModel.saveInventory(request).observe(getViewLifecycleOwner(), resource -> { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index c580905b..5d89e305 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -115,7 +115,7 @@ public class ProductDetailFragment extends Fragment { if (resource == null) return; setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - viewModel.setCategoryList(resource.data.getContent()); + viewModel.setCategoryList(resource.data); } }); } @@ -128,8 +128,8 @@ public class ProductDetailFragment extends Fragment { private void updateCategorySpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, viewModel.getCategoryList().getValue(), - CategoryDTO::getCategoryName, "-- Select Category --", - preselectedCategoryId, CategoryDTO::getCategoryId); + DropdownDTO::getLabel, "-- Select Category --", + preselectedCategoryId, DropdownDTO::getId); } @Override @@ -248,8 +248,8 @@ public class ProductDetailFragment extends Fragment { String desc = binding.etProductDesc.getText().toString().trim(); BigDecimal price = new BigDecimal(binding.etProductPrice.getText().toString().trim()); - CategoryDTO category = viewModel.getCategoryList().getValue().get(binding.spinnerProductCategory.getSelectedItemPosition() - 1); - ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); + DropdownDTO category = viewModel.getCategoryList().getValue().get(binding.spinnerProductCategory.getSelectedItemPosition() - 1); + ProductDTO dto = new ProductDTO(name, category.getId(), desc, price); viewModel.saveProduct(dto).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index df7e4e5d..b1eda73f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -54,6 +54,13 @@ public class PetRepository extends BaseRepository { return executeCall(petApi.getPetDropdowns()); } + /** + * Retrieves available pets for a specific store. + */ + public LiveData>> getAvailablePetsByStore(Long storeId) { + return executeCall(petApi.getAllPets(0, 200, null, "available", null, storeId, null, "petName")); + } + /** * Retrieves a specific pet by its ID from the API. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java index a6d32336..636e1430 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java @@ -3,10 +3,13 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.ProductApi; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.Resource; +import java.util.List; + import javax.inject.Inject; import javax.inject.Singleton; @@ -70,4 +73,18 @@ public class ProductRepository extends BaseRepository { public LiveData> deleteProductImage(Long id) { return executeCall(productApi.deleteProductImage(id)); } + + /** + * Retrieves a list of product dropdowns from the API. + */ + public LiveData>> getProductDropdowns() { + return executeCall(productApi.getProductDropdowns()); + } + + /** + * Retrieves a list of category dropdowns from the API. + */ + public LiveData>> getCategoryDropdowns() { + return executeCall(productApi.getCategoryDropdowns()); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java index 11e8f4fd..45867cd8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/DateTimeUtils.java @@ -63,6 +63,31 @@ public class DateTimeUtils { } } + /** + * Checks if a given date is strictly before today (today and future return false). + * format: date = "YYYY-MM-DD" + */ + public static boolean isDateBeforeToday(String date) { + if (date == null || date.isEmpty()) return false; + try { + String[] parts = date.split("-"); + Calendar today = Calendar.getInstance(); + today.set(Calendar.HOUR_OF_DAY, 0); + today.set(Calendar.MINUTE, 0); + today.set(Calendar.SECOND, 0); + today.set(Calendar.MILLISECOND, 0); + + Calendar selected = Calendar.getInstance(); + selected.set(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]) - 1, + Integer.parseInt(parts[2]), 0, 0, 0); + selected.set(Calendar.MILLISECOND, 0); + return selected.before(today); + } 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" diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java index a744f92c..c15766f9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java @@ -10,10 +10,13 @@ import com.example.petstoremobile.repositories.AdoptionRepository; import com.example.petstoremobile.repositories.CustomerRepository; import com.example.petstoremobile.repositories.PetRepository; 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.Arrays; import java.util.List; +import java.util.Locale; import javax.inject.Inject; @@ -68,17 +71,30 @@ public class AdoptionDetailViewModel extends ViewModel { state.saveButtonText = isEditing ? "Save" : "Add"; state.isAdoptionIdVisible = isEditing; state.isDeleteVisible = isEditing; - state.isPetEnabled = isEditing; - state.isEmployeeEnabled = false; + state.isFeeEnabled = false; // fee is always read-only + if (!isEditing) { + state.isCustomerEnabled = true; + state.isStoreEnabled = true; + state.isPetEnabled = false; // until customer selected + state.isEmployeeEnabled = false; // until store selected + state.isDateEnabled = true; + state.isStatusEnabled = true; + state.availableStatuses = new String[]{"Pending"}; + state.selectedStatus = "Pending"; + } else { + // edit: date-based logic applied after load + state.isCustomerEnabled = false; + state.isStoreEnabled = false; + state.isPetEnabled = false; + state.isEmployeeEnabled = false; + state.isDateEnabled = false; + state.isStatusEnabled = false; + } }); } public void loadInitialFormData(boolean isEditing) { - (isEditing ? petRepository.getPetDropdowns() : petRepository.getAdoptionPets()).observeForever(r -> { - if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { - petList.setValue(r.data); - } - }); + // Pets are loaded dynamically based on store selection; no pre-load needed. customerRepository.getCustomerDropdowns().observeForever(r -> { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { customerList.setValue(r.data); @@ -95,13 +111,7 @@ public class AdoptionDetailViewModel extends ViewModel { List list = customerList.getValue(); Long customerId = (position > 0 && list != null && position <= list.size()) ? list.get(position - 1).getId() : null; - updateViewState(state -> { - state.selectedCustomerId = customerId; - state.isPetEnabled = customerId != null; - if (customerId == null && !state.isEditing) { - state.selectedPetId = null; - } - }); + updateViewState(state -> state.selectedCustomerId = customerId); } public void onStoreSelected(int position) { @@ -110,19 +120,70 @@ public class AdoptionDetailViewModel extends ViewModel { Long storeId = list.get(position - 1).getId(); updateViewState(state -> { state.selectedStoreId = storeId; - state.isEmployeeEnabled = true; + if (!state.isCancelled && !state.isEditing) { + state.isEmployeeEnabled = true; + state.isPetEnabled = true; + } }); loadEmployeesForStore(storeId); + if (!isEditing()) loadAvailablePetsByStore(storeId); } else { employeeList.setValue(new ArrayList<>()); + petList.setValue(new ArrayList<>()); updateViewState(state -> { state.selectedStoreId = null; state.selectedEmployeeId = null; + state.selectedPetId = null; state.isEmployeeEnabled = false; + state.isPetEnabled = false; }); } } + public void onPetSelected(int position) { + List list = petList.getValue(); + if (position > 0 && list != null && position <= list.size()) { + Long petId = list.get(position - 1).getId(); + updateViewState(s -> s.selectedPetId = petId); + loadPetPrice(petId); + } else { + updateViewState(s -> { + s.selectedPetId = null; + s.adoptionFee = ""; + }); + } + } + + private void loadAvailablePetsByStore(Long storeId) { + petRepository.getAvailablePetsByStore(storeId).observeForever(r -> { + if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { + List dropdowns = new ArrayList<>(); + for (com.example.petstoremobile.dtos.PetDTO pet : r.data.getContent()) { + dropdowns.add(new DropdownDTO(pet.getPetId(), pet.getPetName())); + } + petList.setValue(dropdowns); + } + }); + } + + private void loadPetPrice(Long petId) { + petRepository.getPetById(petId).observeForever(r -> { + if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { + com.example.petstoremobile.dtos.PetDTO pet = r.data; + // In edit mode, add the pet to the list so the spinner can display its name + if (isEditing()) { + List single = new ArrayList<>(); + single.add(new DropdownDTO(pet.getPetId(), pet.getPetName())); + petList.setValue(single); + } + if (pet.getPetPrice() != null) { + String price = String.format(Locale.getDefault(), "%.2f", pet.getPetPrice()); + updateViewState(s -> s.adoptionFee = price); + } + } + }); + } + private void loadEmployeesForStore(Long storeId) { storeRepository.getStoreEmployees(storeId).observeForever(r -> { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { @@ -131,26 +192,99 @@ public class AdoptionDetailViewModel extends ViewModel { }); } + /** + * Called when the date or status changes in the UI. Applies date-based field enabling. + */ + public void onDateChanged(String date, String currentStatus) { + updateViewState(s -> { + if (s.isCancelled) return; + s.availableStatuses = calculateAvailableStatuses(s.isEditing, date); + List available = Arrays.asList(s.availableStatuses); + if (!currentStatus.isEmpty() && available.contains(currentStatus)) { + s.selectedStatus = currentStatus; + } else if (!available.contains(s.selectedStatus) && s.availableStatuses.length > 0) { + s.selectedStatus = s.availableStatuses[0]; + } + + if (!s.isEditing) return; // add mode: field enabling handled separately + + boolean isPast = DateTimeUtils.isDateBeforeToday(date); + if (isPast) { + setAllEditableFieldsEnabled(s, false); + s.isStatusEnabled = true; + } else if (!date.isEmpty()) { + setAllEditableFieldsEnabled(s, false); + s.isEmployeeEnabled = true; + s.isDateEnabled = true; + s.isStatusEnabled = true; + } + }); + } + + private String[] calculateAvailableStatuses(boolean isEditing, String date) { + if (!isEditing) return new String[]{"Pending"}; + if (date == null || date.isEmpty()) return new String[]{}; + if (DateTimeUtils.isDateBeforeToday(date)) return new String[]{"Completed", "Missed"}; + return new String[]{"Pending", "Cancelled"}; + } + + /** Disables all editable fields (fee is always disabled separately). */ + private void setAllEditableFieldsEnabled(ViewState s, boolean enabled) { + s.isCustomerEnabled = enabled; + s.isStoreEnabled = enabled; + s.isPetEnabled = enabled; + s.isEmployeeEnabled = enabled; + s.isDateEnabled = enabled; + // fee never editable + } + public LiveData> loadAdoption() { MutableLiveData> result = new MutableLiveData<>(); adoptionRepository.getAdoptionById(adoptionId).observeForever(resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { AdoptionDTO a = resource.data; + String formattedStatus = DateTimeUtils.formatStatusFromBackend( + a.getAdoptionStatus() != null ? a.getAdoptionStatus() : ""); + String adoptionDate = a.getAdoptionDate() != null ? a.getAdoptionDate() : ""; + updateViewState(state -> { state.selectedPetId = a.getPetId() != null ? a.getPetId() : -1; state.selectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; state.selectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1; state.selectedEmployeeId = a.getEmployeeId() != null ? a.getEmployeeId() : -1; - state.adoptionDate = a.getAdoptionDate() != null ? a.getAdoptionDate() : ""; - state.adoptionFee = a.getAdoptionFee() != null ? a.getAdoptionFee().toString() : ""; - state.adoptionStatus = a.getAdoptionStatus() != null ? a.getAdoptionStatus() : ""; - state.isPetEnabled = state.selectedCustomerId != null && state.selectedCustomerId != -1; - state.isEmployeeEnabled = state.selectedStoreId != null && state.selectedStoreId != -1; + state.adoptionDate = adoptionDate; + state.adoptionFee = a.getAdoptionFee() != null ? a.getAdoptionFee().toPlainString() : ""; + state.selectedStatus = formattedStatus; + state.adoptionStatus = formattedStatus; + + if ("Cancelled".equalsIgnoreCase(formattedStatus)) { + state.isCancelled = true; + state.isCustomerEnabled = false; + state.isStoreEnabled = false; + state.isPetEnabled = false; + state.isEmployeeEnabled = false; + state.isStatusEnabled = false; + state.isDateEnabled = false; + state.isFeeEnabled = false; + state.isSaveVisible = false; + state.availableStatuses = new String[]{"Cancelled"}; + } else { + state.availableStatuses = calculateAvailableStatuses(true, adoptionDate); + boolean isPast = DateTimeUtils.isDateBeforeToday(adoptionDate); + if (isPast) { + setAllEditableFieldsEnabled(state, false); + state.isStatusEnabled = true; + } else if (!adoptionDate.isEmpty()) { + setAllEditableFieldsEnabled(state, false); + state.isEmployeeEnabled = true; + state.isDateEnabled = true; + state.isStatusEnabled = true; + } + } }); - if (a.getSourceStoreId() != null) { - loadEmployeesForStore(a.getSourceStoreId()); - } + if (a.getSourceStoreId() != null) loadEmployeesForStore(a.getSourceStoreId()); + if (a.getPetId() != null) loadPetPrice(a.getPetId()); } result.setValue(resource); }); @@ -173,7 +307,6 @@ public class AdoptionDetailViewModel extends ViewModel { public LiveData> getStoreList() { return storeList; } public LiveData> getEmployeeList() { return employeeList; } - // Kept for backward-compatibility with any remaining direct calls public void setEmployeeList(List list) { employeeList.setValue(list); } private void updateViewState(Action action) { @@ -192,8 +325,17 @@ public class AdoptionDetailViewModel extends ViewModel { public boolean isEditing = false; public boolean isAdoptionIdVisible = false; public boolean isDeleteVisible = false; + public boolean isCancelled = false; public boolean isPetEnabled = false; public boolean isEmployeeEnabled = false; + public boolean isCustomerEnabled = true; + public boolean isStoreEnabled = true; + public boolean isStatusEnabled = true; + public boolean isDateEnabled = true; + public boolean isFeeEnabled = false; // always read-only + public boolean isSaveVisible = true; + public String[] availableStatuses = new String[]{}; + public String selectedStatus = ""; public String modeTitle = "Add Adoption"; public String saveButtonText = "Add"; public Long selectedPetId = null; diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java index e05d261a..18c20e24 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java @@ -237,12 +237,14 @@ public class AppointmentDetailViewModel extends ViewModel { currentServiceId = a.getServiceId(); currentStaffId = a.getEmployeeId(); + String formattedStatus = DateTimeUtils.formatStatusFromBackend(a.getAppointmentStatus()); updateViewState(s -> { s.selectedCustomerId = currentCustomerId; s.selectedStoreId = currentStoreId; s.selectedPetId = currentPetId; s.selectedServiceId = currentServiceId; s.selectedStaffId = currentStaffId; + s.selectedStatus = formattedStatus; }); if (currentCustomerId != null) loadPetsForCustomer(currentCustomerId); @@ -280,6 +282,13 @@ public class AppointmentDetailViewModel extends ViewModel { public void onDateOrTimeChanged(String date, String time, String currentStatus) { updateViewState(s -> { s.availableStatuses = calculateAvailableStatuses(s.isEditing, date, time, currentStatus); + // Keep selectedStatus if still valid; prefer explicit currentStatus from UI if valid + java.util.List available = java.util.Arrays.asList(s.availableStatuses); + if (!currentStatus.isEmpty() && available.contains(currentStatus)) { + s.selectedStatus = currentStatus; + } else if (!available.contains(s.selectedStatus) && s.availableStatuses.length > 0) { + s.selectedStatus = s.availableStatuses[0]; + } boolean isPast = DateTimeUtils.isDateTimeInPast(date, time); if (isOriginallyCancel) { @@ -349,6 +358,7 @@ public class AppointmentDetailViewModel extends ViewModel { s.isPetEnabled = false; // until customer selected s.isStaffEnabled = false; // until store selected s.availableStatuses = new String[]{"Booked"}; + s.selectedStatus = "Booked"; } }); } @@ -392,6 +402,7 @@ public class AppointmentDetailViewModel extends ViewModel { public boolean isTimeEnabled = true; public boolean isStatusEnabled = true; public String[] availableStatuses = new String[]{}; + public String selectedStatus = ""; // Selected IDs public Long selectedCustomerId = null; diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java index a76785af..c872ea49 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java @@ -30,7 +30,7 @@ public class InventoryDetailViewModel extends ViewModel { private boolean isEditing = false; private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); - private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); @Inject public InventoryDetailViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository, ProductRepository productRepository) { @@ -55,8 +55,8 @@ public class InventoryDetailViewModel extends ViewModel { return storeRepository.getStoreDropdowns(); } - public LiveData>> loadProducts() { - return productRepository.getAllProducts(null, null, 0, 500, "prodName"); + public LiveData>> loadProducts() { + return productRepository.getProductDropdowns(); } public LiveData> saveInventory(InventoryDTO dto) { @@ -74,6 +74,6 @@ public class InventoryDetailViewModel extends ViewModel { public void setStoreList(List list) { storeList.setValue(list); } public LiveData> getStoreList() { return storeList; } - public void setProductList(List list) { productList.setValue(list); } - public LiveData> getProductList() { return productList; } + public void setProductList(List list) { productList.setValue(list); } + public LiveData> getProductList() { return productList; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java index 54ff65b5..c1ac5fdd 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.repositories.CategoryRepository; @@ -24,7 +25,7 @@ public class ProductDetailViewModel extends ViewModel { private final ProductRepository productRepository; private final CategoryRepository categoryRepository; - private final MutableLiveData> categoryList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> categoryList = new MutableLiveData<>(new ArrayList<>()); private long prodId = -1; private boolean isEditing = false; @@ -47,8 +48,8 @@ public class ProductDetailViewModel extends ViewModel { return isEditing; } - public LiveData>> loadCategories() { - return categoryRepository.getAllCategories(0, 100); + public LiveData>> loadCategories() { + return productRepository.getCategoryDropdowns(); } public LiveData> loadProduct() { @@ -75,11 +76,11 @@ public class ProductDetailViewModel extends ViewModel { return productRepository.deleteProductImage(prodId); } - public void setCategoryList(List list) { + public void setCategoryList(List list) { categoryList.setValue(list); } - public LiveData> getCategoryList() { + public LiveData> getCategoryList() { return categoryList; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java index ecd2d238..6d89ab6f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java @@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; -import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.repositories.CategoryRepository; import com.example.petstoremobile.repositories.ProductRepository; @@ -23,7 +23,7 @@ public class ProductListViewModel extends ViewModel { private final CategoryRepository categoryRepository; private final MutableLiveData> products = new MutableLiveData<>(new ArrayList<>()); - private final MutableLiveData> categories = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> categories = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); @Inject @@ -33,7 +33,7 @@ public class ProductListViewModel extends ViewModel { } public LiveData> getProducts() { return products; } - public LiveData> getCategories() { return categories; } + public LiveData> getCategories() { return categories; } public LiveData getIsLoading() { return isLoading; } public void loadProducts(String query, Long categoryId) { @@ -51,9 +51,9 @@ public class ProductListViewModel extends ViewModel { } public void loadCategories() { - categoryRepository.getAllCategories(0, 100).observeForever(resource -> { + productRepository.getCategoryDropdowns().observeForever(resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - categories.setValue(resource.data.getContent()); + categories.setValue(resource.data); } }); } diff --git a/android/app/src/main/res/layout/fragment_adoption_detail.xml b/android/app/src/main/res/layout/fragment_adoption_detail.xml index a5170113..0bbacddc 100644 --- a/android/app/src/main/res/layout/fragment_adoption_detail.xml +++ b/android/app/src/main/res/layout/fragment_adoption_detail.xml @@ -84,21 +84,6 @@ android:layout_height="wrap_content" android:layout_marginBottom="16dp"/> - - - - - + + + + + Date: Fri, 10 Apr 2026 05:03:36 -0600 Subject: [PATCH 5/8] added Analytics filter --- .../petstoremobile/dtos/EmployeeDTO.java | 114 ++++++-- .../listfragments/AnalyticsFragment.java | 170 +++++++++--- .../listfragments/StaffFragment.java | 2 +- .../detailfragments/StaffDetailFragment.java | 69 ++++- .../viewmodels/AnalyticsViewModel.java | 160 +++++++++-- .../viewmodels/StaffDetailViewModel.java | 26 +- .../main/res/layout/fragment_analytics.xml | 261 +++++++++++++++++- .../main/res/layout/fragment_staff_detail.xml | 31 ++- 8 files changed, 730 insertions(+), 103 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java index 21577a25..f29b4beb 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java @@ -2,8 +2,7 @@ package com.example.petstoremobile.dtos; public class EmployeeDTO { - private long EmployeeId; - private Long userId; + private Long id; private String username; private String firstName; private String lastName; @@ -11,16 +10,18 @@ public class EmployeeDTO { private String email; private String phone; private String role; + private String staffRole; private Boolean active; - private String createAt; + private Integer loyaltyPoints; + private Long primaryStoreId; + private String createdAt; private String updatedAt; + private String password; - - // Constructor for create and update the employee - + public EmployeeDTO() {} public EmployeeDTO(String username, String password, String firstName, String lastName, - String email, String phone, String role, boolean active) { + String email, String phone, String role, String staffRole, boolean active, Long primaryStoreId) { this.username = username; this.password = password; this.firstName = firstName; @@ -28,75 +29,128 @@ public class EmployeeDTO { this.email = email; this.phone = phone; this.role = role; + this.staffRole = staffRole; this.active = active; - } - // password field for request only - private String password; - - - public long getEmployeeId() { - - return EmployeeId; + this.primaryStoreId = primaryStoreId; } - public Long getUserId() { + public Long getId() { + return id; + } - return userId; + public void setId(Long id) { + this.id = id; } public String getUsername() { - return username; } - public String getFirstName() { + public void setUsername(String username) { + this.username = username; + } + public String getFirstName() { return firstName; } - public String getLastName() { + public void setFirstName(String firstName) { + this.firstName = firstName; + } + public String getLastName() { return lastName; } - public String getFullName() { + public void setLastName(String lastName) { + this.lastName = lastName; + } + public String getFullName() { return fullName; } - public String getEmail() { + public void setFullName(String fullName) { + this.fullName = fullName; + } + public String getEmail() { return email; } - public String getPhone() { + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { return phone; } - public String getRole() { + public void setPhone(String phone) { + this.phone = phone; + } + public String getRole() { return role; } - public Boolean getActive() { + public void setRole(String role) { + this.role = role; + } + public String getStaffRole() { + return staffRole; + } + + public void setStaffRole(String staffRole) { + this.staffRole = staffRole; + } + + public Boolean getActive() { return active; } - public String getCreateAt() { + public void setActive(Boolean active) { + this.active = active; + } - return createAt; + public Integer getLoyaltyPoints() { + return loyaltyPoints; + } + + public void setLoyaltyPoints(Integer loyaltyPoints) { + this.loyaltyPoints = loyaltyPoints; + } + + public Long getPrimaryStoreId() { + return primaryStoreId; + } + + public void setPrimaryStoreId(Long primaryStoreId) { + this.primaryStoreId = primaryStoreId; + } + + public String getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; } public String getUpdatedAt() { - return updatedAt; } - public String getPassword() { + public void setUpdatedAt(String updatedAt) { + this.updatedAt = updatedAt; + } + public String getPassword() { return password; } - + public void setPassword(String password) { + this.password = password; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java index 44719127..b8656b60 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.example.petstoremobile.databinding.FragmentAnalyticsBinding; +import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.AnalyticsViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -20,6 +21,10 @@ public class AnalyticsFragment extends Fragment { private FragmentAnalyticsBinding binding; private AnalyticsViewModel viewModel; + private boolean filtersExpanded = false; + + private static final String[] TOP_N_OPTIONS = {"5", "10", "15", "20"}; + private static final int[] TOP_N_VALUES = { 5, 10, 15, 20 }; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -27,19 +32,114 @@ public class AnalyticsFragment extends Fragment { binding = FragmentAnalyticsBinding.inflate(inflater, container, false); viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class); + setupFilterPanel(); observeViewModel(); viewModel.loadAnalytics(); binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics()); - UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this); return binding.getRoot(); } + // Filter Panel + + private void setupFilterPanel() { + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerTopN, TOP_N_OPTIONS); + + // Toggle expand/collapse + binding.rowFilterHeader.setOnClickListener(v -> toggleFilters()); + + // Date pickers + binding.etFilterStartDate.setOnClickListener(v -> + UIUtils.showDatePicker(requireContext(), binding.etFilterStartDate, this::updateFilterSummary)); + binding.etFilterEndDate.setOnClickListener(v -> + UIUtils.showDatePicker(requireContext(), binding.etFilterEndDate, this::updateFilterSummary)); + + // Quick presets + binding.btnPresetToday.setOnClickListener(v -> applyPreset(0, 0)); + binding.btnPreset7D.setOnClickListener(v -> applyPreset(-6, 0)); + binding.btnPreset30D.setOnClickListener(v -> applyPreset(-29, 0)); + binding.btnPreset3M.setOnClickListener(v -> applyPreset(-89, 0)); + binding.btnPreset1Y.setOnClickListener(v -> applyPreset(-364, 0)); + binding.btnPresetAll.setOnClickListener(v -> { + binding.etFilterStartDate.setText(""); + binding.etFilterEndDate.setText(""); + updateFilterSummary(); + }); + + binding.btnFilterApply.setOnClickListener(v -> applyFiltersFromUI()); + binding.btnFilterReset.setOnClickListener(v -> resetFilters()); + } + + private void toggleFilters() { + filtersExpanded = !filtersExpanded; + binding.llFilterContent.setVisibility(filtersExpanded ? View.VISIBLE : View.GONE); + binding.tvFilterToggleIcon.setText(filtersExpanded ? "▲" : "▼"); + } + + private void applyPreset(int startOffset, int endOffset) { + binding.etFilterStartDate.setText(getDateString(startOffset)); + binding.etFilterEndDate.setText(getDateString(endOffset)); + updateFilterSummary(); + applyFiltersFromUI(); + } + + private void applyFiltersFromUI() { + AnalyticsViewModel.FilterState filter = new AnalyticsViewModel.FilterState(); + filter.startDate = binding.etFilterStartDate.getText().toString().trim(); + filter.endDate = binding.etFilterEndDate.getText().toString().trim(); + + Object pm = binding.spinnerFilterPayment.getSelectedItem(); + filter.paymentMethod = pm != null ? pm.toString() : "All"; + + int topNPos = binding.spinnerTopN.getSelectedItemPosition(); + filter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5; + + updateFilterSummary(); + viewModel.applyFilter(filter); + } + + private void resetFilters() { + binding.etFilterStartDate.setText(""); + binding.etFilterEndDate.setText(""); + binding.spinnerTopN.setSelection(0); + // Reset payment method to "All" + SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, "All"); + updateFilterSummary(); + viewModel.resetFilter(); + } + + private void updateFilterSummary() { + String start = binding.etFilterStartDate.getText().toString().trim(); + String end = binding.etFilterEndDate.getText().toString().trim(); + if (start.isEmpty() && end.isEmpty()) { + binding.tvFilterSummary.setText("All time"); + } else if (start.isEmpty()) { + binding.tvFilterSummary.setText("Up to " + shortDate(end)); + } else if (end.isEmpty()) { + binding.tvFilterSummary.setText("From " + shortDate(start)); + } else { + binding.tvFilterSummary.setText(shortDate(start) + " – " + shortDate(end)); + } + } + + private String shortDate(String date) { + return (date != null && date.length() >= 10) ? date.substring(5) : date; + } + + private String getDateString(int offsetDays) { + Calendar c = Calendar.getInstance(); + c.add(Calendar.DAY_OF_YEAR, offsetDays); + return String.format(Locale.US, "%04d-%02d-%02d", + c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH)); + } + + // ViewModel Observation + private void observeViewModel() { viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay); - + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); if (loading) { @@ -53,6 +153,15 @@ public class AnalyticsFragment extends Fragment { viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> { if (error != null) showError(error); }); + + viewModel.getAvailablePaymentMethods().observe(getViewLifecycleOwner(), methods -> { + if (methods == null || methods.isEmpty()) return; + String currentSelection = binding.spinnerFilterPayment.getSelectedItem() != null + ? binding.spinnerFilterPayment.getSelectedItem().toString() : "All"; + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerFilterPayment, + methods.toArray(new String[0])); + SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, currentSelection); + }); } @Override @@ -61,10 +170,12 @@ public class AnalyticsFragment extends Fragment { binding = null; } + // Display + private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) { if (data == null) return; - // Summary + // Summary cards binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP)); binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions)); binding.tvAvgTransaction.setText("$" + data.avgTransaction); @@ -73,11 +184,12 @@ public class AnalyticsFragment extends Fragment { // Top Revenue Products binding.llTopRevenue.removeAllViews(); if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) { - BigDecimal maxRevenue = data.topRevenueProducts.get(0).getValue(); - if (maxRevenue.compareTo(BigDecimal.ZERO) == 0) maxRevenue = BigDecimal.ONE; + BigDecimal maxRev = data.topRevenueProducts.get(0).getValue(); + if (maxRev.compareTo(BigDecimal.ZERO) == 0) maxRev = BigDecimal.ONE; for (Map.Entry e : data.topRevenueProducts) { - addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35"); + addBarRow(binding.llTopRevenue, e.getKey(), + "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxRev.floatValue(), "#ff6b35"); } } else { addEmptyRow(binding.llTopRevenue, "No data"); @@ -99,15 +211,13 @@ public class AnalyticsFragment extends Fragment { // Payment Methods binding.llPaymentMethods.removeAllViews(); if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) { - int maxPayment = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1); - String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" }; + int maxPay = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1); + String[] payColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" }; int ci = 0; for (Map.Entry e : data.paymentMethodStats) { addBarRow(binding.llPaymentMethods, e.getKey(), e.getValue() + " transactions", - (float) e.getValue() / maxPayment, - paymentColors[ci % paymentColors.length]); - ci++; + (float) e.getValue() / maxPay, payColors[ci++ % payColors.length]); } } else { addEmptyRow(binding.llPaymentMethods, "No data"); @@ -116,36 +226,37 @@ public class AnalyticsFragment extends Fragment { // Employee Performance binding.llEmployeePerformance.removeAllViews(); if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) { - BigDecimal maxEmp = data.employeePerformance.get(data.employeePerformance.size() - 1).getValue(); + BigDecimal maxEmp = data.employeePerformance.get(0).getValue(); if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; - maxEmp = data.employeePerformance.get(0).getValue(); - if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; - for (Map.Entry e : data.employeePerformance) { addBarRow(binding.llEmployeePerformance, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxEmp.floatValue(), - "#1a759f"); + e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f"); } } else { addEmptyRow(binding.llEmployeePerformance, "No data"); } // Daily Revenue + binding.tvDailyRevenueTitle.setText(data.dailyRevenueTitle); binding.llDailyRevenue.removeAllViews(); if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) { - BigDecimal maxDaily = data.dailyRevenue.stream().map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE); + BigDecimal maxDaily = data.dailyRevenue.stream() + .map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE); if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE; for (Map.Entry e : data.dailyRevenue) { String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey(); addBarRow(binding.llDailyRevenue, label, "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxDaily.floatValue(), - "#ff6b35"); + e.getValue().floatValue() / maxDaily.floatValue(), "#ff6b35"); } + } else { + addEmptyRow(binding.llDailyRevenue, "No data"); } } + // Chart Helpers + private void addBarRow(LinearLayout parent, String label, String value, float ratio, String color) { if (getContext() == null) return; LinearLayout row = new LinearLayout(getContext()); @@ -156,8 +267,7 @@ public class AnalyticsFragment extends Fragment { labelRow.setOrientation(LinearLayout.HORIZONTAL); TextView tvLabel = new TextView(getContext()); - tvLabel.setLayoutParams(new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvLabel.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); tvLabel.setText(label); tvLabel.setTextColor(Color.parseColor("#444441")); tvLabel.setTextSize(13f); @@ -172,22 +282,19 @@ public class AnalyticsFragment extends Fragment { labelRow.addView(tvValue); LinearLayout barBg = new LinearLayout(getContext()); - LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, 12); + LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 12); bgParams.setMargins(0, 4, 0, 0); barBg.setLayoutParams(bgParams); barBg.setBackgroundColor(Color.parseColor("#EEEEEE")); + float safeRatio = Math.max(0f, Math.min(1f, ratio)); View barFill = new View(getContext()); - LinearLayout.LayoutParams fillParams = new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.MATCH_PARENT, ratio); - barFill.setLayoutParams(fillParams); + barFill.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, safeRatio)); barFill.setBackgroundColor(Color.parseColor(color)); barBg.addView(barFill); View spacer = new View(getContext()); - spacer.setLayoutParams(new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - ratio)); + spacer.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - safeRatio)); barBg.addView(spacer); row.addView(labelRow); @@ -205,8 +312,7 @@ public class AnalyticsFragment extends Fragment { } private void showError(String msg) { - if (getContext() == null || binding == null) - return; + if (getContext() == null || binding == null) return; binding.tvTotalRevenue.setText("Error"); binding.tvTotalTransactions.setText("—"); binding.tvAvgTransaction.setText("—"); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java index 8407d3f6..62ec931f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java @@ -76,7 +76,7 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye Bundle args = new Bundle(); if (position != -1) { EmployeeDTO e = staffList.get(position); - args.putLong("employeeId", e.getEmployeeId()); + args.putLong("employeeId", e.getId()); args.putString("username", e.getUsername() != null ? e.getUsername() : ""); args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : ""); args.putString("lastName", e.getLastName() != null ? e.getLastName() : ""); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java index 1c1bfc4e..3c1f81d6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java @@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentStaffDetailBinding; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.EmployeeDTO; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; @@ -16,6 +17,9 @@ import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.StaffDetailViewModel; import com.example.petstoremobile.utils.Resource; + +import java.util.List; + import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint @@ -25,8 +29,11 @@ public class StaffDetailFragment extends Fragment { private StaffDetailViewModel viewModel; private final String[] ROLES = {"STAFF", "ADMIN"}; + private final String[] STAFF_ROLES = {"STORE_MANAGER", "SALES_ASSOCIATE", "GROOMER", "VETERINARIAN"}; private final String[] STATUSES = {"Active", "Inactive"}; + private long preselectedStoreId = -1; + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -34,6 +41,8 @@ public class StaffDetailFragment extends Fragment { viewModel = new ViewModelProvider(this).get(StaffDetailViewModel.class); setupSpinners(); + observeViewModel(); + loadStores(); handleArguments(); binding.btnStaffBack.setOnClickListener(v -> navigateBack()); @@ -45,11 +54,30 @@ public class StaffDetailFragment extends Fragment { return binding.getRoot(); } + private void observeViewModel() { + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner()); + } + private void setupSpinners() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffRole, ROLES); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffType, STAFF_ROLES); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES); } + private void loadStores() { + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setStoreList(resource.data); + } + }); + } + + private void refreshStoreSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaffStore, viewModel.getStoreList().getValue(), + DropdownDTO::getLabel, "-- Select Store --", + preselectedStoreId, DropdownDTO::getId); + } + private void handleArguments() { Bundle a = getArguments(); if (a != null && a.getBoolean("isEditing", false)) { @@ -59,16 +87,9 @@ public class StaffDetailFragment extends Fragment { binding.tvStaffMode.setText("Edit Staff Account"); binding.tvStaffId.setText("ID: " + employeeId); binding.tvStaffId.setVisibility(View.VISIBLE); - binding.etStaffUsername.setText(a.getString("username", "")); - binding.etStaffFirstName.setText(a.getString("firstName", "")); - binding.etStaffLastName.setText(a.getString("lastName", "")); - binding.etStaffEmail.setText(a.getString("email", "")); - binding.etStaffPhone.setText(a.getString("phone", "")); binding.btnDeleteStaff.setVisibility(View.VISIBLE); - SpinnerUtils.setSelectionByValue(binding.spinnerStaffRole, a.getString("role", "STAFF")); - binding.spinnerStaffStatus.setSelection(a.getBoolean("active", true) ? 0 : 1); - + loadEmployeeData(employeeId); } else { viewModel.setEmployeeId(-1, false); binding.tvStaffMode.setText("Add Staff Account"); @@ -77,6 +98,29 @@ public class StaffDetailFragment extends Fragment { } } + private void loadEmployeeData(long id) { + viewModel.loadEmployee(id).observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + EmployeeDTO e = resource.data; + binding.etStaffUsername.setText(e.getUsername()); + binding.etStaffFirstName.setText(e.getFirstName()); + binding.etStaffLastName.setText(e.getLastName()); + binding.etStaffEmail.setText(e.getEmail()); + binding.etStaffPhone.setText(e.getPhone()); + + SpinnerUtils.setSelectionByValue(binding.spinnerStaffRole, e.getRole()); + SpinnerUtils.setSelectionByValue(binding.spinnerStaffType, e.getStaffRole()); + binding.spinnerStaffStatus.setSelection(Boolean.TRUE.equals(e.getActive()) ? 0 : 1); + + preselectedStoreId = e.getPrimaryStoreId() != null ? e.getPrimaryStoreId() : -1; + refreshStoreSpinner(); + } + } + }); + } + private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); @@ -100,6 +144,7 @@ public class StaffDetailFragment extends Fragment { if (!InputValidator.isNotEmpty(binding.etStaffLastName, "Last Name")) return; if (!InputValidator.isValidEmail(binding.etStaffEmail)) return; if (!InputValidator.isValidPhone(binding.etStaffPhone)) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerStaffStore, "Primary Store")) return; String username = binding.etStaffUsername.getText().toString().trim(); String password = binding.etStaffPassword.getText().toString().trim(); @@ -108,7 +153,11 @@ public class StaffDetailFragment extends Fragment { String email = binding.etStaffEmail.getText().toString().trim(); String phone = binding.etStaffPhone.getText().toString().trim(); String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()]; + String staffRole = STAFF_ROLES[binding.spinnerStaffType.getSelectedItemPosition()]; boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0; + + List stores = viewModel.getStoreList().getValue(); + Long storeId = stores.get(binding.spinnerStaffStore.getSelectedItemPosition() - 1).getId(); EmployeeDTO dto = new EmployeeDTO( username, @@ -118,7 +167,9 @@ public class StaffDetailFragment extends Fragment { email, phone, role, - active + staffRole, + active, + storeId ); viewModel.saveEmployee(dto).observe(getViewLifecycleOwner(), resource -> { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java index 76c039ac..6ebfb741 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java @@ -16,6 +16,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.TreeMap; @@ -30,6 +31,10 @@ public class AnalyticsViewModel extends ViewModel { private final MutableLiveData analyticsData = new MutableLiveData<>(); private final MutableLiveData isLoading = new MutableLiveData<>(false); private final MutableLiveData errorMessage = new MutableLiveData<>(); + private final MutableLiveData> availablePaymentMethods = new MutableLiveData<>(new ArrayList<>()); + + private List cachedSales = new ArrayList<>(); + private FilterState currentFilter = new FilterState(); @Inject public AnalyticsViewModel(SaleRepository saleRepository) { @@ -39,14 +44,17 @@ public class AnalyticsViewModel extends ViewModel { public LiveData getAnalyticsData() { return analyticsData; } public LiveData getIsLoading() { return isLoading; } public LiveData getErrorMessage() { return errorMessage; } + public LiveData> getAvailablePaymentMethods() { return availablePaymentMethods; } public void loadAnalytics() { isLoading.setValue(true); errorMessage.setValue(null); - saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc").observeForever(resource -> { + saleRepository.getAllSales(0, 2000, null, null, null, "saleDate,desc").observeForever(resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - computeAnalytics(resource.data.getContent()); + cachedSales = resource.data.getContent(); + derivePaymentMethods(); + applyCurrentFilter(); isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { errorMessage.setValue(resource.message); @@ -56,11 +64,53 @@ public class AnalyticsViewModel extends ViewModel { }); } - private void computeAnalytics(List sales) { + public void applyFilter(FilterState filter) { + currentFilter = filter; + applyCurrentFilter(); + } + + public void resetFilter() { + currentFilter = new FilterState(); + applyCurrentFilter(); + } + + private void applyCurrentFilter() { + List filtered = filterSales(cachedSales, currentFilter); + computeAnalytics(filtered, currentFilter); + } + + private void derivePaymentMethods() { + java.util.Set methods = new java.util.TreeSet<>(); + for (SaleDTO s : cachedSales) { + if (s.getPaymentMethod() != null && !s.getPaymentMethod().isEmpty()) { + methods.add(s.getPaymentMethod()); + } + } + List result = new ArrayList<>(); + result.add("All"); + result.addAll(methods); + availablePaymentMethods.setValue(result); + } + + private List filterSales(List sales, FilterState filter) { + List result = new ArrayList<>(); + for (SaleDTO s : sales) { + String date = s.getSaleDate() != null && s.getSaleDate().length() >= 10 + ? s.getSaleDate().substring(0, 10) : ""; + if (!filter.startDate.isEmpty() && !date.isEmpty() && date.compareTo(filter.startDate) < 0) continue; + if (!filter.endDate.isEmpty() && !date.isEmpty() && date.compareTo(filter.endDate) > 0) continue; + if (!filter.paymentMethod.equals("All") && !filter.paymentMethod.isEmpty()) { + if (!filter.paymentMethod.equalsIgnoreCase(s.getPaymentMethod())) continue; + } + result.add(s); + } + return result; + } + + private void computeAnalytics(List sales, FilterState filter) { List regularSales = new ArrayList<>(); for (SaleDTO s : sales) { - if (!Boolean.TRUE.equals(s.getIsRefund())) - regularSales.add(s); + if (!Boolean.TRUE.equals(s.getIsRefund())) regularSales.add(s); } AnalyticsData data = new AnalyticsData(); @@ -83,72 +133,127 @@ public class AnalyticsViewModel extends ViewModel { : BigDecimal.ZERO; data.totalItems = totalItems; - // Product Maps Map revenueByProduct = new LinkedHashMap<>(); Map quantityByProduct = new LinkedHashMap<>(); Map paymentCount = new LinkedHashMap<>(); Map employeeRevenue = new LinkedHashMap<>(); for (SaleDTO s : regularSales) { - // Payments String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown"; paymentCount.merge(method, 1, Integer::sum); - // Employee String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown"; if (s.getTotalAmount() != null) employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add); - // Items if (s.getItems() != null) { for (SaleDTO.SaleItemDTO item : s.getItems()) { String name = item.getProductName() != null ? item.getProductName() : "Unknown"; int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0; BigDecimal lineTotal = item.getUnitPrice() != null - ? item.getUnitPrice().multiply(BigDecimal.valueOf(qty)) - : BigDecimal.ZERO; + ? item.getUnitPrice().multiply(BigDecimal.valueOf(qty)) : BigDecimal.ZERO; revenueByProduct.merge(name, lineTotal, BigDecimal::add); quantityByProduct.merge(name, qty, Integer::sum); } } } - // Sort Top Revenue + int topN = filter.topN > 0 ? filter.topN : 5; + data.topRevenueProducts = new ArrayList<>(revenueByProduct.entrySet()); data.topRevenueProducts.sort((a, b) -> b.getValue().compareTo(a.getValue())); - if (data.topRevenueProducts.size() > 5) data.topRevenueProducts = data.topRevenueProducts.subList(0, 5); + if (data.topRevenueProducts.size() > topN) data.topRevenueProducts = data.topRevenueProducts.subList(0, topN); - // Sort Top Quantity data.topQuantityProducts = new ArrayList<>(quantityByProduct.entrySet()); data.topQuantityProducts.sort((a, b) -> b.getValue() - a.getValue()); - if (data.topQuantityProducts.size() > 5) data.topQuantityProducts = data.topQuantityProducts.subList(0, 5); + if (data.topQuantityProducts.size() > topN) data.topQuantityProducts = data.topQuantityProducts.subList(0, topN); - // Payment Stats data.paymentMethodStats = new ArrayList<>(paymentCount.entrySet()); + data.paymentMethodStats.sort((a, b) -> b.getValue() - a.getValue()); - // Employee Performance data.employeePerformance = new ArrayList<>(employeeRevenue.entrySet()); data.employeePerformance.sort((a, b) -> b.getValue().compareTo(a.getValue())); - // Daily Revenue (last 7 days) - Map dailyMap = new TreeMap<>(); - for (int i = 6; i >= 0; i--) { - Calendar day = Calendar.getInstance(); - day.add(Calendar.DAY_OF_YEAR, -i); - String key = String.format("%04d-%02d-%02d", - day.get(Calendar.YEAR), day.get(Calendar.MONTH) + 1, day.get(Calendar.DAY_OF_MONTH)); - dailyMap.put(key, BigDecimal.ZERO); + // Daily revenue display to filter date range, max 60 days + String rangeStart = filter.startDate; + String rangeEnd = filter.endDate; + if (rangeStart.isEmpty() && rangeEnd.isEmpty()) { + rangeEnd = todayString(0); + rangeStart = todayString(-6); + } else if (rangeStart.isEmpty()) { + rangeStart = shiftDate(rangeEnd, -6); + } else if (rangeEnd.isEmpty()) { + rangeEnd = todayString(0); } + + List dateRange = buildDateRange(rangeStart, rangeEnd, 60); + Map dailyMap = new TreeMap<>(); + for (String d : dateRange) dailyMap.put(d, BigDecimal.ZERO); for (SaleDTO s : regularSales) { if (s.getSaleDate() != null && s.getTotalAmount() != null) { - String date = s.getSaleDate().length() >= 10 ? s.getSaleDate().substring(0, 10) : s.getSaleDate(); - if (dailyMap.containsKey(date)) dailyMap.merge(date, s.getTotalAmount(), BigDecimal::add); + String d = s.getSaleDate().length() >= 10 ? s.getSaleDate().substring(0, 10) : s.getSaleDate(); + if (dailyMap.containsKey(d)) dailyMap.merge(d, s.getTotalAmount(), BigDecimal::add); } } data.dailyRevenue = new ArrayList<>(dailyMap.entrySet()); + data.dailyRevenueTitle = buildDailyTitle(filter, rangeStart, rangeEnd); analyticsData.setValue(data); } + private String todayString(int offsetDays) { + Calendar c = Calendar.getInstance(); + c.add(Calendar.DAY_OF_YEAR, offsetDays); + return String.format(Locale.US, "%04d-%02d-%02d", + c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH)); + } + + private String shiftDate(String date, int offsetDays) { + try { + String[] p = date.split("-"); + Calendar c = Calendar.getInstance(); + c.set(Integer.parseInt(p[0]), Integer.parseInt(p[1]) - 1, Integer.parseInt(p[2]), 0, 0, 0); + c.add(Calendar.DAY_OF_YEAR, offsetDays); + return String.format(Locale.US, "%04d-%02d-%02d", + c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH)); + } catch (Exception e) { + return date; + } + } + + private List buildDateRange(String start, String end, int maxDays) { + List dates = new ArrayList<>(); + try { + String[] sp = start.split("-"); + String[] ep = end.split("-"); + Calendar cur = Calendar.getInstance(); + cur.set(Integer.parseInt(sp[0]), Integer.parseInt(sp[1]) - 1, Integer.parseInt(sp[2]), 0, 0, 0); + Calendar endCal = Calendar.getInstance(); + endCal.set(Integer.parseInt(ep[0]), Integer.parseInt(ep[1]) - 1, Integer.parseInt(ep[2]), 0, 0, 0); + int count = 0; + while (!cur.after(endCal) && count < maxDays) { + dates.add(String.format(Locale.US, "%04d-%02d-%02d", + cur.get(Calendar.YEAR), cur.get(Calendar.MONTH) + 1, cur.get(Calendar.DAY_OF_MONTH))); + cur.add(Calendar.DAY_OF_YEAR, 1); + count++; + } + } catch (Exception ignored) {} + return dates; + } + + private String buildDailyTitle(FilterState filter, String rangeStart, String rangeEnd) { + if (filter.startDate.isEmpty() && filter.endDate.isEmpty()) return "Daily Revenue (Last 7 Days)"; + String s = rangeStart.length() >= 10 ? rangeStart.substring(5) : rangeStart; + String e = rangeEnd.length() >= 10 ? rangeEnd.substring(5) : rangeEnd; + return "Daily Revenue (" + s + " – " + e + ")"; + } + + public static class FilterState { + public String startDate = ""; + public String endDate = ""; + public String paymentMethod = "All"; + public int topN = 5; + } + public static class AnalyticsData { public BigDecimal totalRevenue; public int totalTransactions; @@ -159,5 +264,6 @@ public class AnalyticsViewModel extends ViewModel { public List> paymentMethodStats; public List> employeePerformance; public List> dailyRevenue; + public String dailyRevenueTitle = "Daily Revenue"; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java index 91162405..dffe47c3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java @@ -1,12 +1,17 @@ package com.example.petstoremobile.viewmodels; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.EmployeeDTO; import com.example.petstoremobile.repositories.EmployeeRepository; +import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.utils.Resource; +import java.util.List; + import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; @@ -14,12 +19,31 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class StaffDetailViewModel extends ViewModel { private final EmployeeRepository repository; + private final StoreRepository storeRepository; + private final MutableLiveData> storeList = new MutableLiveData<>(); private long employeeId = -1; private boolean isEditing = false; @Inject - public StaffDetailViewModel(EmployeeRepository repository) { + public StaffDetailViewModel(EmployeeRepository repository, StoreRepository storeRepository) { this.repository = repository; + this.storeRepository = storeRepository; + } + + public LiveData>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData> getStoreList() { + return storeList; + } + + public void setStoreList(List list) { + storeList.setValue(list); + } + + public LiveData> loadEmployee(long id) { + return repository.getEmployeeById(id); } public void setEmployeeId(long id, boolean isEditing) { diff --git a/android/app/src/main/res/layout/fragment_analytics.xml b/android/app/src/main/res/layout/fragment_analytics.xml index d06b36ec..19a7a51b 100644 --- a/android/app/src/main/res/layout/fragment_analytics.xml +++ b/android/app/src/main/res/layout/fragment_analytics.xml @@ -45,9 +45,267 @@ + + + + + + + + + + + + + + + + + + + + +