Added so adoption status can be missed and fixed adoption bugs for andriod

This commit is contained in:
Alex
2026-04-10 04:31:10 -06:00
parent 3bb399e6e4
commit 49ee40b912
16 changed files with 317 additions and 84 deletions

View File

@@ -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<Void> deleteProductImage(@Path("id") Long id);
@GET("api/v1/dropdowns/products")
Call<List<DropdownDTO>> getProductDropdowns();
@GET("api/v1/dropdowns/categories")
Call<List<DropdownDTO>> getCategoryDropdowns();
}

View File

@@ -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<CategoryDTO> categories = viewModel.getCategories().getValue();
List<DropdownDTO> 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);

View File

@@ -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<DropdownDTO> pets = viewModel.getPetList().getValue();
@@ -192,6 +209,8 @@ public class AdoptionDetailFragment extends Fragment {
List<DropdownDTO> 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);

View File

@@ -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<DropdownDTO> customers = viewModel.getCustomers().getValue();

View File

@@ -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 -> {

View File

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

View File

@@ -54,6 +54,13 @@ public class PetRepository extends BaseRepository {
return executeCall(petApi.getPetDropdowns());
}
/**
* Retrieves available pets for a specific store.
*/
public LiveData<Resource<PageResponse<PetDTO>>> 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.
*/

View File

@@ -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<Resource<Void>> deleteProductImage(Long id) {
return executeCall(productApi.deleteProductImage(id));
}
/**
* Retrieves a list of product dropdowns from the API.
*/
public LiveData<Resource<List<DropdownDTO>>> getProductDropdowns() {
return executeCall(productApi.getProductDropdowns());
}
/**
* Retrieves a list of category dropdowns from the API.
*/
public LiveData<Resource<List<DropdownDTO>>> getCategoryDropdowns() {
return executeCall(productApi.getCategoryDropdowns());
}
}

View File

@@ -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"

View File

@@ -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.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<DropdownDTO> 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;
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<DropdownDTO> 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<DropdownDTO> 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<DropdownDTO> 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<String> 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<Resource<AdoptionDTO>> loadAdoption() {
MutableLiveData<Resource<AdoptionDTO>> 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<List<DropdownDTO>> getStoreList() { return storeList; }
public LiveData<List<DropdownDTO>> getEmployeeList() { return employeeList; }
// Kept for backward-compatibility with any remaining direct calls
public void setEmployeeList(List<DropdownDTO> list) { employeeList.setValue(list); }
private void updateViewState(Action<ViewState> 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;

View File

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

View File

@@ -30,7 +30,7 @@ public class InventoryDetailViewModel extends ViewModel {
private boolean isEditing = false;
private final MutableLiveData<List<DropdownDTO>> storeList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<ProductDTO>> productList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> 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<Resource<PageResponse<ProductDTO>>> loadProducts() {
return productRepository.getAllProducts(null, null, 0, 500, "prodName");
public LiveData<Resource<List<DropdownDTO>>> loadProducts() {
return productRepository.getProductDropdowns();
}
public LiveData<Resource<InventoryDTO>> saveInventory(InventoryDTO dto) {
@@ -74,6 +74,6 @@ public class InventoryDetailViewModel extends ViewModel {
public void setStoreList(List<DropdownDTO> list) { storeList.setValue(list); }
public LiveData<List<DropdownDTO>> getStoreList() { return storeList; }
public void setProductList(List<ProductDTO> list) { productList.setValue(list); }
public LiveData<List<ProductDTO>> getProductList() { return productList; }
public void setProductList(List<DropdownDTO> list) { productList.setValue(list); }
public LiveData<List<DropdownDTO>> getProductList() { return productList; }
}

View File

@@ -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<List<CategoryDTO>> categoryList = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> 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<Resource<PageResponse<CategoryDTO>>> loadCategories() {
return categoryRepository.getAllCategories(0, 100);
public LiveData<Resource<List<DropdownDTO>>> loadCategories() {
return productRepository.getCategoryDropdowns();
}
public LiveData<Resource<ProductDTO>> loadProduct() {
@@ -75,11 +76,11 @@ public class ProductDetailViewModel extends ViewModel {
return productRepository.deleteProductImage(prodId);
}
public void setCategoryList(List<CategoryDTO> list) {
public void setCategoryList(List<DropdownDTO> list) {
categoryList.setValue(list);
}
public LiveData<List<CategoryDTO>> getCategoryList() {
public LiveData<List<DropdownDTO>> getCategoryList() {
return categoryList;
}
}

View File

@@ -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<List<ProductDTO>> products = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<CategoryDTO>> categories = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<DropdownDTO>> categories = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
@Inject
@@ -33,7 +33,7 @@ public class ProductListViewModel extends ViewModel {
}
public LiveData<List<ProductDTO>> getProducts() { return products; }
public LiveData<List<CategoryDTO>> getCategories() { return categories; }
public LiveData<List<DropdownDTO>> getCategories() { return categories; }
public LiveData<Boolean> 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);
}
});
}

View File

@@ -84,21 +84,6 @@
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Pet -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pet"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerAdoptionPet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Store -->
<TextView
android:layout_width="wrap_content"
@@ -129,6 +114,21 @@
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Pet -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pet"
android:textColor="@color/text_dark"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<Spinner
android:id="@+id/spinnerAdoptionPet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
<!-- Adoption Date -->
<TextView
android:layout_width="wrap_content"

View File

@@ -29,6 +29,7 @@ public class AdoptionService {
private static final String ADOPTION_STATUS_PENDING = "Pending";
private static final String ADOPTION_STATUS_COMPLETED = "Completed";
private static final String ADOPTION_STATUS_CANCELLED = "Cancelled";
private static final String ADOPTION_STATUS_MISSED = "Missed";
private static final String PET_STATUS_AVAILABLE = "Available";
private static final String PET_STATUS_ADOPTED = "Adopted";
@@ -218,7 +219,10 @@ public class AdoptionService {
if (ADOPTION_STATUS_CANCELLED.equalsIgnoreCase(trimmedStatus)) {
return ADOPTION_STATUS_CANCELLED;
}
throw new IllegalArgumentException("Adoption status must be Pending, Completed, or Cancelled");
if (ADOPTION_STATUS_MISSED.equalsIgnoreCase(trimmedStatus)) {
return ADOPTION_STATUS_MISSED;
}
throw new IllegalArgumentException("Adoption status must be Pending, Completed, Cancelled, or Missed");
}
private void validatePetAvailability(Pet pet, Long adoptionId, Long currentPetId) {