From 4fc33fedf42502d8032a731c5149e78509443607 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:22:25 -0600 Subject: [PATCH 01/46] fix minor bugs and UI inconsistancy --- .../adapters/EmployeeAdapter.java | 53 ++++--- .../petstoremobile/adapters/SaleAdapter.java | 8 +- .../example/petstoremobile/api/SaleApi.java | 5 +- .../listfragments/AnalyticsFragment.java | 2 +- .../fragments/listfragments/PetFragment.java | 3 + .../fragments/listfragments/SaleFragment.java | 135 +++++++++++----- .../listfragments/StaffFragment.java | 18 ++- .../AppointmentDetailFragment.java | 2 +- .../detailfragments/PetDetailFragment.java | 17 ++ .../ProductDetailFragment.java | 7 +- .../detailfragments/RefundFragment.java | 2 +- .../PetProfileFragment.java | 27 +++- .../repositories/SaleRepository.java | 4 +- .../viewmodels/SaleViewModel.java | 4 +- .../main/res/layout/fragment_pet_profile.xml | 35 ++++- .../app/src/main/res/layout/fragment_sale.xml | 110 ++++++++++--- .../src/main/res/layout/fragment_staff.xml | 66 ++++++-- .../app/src/main/res/layout/item_employee.xml | 148 ++++++++++-------- android/app/src/main/res/layout/item_sale.xml | 137 +++++++++------- android/app/src/res/layout/item_sale.xml | 101 ++++++++++++ .../backend/controller/SaleController.java | 3 +- .../backend/repository/SaleRepository.java | 7 +- .../petshop/backend/service/SaleService.java | 17 +- 23 files changed, 665 insertions(+), 246 deletions(-) create mode 100644 android/app/src/res/layout/item_sale.xml diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java index e860e30e..3dca0d7c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java @@ -2,10 +2,12 @@ package com.example.petstoremobile.adapters; import android.graphics.Color; import android.view.*; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemEmployeeBinding; import com.example.petstoremobile.dtos.EmployeeDTO; import java.util.List; @@ -24,47 +26,48 @@ public class EmployeeAdapter extends RecyclerView.Adapter listener.onEmployeeClick(position)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java index 503186be..deca5624 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java @@ -1,11 +1,12 @@ package com.example.petstoremobile.adapters; -import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.ItemSaleBinding; import com.example.petstoremobile.dtos.SaleDTO; import java.util.List; @@ -53,11 +54,10 @@ public class SaleAdapter extends RecyclerView.Adapter listener.onSaleClick(position)); diff --git a/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java index 72bfd8f4..c7261b16 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java @@ -15,7 +15,10 @@ public interface SaleApi { @GET("api/v1/sales") Call> getAllSales( @Query("page") int page, - @Query("size") int size); + @Query("size") int size, + @Query("query") String query, + @Query("paymentMethod") String paymentMethod, + @Query("sortBy") String sortBy); @GET("api/v1/sales/{id}") Call getSaleById(@Path("id") Long id); 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 c9eca2dd..a1e49971 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 @@ -75,7 +75,7 @@ public class AnalyticsFragment extends Fragment { tvAvgTransaction.setText("..."); tvTotalItems.setText("..."); - RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 1000) + RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 1000, null, null, null) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index 578022c3..096f1212 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -19,6 +19,7 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.PetAdapter; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentPetBinding; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.StoreDTO; @@ -48,6 +49,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private BulkDeleteHandler bulkDeleteHandler; @Inject @Named("baseUrl") String baseUrl; + @Inject TokenManager tokenManager; /** * Initializes the fragment and its associated ViewModels. @@ -272,6 +274,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private void setupRecyclerView() { adapter = new PetAdapter(petList, this); adapter.setBaseUrl(baseUrl); + adapter.setToken(tokenManager.getToken()); binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPets.setAdapter(adapter); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index dc882fff..5908a5d8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -7,11 +7,14 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.text.Editable; import android.text.TextWatcher; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SaleAdapter; @@ -19,9 +22,11 @@ import com.example.petstoremobile.databinding.FragmentSaleBinding; import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.SaleViewModel; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import dagger.hilt.android.AndroidEntryPoint; @@ -29,12 +34,19 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { + private static final String TAG = "SaleFragment"; + private static final int PAGE_SIZE = 20; + private FragmentSaleBinding binding; - private List saleList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private final List saleList = new ArrayList<>(); private SaleAdapter adapter; private SaleViewModel saleViewModel; + // Pagination + private int currentPage = 0; + private boolean isLastPage = false; + private boolean isLoading = false; + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -49,8 +61,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis setupRecyclerView(); setupSearch(); + setupPaymentMethodFilter(); setupSwipeRefresh(); - loadSales(); + setupFilterToggle(); + loadSales(true); binding.btnHamburger.setOnClickListener(v -> { Fragment parent = getParentFragment(); @@ -69,6 +83,28 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis NavHostFragment.findNavController(this).navigate(R.id.nav_refund)); } + private void setupFilterToggle() { + binding.btnToggleFilter.setOnClickListener(v -> { + if (binding.layoutFilter.getVisibility() == View.GONE) { + binding.layoutFilter.setVisibility(View.VISIBLE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); + } else { + binding.layoutFilter.setVisibility(View.GONE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); + binding.etSearchSale.setText(""); + binding.spinnerPaymentMethod.setSelection(0); + } + }); + } + + private void setupPaymentMethodFilter() { + List paymentMethods = Arrays.asList("Cash", "Card"); + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerPaymentMethod, paymentMethods, + s -> s, "All Payments", null, s -> (long) s.hashCode()); + + SpinnerUtils.setupFilterSpinner(binding.spinnerPaymentMethod, () -> loadSales(true)); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -76,9 +112,24 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } private void setupRecyclerView() { - adapter = new SaleAdapter(filteredList, this); + adapter = new SaleAdapter(saleList, this); binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewSales.setAdapter(adapter); + + binding.recyclerViewSales.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewSales.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + loadSales(false); + } + } + }); } private void setupSearch() { @@ -87,50 +138,64 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis @Override public void afterTextChanged(Editable s) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { - filterSales(s.toString()); + loadSales(true); } }); } - private void filterSales(String query) { - filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(saleList); - } else { - String lower = query.toLowerCase(); - for (SaleDTO s : saleList) { - if ((s.getEmployeeName() != null && s.getEmployeeName().toLowerCase().contains(lower)) - || (s.getSaleDate() != null && s.getSaleDate().toLowerCase().contains(lower)) - || (s.getPaymentMethod() != null && s.getPaymentMethod().toLowerCase().contains(lower)) - || (s.getSaleId() != null && String.valueOf(s.getSaleId()).contains(lower))) { - filteredList.add(s); - } - } - } - if (adapter != null) adapter.notifyDataSetChanged(); - } - private void setupSwipeRefresh() { - binding.swipeRefreshSale.setOnRefreshListener(() -> { - loadSales(); - }); + binding.swipeRefreshSale.setOnRefreshListener(() -> loadSales(true)); } - private void loadSales() { - saleViewModel.getAllSales(0, 200).observe(getViewLifecycleOwner(), resource -> { - binding.swipeRefreshSale.setRefreshing(false); - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - saleList.clear(); - saleList.addAll(resource.data.getContent()); - filterSales(binding.etSearchSale.getText() != null - ? binding.etSearchSale.getText().toString() : ""); + private void loadSales(boolean reset) { + if (isLoading) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + String query = binding.etSearchSale != null ? binding.etSearchSale.getText().toString().trim() : ""; + if (query.isEmpty()) query = null; + + String paymentMethod = null; + if (binding.spinnerPaymentMethod.getSelectedItemPosition() > 0) { + paymentMethod = (String) binding.spinnerPaymentMethod.getSelectedItem(); + } + + saleViewModel.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, "saleDate,desc").observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + + switch (resource.status) { + case LOADING: + isLoading = true; + binding.swipeRefreshSale.setRefreshing(true); + break; + case SUCCESS: + isLoading = false; + binding.swipeRefreshSale.setRefreshing(false); + if (resource.data != null) { + if (reset) saleList.clear(); + saleList.addAll(resource.data.getContent()); + adapter.notifyDataSetChanged(); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + } + break; + case ERROR: + isLoading = false; + binding.swipeRefreshSale.setRefreshing(false); + Log.e(TAG, "Error loading sales: " + resource.message); + Toast.makeText(getContext(), "Failed to load sales: " + resource.message, Toast.LENGTH_SHORT).show(); + break; } }); } @Override public void onSaleClick(int position) { - SaleDTO sale = filteredList.get(position); + if (position < 0 || position >= saleList.size()) return; + SaleDTO sale = saleList.get(position); Bundle args = new Bundle(); if (sale.getSaleId() != null) { args.putLong("saleId", sale.getSaleId()); 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 1c8fcf4e..63879d9c 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 @@ -29,12 +29,15 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye private EmployeeAdapter adapter; private SwipeRefreshLayout swipeRefresh; private EditText etSearch; + private LinearLayout layoutFilter; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_staff, container, false); + layoutFilter = view.findViewById(R.id.layoutFilterStaff); + setupRecyclerView(view); setupSearch(view); setupSwipeRefresh(view); @@ -46,6 +49,15 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye ImageButton hamburger = view.findViewById(R.id.btnHamburgerStaff); hamburger.setOnClickListener(v -> openDrawer()); + ImageButton btnToggleFilter = view.findViewById(R.id.btnToggleFilterStaff); + btnToggleFilter.setOnClickListener(v -> { + if (layoutFilter.getVisibility() == View.VISIBLE) { + layoutFilter.setVisibility(View.GONE); + } else { + layoutFilter.setVisibility(View.VISIBLE); + } + }); + return view; } @@ -112,8 +124,10 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye employeeList.addAll(r.body().getContent()); filter(etSearch != null ? etSearch.getText().toString() : ""); } else { - Toast.makeText(getContext(), "Failed to load staff", - Toast.LENGTH_SHORT).show(); + if (getContext() != null) { + Toast.makeText(getContext(), "Failed to load staff", + Toast.LENGTH_SHORT).show(); + } } } public void onFailure(Call> c, Throwable t) { 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 7757f156..1081462a 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 @@ -415,7 +415,7 @@ public class AppointmentDetailFragment extends Fragment { } else if (errorMessage.toLowerCase().contains("not available")) { showNoAvailabilityDialog(); } else { - Toast.makeText(getContext(), "Operation failed", Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), errorMessage, Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(getContext(), "Something went wrong", Toast.LENGTH_SHORT).show(); 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 62d8bf42..ea08d16d 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 @@ -220,6 +220,13 @@ public class PetDetailFragment extends Fragment { binding.tvPetId.setText("ID: " + petId); binding.tvPetId.setVisibility(View.VISIBLE); binding.btnDeletePet.setVisibility(View.VISIBLE); + + // Disable species and breed fields in edit mode + binding.etPetSpecies.setEnabled(false); + binding.etPetBreed.setEnabled(false); + binding.etPetSpecies.setAlpha(0.5f); + binding.etPetBreed.setAlpha(0.5f); + loadPetData(); } else { // Pet is being added @@ -229,6 +236,12 @@ public class PetDetailFragment extends Fragment { binding.tvPetId.setVisibility(View.GONE); binding.btnDeletePet.setVisibility(View.GONE); binding.btnSavePet.setText("Add"); + + // Enable species and breed fields in edit mode + binding.etPetSpecies.setEnabled(true); + binding.etPetBreed.setEnabled(true); + binding.etPetSpecies.setAlpha(1.0f); + binding.etPetBreed.setAlpha(1.0f); } } @@ -334,16 +347,20 @@ public class PetDetailFragment extends Fragment { if ("Available".equalsIgnoreCase(status)) { binding.spinnerCustomer.setSelection(0); binding.spinnerCustomer.setEnabled(false); + binding.spinnerCustomer.setAlpha(0.5f); } else { binding.spinnerCustomer.setEnabled(true); + binding.spinnerCustomer.setAlpha(1.0f); } //Disable the store spinner if the status is "Owned" if ("Owned".equalsIgnoreCase(status)) { binding.spinnerStore.setSelection(0); binding.spinnerStore.setEnabled(false); + binding.spinnerStore.setAlpha(0.5f); } else { binding.spinnerStore.setEnabled(true); + binding.spinnerStore.setAlpha(1.0f); } } 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 48a988d2..d2527d71 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 @@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.example.petstoremobile.R; import com.example.petstoremobile.api.*; import com.example.petstoremobile.api.auth.TokenManager; @@ -73,7 +74,11 @@ public class ProductDetailFragment extends Fragment { @Override public void onImagePicked(Uri uri) { photoUri = uri; - Glide.with(ProductDetailFragment.this).load(uri).into(binding.ivProductImage); + Glide.with(ProductDetailFragment.this) + .load(uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .into(binding.ivProductImage); hasImage = true; isImageChanged = true; isImageRemoved = false; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java index a53b5402..3249e8d2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java @@ -101,7 +101,7 @@ public class RefundFragment extends Fragment { } private void loadAllSales() { - RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 1000) + RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 1000, null, null, null) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 9e876c69..371e5c20 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -131,11 +131,30 @@ public class PetProfileFragment extends Fragment { binding.tvPetPrice.setText("$0.00"); } - // Display owner name if available, otherwise show No Owner - if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) { - binding.tvPetOwner.setText(pet.getCustomerName()); + String status = pet.getPetStatus(); + + // Display owner name only if the pet is Adopted or Owned + if ("Adopted".equalsIgnoreCase(status) || "Owned".equalsIgnoreCase(status)) { + binding.layoutPetOwner.setVisibility(View.VISIBLE); + if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) { + binding.tvPetOwner.setText(pet.getCustomerName()); + } else { + binding.tvPetOwner.setText("No Owner"); + } } else { - binding.tvPetOwner.setText("No Owner"); + binding.layoutPetOwner.setVisibility(View.GONE); + } + + // Display store name only if the pet is Adopted or Available + if ("Available".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) { + binding.layoutPetStore.setVisibility(View.VISIBLE); + if (pet.getStoreName() != null && !pet.getStoreName().isEmpty()) { + binding.tvPetStore.setText(pet.getStoreName()); + } else { + binding.tvPetStore.setText("No Store"); + } + } else { + binding.layoutPetStore.setVisibility(View.GONE); } } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load pet data: " + resource.message, Toast.LENGTH_SHORT).show(); diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java index 4068c472..370b28a9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java @@ -20,8 +20,8 @@ public class SaleRepository extends BaseRepository { this.saleApi = saleApi; } - public LiveData>> getAllSales(int page, int size) { - return executeCall(saleApi.getAllSales(page, size)); + public LiveData>> getAllSales(int page, int size, String query, String paymentMethod, String sortBy) { + return executeCall(saleApi.getAllSales(page, size, query, paymentMethod, sortBy)); } public LiveData> getSaleById(Long id) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java index aca186f6..4bdd337e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java @@ -21,8 +21,8 @@ public class SaleViewModel extends ViewModel { this.saleRepository = saleRepository; } - public LiveData>> getAllSales(int page, int size) { - return saleRepository.getAllSales(page, size); + public LiveData>> getAllSales(int page, int size, String query, String paymentMethod, String sortBy) { + return saleRepository.getAllSales(page, size, query, paymentMethod, sortBy); } public LiveData> getSaleById(Long id) { diff --git a/android/app/src/main/res/layout/fragment_pet_profile.xml b/android/app/src/main/res/layout/fragment_pet_profile.xml index f5af4445..25f2214c 100644 --- a/android/app/src/main/res/layout/fragment_pet_profile.xml +++ b/android/app/src/main/res/layout/fragment_pet_profile.xml @@ -222,13 +222,15 @@ + android:padding="16dp" + android:visibility="gone"> + + + + + + + + diff --git a/android/app/src/main/res/layout/fragment_sale.xml b/android/app/src/main/res/layout/fragment_sale.xml index cea233df..b5256f3d 100644 --- a/android/app/src/main/res/layout/fragment_sale.xml +++ b/android/app/src/main/res/layout/fragment_sale.xml @@ -29,35 +29,101 @@ android:contentDescription="Open menu"/> + android:textStyle="bold" + android:layout_marginStart="8dp"/> + + - + android:orientation="vertical" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:visibility="gone" + android:background="@color/primary_dark" + android:elevation="4dp"> - - - - - - - - - - - - + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml index dec7a883..215211b9 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml @@ -10,6 +10,7 @@ + @@ -25,6 +26,7 @@ + + + - + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml index 8e920be0..a508f9c0 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/inventory-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/inventory-view.fxml index 18e9ad5a..81eb71ba 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/inventory-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/inventory-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml index d599e010..5aa0acdb 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml @@ -53,6 +53,14 @@ + @@ -69,6 +77,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-supplier-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-supplier-view.fxml index 19817faa..7d5afc4c 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-supplier-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-supplier-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml index eeff74b4..641ba665 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml @@ -52,6 +52,14 @@ + @@ -67,6 +75,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/purchase-order-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/purchase-order-view.fxml index 74a00e6d..ab9276c4 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/purchase-order-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/purchase-order-view.fxml @@ -55,6 +55,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml index 3a3c9608..abf0df77 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml @@ -89,6 +89,11 @@ + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/service-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/service-view.fxml index 5353b0e6..3a4ae448 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/service-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/service-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml index 9619e374..da15274d 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/staff-accounts-view.fxml @@ -76,6 +76,12 @@ + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/supplier-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/supplier-view.fxml index 9c6d75cc..ade4b18d 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/supplier-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/supplier-view.fxml @@ -51,6 +51,14 @@ + @@ -65,6 +73,12 @@ + + -- 2.49.1 From 39fdf8814a737f64e2c50c840c30917536b4071d Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Thu, 9 Apr 2026 11:49:55 -0600 Subject: [PATCH 26/46] Adjust Sales Layout --- .../org/example/petshopdesktop/modelviews/sale-view.fxml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml index abf0df77..b237753f 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/sale-view.fxml @@ -19,7 +19,7 @@ - + - + -- 2.49.1 From 863692c05850dad5c6479c304ac2457c358739ac Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:17:51 -0600 Subject: [PATCH 27/46] created viewmodels for detailFragments --- .../AdoptionDetailFragment.java | 198 ++++-------- .../AppointmentDetailFragment.java | 116 ++------ .../InventoryDetailFragment.java | 139 +++------ .../detailfragments/PetDetailFragment.java | 128 +++----- .../ProductDetailFragment.java | 133 +++------ .../ProductSupplierDetailFragment.java | 122 +++----- .../PurchaseOrderDetailFragment.java | 8 +- .../detailfragments/RefundFragment.java | 281 +++++------------- .../detailfragments/SaleDetailFragment.java | 238 +++++++-------- .../ServiceDetailFragment.java | 75 ++--- .../detailfragments/StaffDetailFragment.java | 76 ++--- .../SupplierDetailFragment.java | 77 ++--- .../viewmodels/AdoptionDetailViewModel.java | 102 +++++++ .../viewmodels/InventoryDetailViewModel.java | 79 +++++ .../viewmodels/PetDetailViewModel.java | 94 ++++++ .../viewmodels/ProductDetailViewModel.java | 85 ++++++ .../ProductSupplierDetailViewModel.java | 78 +++++ .../PurchaseOrderDetailViewModel.java | 26 ++ .../viewmodels/RefundViewModel.java | 167 +++++++++++ .../viewmodels/SaleDetailViewModel.java | 109 +++++++ .../viewmodels/ServiceDetailViewModel.java | 54 ++++ .../viewmodels/StaffDetailViewModel.java | 49 +++ .../viewmodels/SupplierDetailViewModel.java | 54 ++++ 23 files changed, 1359 insertions(+), 1129 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java 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 ece652fd..eae36642 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 @@ -16,11 +16,7 @@ import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.AdoptionViewModel; -import com.example.petstoremobile.viewmodels.CustomerViewModel; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; -import com.example.petstoremobile.viewmodels.UserViewModel; +import com.example.petstoremobile.viewmodels.AdoptionDetailViewModel; import java.math.BigDecimal; import java.util.*; @@ -34,35 +30,19 @@ import dagger.hilt.android.AndroidEntryPoint; public class AdoptionDetailFragment extends Fragment { private FragmentAdoptionDetailBinding binding; + private AdoptionDetailViewModel viewModel; - private long adoptionId = -1; - private boolean isEditing = false; private long preselectedPetId = -1; private long preselectedCustomerId = -1; private long preselectedStoreId = -1; private long preselectedEmployeeId = -1; - private List petList = new ArrayList<>(); - private List customerList = new ArrayList<>(); - private List storeList = new ArrayList<>(); - private List employeeList = new ArrayList<>(); - private final String[] STATUSES = {"Pending", "Completed", "Cancelled"}; - private AdoptionViewModel adoptionViewModel; - private PetViewModel petViewModel; - private CustomerViewModel customerViewModel; - private StoreViewModel storeViewModel; - private UserViewModel userViewModel; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); - petViewModel = new ViewModelProvider(this).get(PetViewModel.class); - customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); - userViewModel = new ViewModelProvider(this).get(UserViewModel.class); + viewModel = new ViewModelProvider(this).get(AdoptionDetailViewModel.class); } @Override @@ -77,6 +57,7 @@ public class AdoptionDetailFragment extends Fragment { super.onViewCreated(view, savedInstanceState); setupSpinners(); setupDatePicker(); + observeViewModel(); loadSpinnersData(); handleArguments(); @@ -85,29 +66,31 @@ public class AdoptionDetailFragment extends Fragment { binding.btnDeleteAdoption.setOnClickListener(v -> confirmDelete()); } + 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()); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Configures the spinner for adoption status. - */ private void setupSpinners() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES); - // Pet spinner disabled by default until customer is selected UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); - // Listener to enable pet spinner based on customer selection 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 (!isEditing) { + if (!viewModel.isEditing()) { binding.spinnerAdoptionPet.setSelection(0); UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); } @@ -117,27 +100,21 @@ public class AdoptionDetailFragment extends Fragment { public void onNothingSelected(AdapterView parent) {} }); - // Listener to load employees based on selected store binding.spinnerAdoptionStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (position > 0 && position <= storeList.size()) { - DropdownDTO selectedStore = storeList.get(position - 1); + if (position > 0 && viewModel.getStoreList().getValue() != null && position <= viewModel.getStoreList().getValue().size()) { + DropdownDTO selectedStore = viewModel.getStoreList().getValue().get(position - 1); loadEmployees(selectedStore.getId()); } else { - employeeList.clear(); - refreshEmployeeSpinner(); + viewModel.setEmployeeList(new ArrayList<>()); } } - @Override public void onNothingSelected(AdapterView parent) {} }); } - /** - * Configures the date picker dialog for the adoption date field. - */ private void setupDatePicker() { binding.etAdoptionDate.setOnClickListener(v -> { Calendar c = Calendar.getInstance(); @@ -150,129 +127,77 @@ public class AdoptionDetailFragment extends Fragment { }); } - /** - * Fetches required data for spinners from the backend. - */ private void loadSpinnersData() { - loadPets(); - loadCustomers(); - loadStores(); - } - - /** - * Loads the list of pets from the API. - */ - private void loadPets() { - petViewModel.getAdoptionPets().observe(getViewLifecycleOwner(), resource -> { + viewModel.loadPets().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - petList = resource.data; - refreshPetSpinner(); + viewModel.setPetList(resource.data); + } + }); + viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setCustomerList(resource.data); + } + }); + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setStoreList(resource.data); } }); } - /** - * Populates the pet selection spinner with data. - */ private void refreshPetSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, viewModel.getPetList().getValue(), DropdownDTO::getLabel, "-- Select Pet --", preselectedPetId, DropdownDTO::getId); } - /** - * Loads the list of customers from the API. - */ - private void loadCustomers() { - customerViewModel.getCustomerDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data; - refreshCustomerSpinner(); - } - }); - } - - /** - * Populates the customer selection spinner with data. - */ private void refreshCustomerSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList, - DropdownDTO::getLabel, - "-- Select Customer --", + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, viewModel.getCustomerList().getValue(), + DropdownDTO::getLabel, "-- Select Customer --", preselectedCustomerId, DropdownDTO::getId); } - /** - * Loads the list of stores from the API. - */ - private void loadStores() { - storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data; - refreshStoreSpinner(); - } - }); - } - - /** - * Populates the store selection spinner with data. - */ private void refreshStoreSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, storeList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, viewModel.getStoreList().getValue(), DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId); } - /** - * Loads the list of employees for a specific store. - */ private void loadEmployees(Long storeId) { - storeViewModel.getStoreEmployees(storeId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadEmployees(storeId).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - employeeList = resource.data; - refreshEmployeeSpinner(); + viewModel.setEmployeeList(resource.data); } }); } - /** - * Populates the employee selection spinner with data. - */ private void refreshEmployeeSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, employeeList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, viewModel.getEmployeeList().getValue(), DropdownDTO::getLabel, "-- Select Staff --", preselectedEmployeeId, DropdownDTO::getId); } - /** - * Handles arguments to determine if the fragment is in edit or add mode. - */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("adoptionId")) { - isEditing = true; - adoptionId = a.getLong("adoptionId"); + long adoptionId = a.getLong("adoptionId"); + viewModel.setAdoptionId(adoptionId); binding.tvAdoptionMode.setText("Edit Adoption"); binding.tvAdoptionId.setText("ID: " + adoptionId); binding.tvAdoptionId.setVisibility(View.VISIBLE); binding.btnDeleteAdoption.setVisibility(View.VISIBLE); loadAdoptionData(); } else { - isEditing = false; + viewModel.setAdoptionId(-1); binding.tvAdoptionMode.setText("Add Adoption"); binding.btnDeleteAdoption.setVisibility(View.GONE); binding.tvAdoptionId.setVisibility(View.GONE); - - // Explicitly disable in add mode UIUtils.setViewsEnabled(false, binding.spinnerAdoptionPet); } } - /** - * Fetches specific adoption details from the backend using the ID. - */ private void loadAdoptionData() { - adoptionViewModel.getAdoptionById(adoptionId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadAdoption().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { AdoptionDTO a = resource.data; @@ -289,7 +214,6 @@ public class AdoptionDetailFragment extends Fragment { refreshCustomerSpinner(); refreshStoreSpinner(); - // In edit mode, if a customer is already set, ensure pet spinner is enabled if (preselectedCustomerId != -1) { UIUtils.setViewsEnabled(true, binding.spinnerAdoptionPet); } @@ -299,9 +223,6 @@ public class AdoptionDetailFragment extends Fragment { }); } - /** - * Validates input and saves the adoption request to the backend. - */ private void saveAdoption() { if (binding.spinnerAdoptionCustomer.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; @@ -328,13 +249,13 @@ public class AdoptionDetailFragment extends Fragment { } } - DropdownDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); - DropdownDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); - DropdownDTO store = storeList.get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); + 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) { - employeeId = employeeList.get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId(); + employeeId = viewModel.getEmployeeList().getValue().get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId(); } String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; @@ -349,33 +270,19 @@ public class AdoptionDetailFragment extends Fragment { fee ); - if (isEditing) { - adoptionViewModel.updateAdoption(adoptionId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - adoptionViewModel.createAdoption(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } + viewModel.saveAdoption(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), viewModel.isEditing() ? "Updated" : "Saved", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Shows a confirmation dialog before deleting an adoption request. - */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption", () -> - adoptionViewModel.deleteAdoption(adoptionId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteAdoption().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); navigateBack(); @@ -385,9 +292,6 @@ public class AdoptionDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous fragment. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } 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 1d280270..bf63fa41 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 @@ -43,37 +43,28 @@ public class AppointmentDetailFragment extends Fragment { private final Integer[] HOURS = {9, 10, 11, 12, 13, 14, 15, 16, 17}; private final Integer[] MINUTES = {0, 15, 30, 45}; - private AppointmentDetailViewModel appointmentViewModel; + private AppointmentDetailViewModel viewModel; private boolean isUpdatingUI = false; - /** - * Called when the fragment is first created. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - appointmentViewModel = new ViewModelProvider(this).get(AppointmentDetailViewModel.class); + viewModel = new ViewModelProvider(this).get(AppointmentDetailViewModel.class); } - /** - * Creates and returns the view hierarchy with the fragment. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentAppointmentDetailBinding.inflate(inflater, container, false); return binding.getRoot(); } - /** - * Called immediately after onCreateView has returned. - */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); setupSpinners(); setupDatePicker(); observeViewModel(); - appointmentViewModel.loadInitialFormData(); + viewModel.loadInitialFormData(); handleArguments(); binding.btnApptBack.setOnClickListener(v -> navigateBack()); @@ -81,110 +72,83 @@ public class AppointmentDetailFragment extends Fragment { binding.btnDeleteAppointment.setOnClickListener(v -> confirmDelete()); } - /** - * Called when the view previously created by onCreateView has been detached. - */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Configures the adapters and listeners for all spinners. - */ private void setupSpinners() { - //Status Spinner is empty by default the date determines whats in here SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, new String[]{}); - // Set up hour and minute spinners String[] hours = new String[HOURS.length]; for (int i = 0; i < HOURS.length; i++) hours[i] = DateTimeUtils.formatTime(HOURS[i], 0); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00", "15", "30", "45"}); - // Pet and Staff spinners disabled by until parent selection UIUtils.setViewsEnabled(false, binding.spinnerPet, binding.spinnerStaff); - // Listener to notify ViewModel of customer selection binding.spinnerCustomer.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - appointmentViewModel.onCustomerSelected(position); + viewModel.onCustomerSelected(position); } @Override public void onNothingSelected(AdapterView parent) {} }); - // Listener to notify ViewModel of store selection binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - appointmentViewModel.onStoreSelected(position); + viewModel.onStoreSelected(position); } @Override public void onNothingSelected(AdapterView parent) {} }); - // Listeners for other selections - SpinnerUtils.setOnIndexSelectedListener(binding.spinnerService, p -> appointmentViewModel.onServiceSelected(p)); - SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPet, p -> appointmentViewModel.onPetSelected(p)); - SpinnerUtils.setOnIndexSelectedListener(binding.spinnerStaff, p -> appointmentViewModel.onStaffSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerService, p -> viewModel.onServiceSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerPet, p -> viewModel.onPetSelected(p)); + SpinnerUtils.setOnIndexSelectedListener(binding.spinnerStaff, p -> viewModel.onStaffSelected(p)); - // Listeners for time changes SpinnerUtils.setOnIndexSelectedListener(binding.spinnerHour, p -> notifyDateTimeStatusChange()); SpinnerUtils.setOnIndexSelectedListener(binding.spinnerMinute, p -> notifyDateTimeStatusChange()); - - // Listener to notify ViewModel of status selection SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAppointmentStatus, p -> notifyDateTimeStatusChange()); } - /** - * Configures the date picker dialog for the appointment date field. - */ private void setupDatePicker() { binding.etAppointmentDate.setOnClickListener(v -> UIUtils.showDatePicker(requireContext(), binding.etAppointmentDate, this::notifyDateTimeStatusChange)); } - /** - * Observes the ViewModel for UI state and list updates. - */ private void observeViewModel() { - appointmentViewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); + viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); - // Populate spinners when data arrives - appointmentViewModel.getCustomers().observe(getViewLifecycleOwner(), list -> + viewModel.getCustomers().observe(getViewLifecycleOwner(), list -> SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, list, DropdownDTO::getLabel, "-- Select Customer --", preselectedCustomerId, DropdownDTO::getId)); - appointmentViewModel.getStores().observe(getViewLifecycleOwner(), list -> + viewModel.getStores().observe(getViewLifecycleOwner(), list -> SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, list, DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId)); - appointmentViewModel.getServices().observe(getViewLifecycleOwner(), list -> + viewModel.getServices().observe(getViewLifecycleOwner(), list -> SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, list, ServiceDTO::getServiceName, "-- Select Service --", preselectedServiceId, ServiceDTO::getServiceId)); - appointmentViewModel.getCustomerPets().observe(getViewLifecycleOwner(), list -> + viewModel.getCustomerPets().observe(getViewLifecycleOwner(), list -> SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, list, DropdownDTO::getLabel, "-- Select Pet --", preselectedPetId, DropdownDTO::getId)); - appointmentViewModel.getStoreEmployees().observe(getViewLifecycleOwner(), list -> + viewModel.getStoreEmployees().observe(getViewLifecycleOwner(), list -> SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, list, DropdownDTO::getLabel, "-- Select Staff --", preselectedStaffId, DropdownDTO::getId)); } - /** - * Applies the ViewState provided by the ViewModel to the UI components. - */ private void applyViewState(AppointmentDetailViewModel.ViewState state) { isUpdatingUI = true; - // Mode specific UI binding.tvApptMode.setText(state.isEditing ? "Edit Appointment" : "Add Appointment"); - binding.tvAppointmentId.setText(DateTimeUtils.formatId(appointmentViewModel.getAppointmentId())); + binding.tvAppointmentId.setText(DateTimeUtils.formatId(viewModel.getAppointmentId())); binding.tvAppointmentId.setVisibility(state.isEditing ? View.VISIBLE : View.GONE); binding.btnDeleteAppointment.setVisibility(state.isDeleteVisible ? View.VISIBLE : View.GONE); binding.btnSaveAppointment.setVisibility(state.isSaveVisible ? View.VISIBLE : View.GONE); - // Enabling/Disabling Views and Labels UIUtils.setFieldEnabled(state.isCustomerEnabled, binding.spinnerCustomer, binding.tvLabelCustomer); UIUtils.setFieldEnabled(state.isStoreEnabled, binding.spinnerStore, binding.tvLabelStore); UIUtils.setFieldEnabled(state.isPetEnabled, binding.spinnerPet, binding.tvLabelPet); @@ -195,7 +159,6 @@ public class AppointmentDetailFragment extends Fragment { UIUtils.setViewsEnabled(state.isTimeEnabled, binding.spinnerMinute); UIUtils.setViewsEnabled(state.isStatusEnabled, binding.spinnerAppointmentStatus); - // Update status options Object selected = binding.spinnerAppointmentStatus.getSelectedItem(); String current = selected != null ? selected.toString() : ""; SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, state.availableStatuses); @@ -204,9 +167,6 @@ public class AppointmentDetailFragment extends Fragment { isUpdatingUI = false; } - /** - * Notifies the ViewModel that the date, time, or status has changed. - */ private void notifyDateTimeStatusChange() { if (isUpdatingUI) return; @@ -214,27 +174,21 @@ public class AppointmentDetailFragment extends Fragment { String time = buildTimeString(); Object selected = binding.spinnerAppointmentStatus.getSelectedItem(); String status = selected != null ? selected.toString() : ""; - appointmentViewModel.onDateOrTimeChanged(date, time, status); + viewModel.onDateOrTimeChanged(date, time, status); } - /** - * Handles arguments to determine if the fragment is in edit or add mode. - */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("appointmentId")) { - appointmentViewModel.setAppointmentId(a.getLong("appointmentId")); + viewModel.setAppointmentId(a.getLong("appointmentId")); loadAppointmentData(); } else { - appointmentViewModel.setAppointmentId(-1); + viewModel.setAppointmentId(-1); } } - /** - * Fetches specific appointment details from the backend using the ID. - */ private void loadAppointmentData() { - appointmentViewModel.loadAppointment().observe(getViewLifecycleOwner(), resource -> { + viewModel.loadAppointment().observe(getViewLifecycleOwner(), resource -> { if (resource == null || resource.status != Resource.Status.SUCCESS || resource.data == null) return; AppointmentDTO a = resource.data; preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; @@ -254,9 +208,6 @@ public class AppointmentDetailFragment extends Fragment { }); } - /** - * Validates input and saves the appointment to the backend. - */ private void saveAppointment() { if (!validateRequiredFields()) return; @@ -264,14 +215,14 @@ public class AppointmentDetailFragment extends Fragment { String time = buildTimeString(); String status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase(); - if (!appointmentViewModel.isValidFutureBooking(status, date, time)) { + if (!viewModel.isValidFutureBooking(status, date, time)) { DialogUtils.showInfoDialog(requireContext(), "Invalid Time", "Booked appointments must be in the future."); return; } - appointmentViewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> { + viewModel.saveAppointment(date, time, status).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - AppointmentDetailViewModel.ViewState state = appointmentViewModel.getViewState().getValue(); + AppointmentDetailViewModel.ViewState state = viewModel.getViewState().getValue(); String message = (state != null && state.isEditing) ? "Updated" : "Saved"; Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); navigateBack(); @@ -281,9 +232,6 @@ public class AppointmentDetailFragment extends Fragment { }); } - /** - * Validates that all required fields are selected. - */ private boolean validateRequiredFields() { if (binding.spinnerCustomer.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a customer"); if (binding.spinnerStore.getSelectedItemPosition() == 0) return UIUtils.showToast(getContext(), "Select a store"); @@ -293,24 +241,15 @@ public class AppointmentDetailFragment extends Fragment { return true; } - /** - * Builds a time string from the hour and minute spinners. - */ private String buildTimeString() { return DateTimeUtils.formatTime(HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); } - /** - * Handles errors that occur during the saving process. - */ private void handleSaveError(String errorMessage) { if (errorMessage != null && errorMessage.toLowerCase().contains("not available")) showNoAvailabilityDialog(); else Toast.makeText(getContext(), errorMessage != null ? errorMessage : "Error saving", Toast.LENGTH_SHORT).show(); } - /** - * Shows a specialized dialog when a time slot is not available. - */ private void showNoAvailabilityDialog() { new androidx.appcompat.app.AlertDialog.Builder(requireContext()) .setTitle("No Availability") @@ -319,26 +258,17 @@ public class AppointmentDetailFragment extends Fragment { .setNegativeButton("Cancel Booking", (d, w) -> navigateBack()).show(); } - /** - * Shows a confirmation dialog and handles the deletion of an appointment. - */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () -> - appointmentViewModel.deleteAppointment().observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteAppointment().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) navigateBack(); })); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Parses a time string and sets the hour and minute spinners. - */ private void parseAndSetTimeSpinners(String time) { int[] parsedTime = DateTimeUtils.parseTimeString(time); if (parsedTime == null) return; 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 640b99b9..a6d1c613 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 @@ -17,16 +17,10 @@ import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding; import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.ProductDTO; -import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; -import com.example.petstoremobile.viewmodels.InventoryViewModel; -import com.example.petstoremobile.viewmodels.ProductViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; - -import java.util.ArrayList; -import java.util.List; +import com.example.petstoremobile.viewmodels.InventoryDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -37,33 +31,17 @@ import dagger.hilt.android.AndroidEntryPoint; public class InventoryDetailFragment extends Fragment { private FragmentInventoryDetailBinding binding; + private InventoryDetailViewModel viewModel; - private InventoryViewModel inventoryViewModel; - private ProductViewModel productViewModel; - private StoreViewModel storeViewModel; - - private boolean isEditing = false; - private long inventoryId = -1; private long preselectedStoreId = -1; private long preselectedProductId = -1; - private List storeList = new ArrayList<>(); - private List productList = new ArrayList<>(); - - /** - * Initializes the view models. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - inventoryViewModel = new ViewModelProvider(this).get(InventoryViewModel.class); - productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(InventoryDetailViewModel.class); } - /** - * Inflates the layout. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -71,13 +49,11 @@ public class InventoryDetailFragment extends Fragment { return binding.getRoot(); } - /** - * Sets up UI components after the view is created. - */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + observeViewModel(); loadSpinnersData(); handleArguments(); @@ -86,64 +62,47 @@ public class InventoryDetailFragment extends Fragment { binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete()); } + private void observeViewModel() { + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner()); + viewModel.getProductList().observe(getViewLifecycleOwner(), list -> refreshProductSpinner()); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Fetches required data for spinners from the backend. - */ private void loadSpinnersData() { - loadStores(); - loadProducts(); - } - - /** - * Loads the list of stores for the spinner. - */ - private void loadStores() { - storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data; - refreshStoreSpinner(); + viewModel.setStoreList(resource.data); + } + }); + viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setProductList(resource.data.getContent()); } }); } private void refreshStoreSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, storeList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, viewModel.getStoreList().getValue(), DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId); } - /** - * Loads the list of products for the spinner. - */ - private void loadProducts() { - productViewModel.getAllProducts(null, null, 0, 500, "prodName").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - productList = resource.data.getContent(); - refreshProductSpinner(); - } - }); - } - private void refreshProductSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, productList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, viewModel.getProductList().getValue(), ProductDTO::getProdName, "-- Select Product --", preselectedProductId, ProductDTO::getProdId); } - /** - * Handles fragment arguments to determine if we are in edit or add mode. - */ private void handleArguments() { Bundle args = getArguments(); if (args != null && args.containsKey("inventoryId")) { - isEditing = true; - inventoryId = args.getLong("inventoryId"); + long inventoryId = args.getLong("inventoryId"); + viewModel.setInventoryId(inventoryId); binding.tvInventoryMode.setText("Edit Inventory"); binding.tvInventoryId.setText("Inventory ID: " + inventoryId); @@ -153,7 +112,7 @@ public class InventoryDetailFragment extends Fragment { loadInventoryData(); } else { - isEditing = false; + viewModel.setInventoryId(-1); binding.tvInventoryMode.setText("Add Inventory"); binding.tvInventoryId.setVisibility(View.GONE); binding.btnDeleteInventory.setVisibility(View.GONE); @@ -161,11 +120,8 @@ public class InventoryDetailFragment extends Fragment { } } - /** - * Loads existing inventory data from the backend. - */ private void loadInventoryData() { - inventoryViewModel.getInventoryById(inventoryId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadInventory().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { InventoryDTO inv = resource.data; @@ -181,9 +137,6 @@ public class InventoryDetailFragment extends Fragment { }); } - /** - * Validates input and saves the current inventory item details to the backend. - */ private void saveInventory() { if (binding.spinnerInventoryStore.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Please select a store", Toast.LENGTH_SHORT).show(); @@ -200,38 +153,23 @@ public class InventoryDetailFragment extends Fragment { } int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim()); - DropdownDTO store = storeList.get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); - ProductDTO product = productList.get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); + DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); + ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); InventoryDTO request = new InventoryDTO(product.getProdId(), store.getId(), quantity); setButtonsEnabled(false); - if (isEditing) { - inventoryViewModel.updateInventory(inventoryId, request).observe(getViewLifecycleOwner(), resource -> { - setButtonsEnabled(true); - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - inventoryViewModel.createInventory(request).observe(getViewLifecycleOwner(), resource -> { - setButtonsEnabled(true); - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } + viewModel.saveInventory(request).observe(getViewLifecycleOwner(), resource -> { + setButtonsEnabled(true); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), viewModel.isEditing() ? "Inventory updated" : "Inventory created", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Shows a confirmation dialog before deleting an inventory item. - */ private void confirmDelete() { new AlertDialog.Builder(requireContext()) .setTitle("Delete inventory item?") @@ -241,12 +179,9 @@ public class InventoryDetailFragment extends Fragment { .show(); } - /** - * Sends a request to the API to delete the inventory item. - */ private void deleteInventory() { setButtonsEnabled(false); - inventoryViewModel.deleteInventory(inventoryId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteInventory().observe(getViewLifecycleOwner(), resource -> { setButtonsEnabled(true); if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); @@ -257,16 +192,10 @@ public class InventoryDetailFragment extends Fragment { }); } - /** - * Navigates back to the previous fragment. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Enables or disables action buttons. - */ private void setButtonsEnabled(boolean enabled) { binding.btnSaveInventory.setEnabled(enabled); binding.btnDeleteInventory.setEnabled(enabled); 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 6c44ca59..d260d723 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 @@ -26,12 +26,8 @@ import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.CustomerViewModel; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.PetDetailViewModel; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; import dagger.hilt.android.AndroidEntryPoint; @@ -43,23 +39,15 @@ import dagger.hilt.android.AndroidEntryPoint; public class PetDetailFragment extends Fragment { private FragmentPetDetailBinding binding; - private long petId; - private boolean isEditing = false; - - private PetViewModel viewModel; - private CustomerViewModel customerViewModel; - private StoreViewModel storeViewModel; - private List customerList = new ArrayList<>(); - private List storeList = new ArrayList<>(); + private PetDetailViewModel viewModel; + private Long selectedCustomerId = null; private Long selectedStoreId = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PetViewModel.class); - customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(PetDetailViewModel.class); } @Override @@ -74,8 +62,7 @@ public class PetDetailFragment extends Fragment { super.onViewCreated(view, savedInstanceState); setupSpinner(); - loadCustomers(); - loadStores(); + observeViewModel(); handleArguments(); //set button click listeners @@ -84,6 +71,23 @@ public class PetDetailFragment extends Fragment { binding.btnDeletePet.setOnClickListener(v -> deletePet()); } + private void observeViewModel() { + viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> updateCustomerSpinnerSelection()); + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> updateStoreSpinnerSelection()); + + viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setCustomerList(resource.data); + } + }); + + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setStoreList(resource.data); + } + }); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -113,14 +117,14 @@ public class PetDetailFragment extends Fragment { Long customerId = null; int customerPos = binding.spinnerCustomer.getSelectedItemPosition(); if (customerPos > 0) { // 0 means no customer for pet - customerId = customerList.get(customerPos - 1).getId(); + customerId = viewModel.getCustomerList().getValue().get(customerPos - 1).getId(); } // Get selected store Long storeId = null; int storePos = binding.spinnerStore.getSelectedItemPosition(); if (storePos > 0) { - storeId = storeList.get(storePos - 1).getId(); + storeId = viewModel.getStoreList().getValue().get(storePos - 1).getId(); } // Validation: If status is Available, a store must be selected @@ -150,31 +154,20 @@ public class PetDetailFragment extends Fragment { petDTO.setCustomerId(customerId); petDTO.setStoreId(storeId); - //check if the pet is being edited or added - if (isEditing) { - // Update existing pet - petDTO.setPetId(petId); - viewModel.updatePet(petId, petDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) petId); + viewModel.savePet(petDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + if (viewModel.isEditing()) { + ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) viewModel.getPetId()); Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); - navigateToPetList(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - // Add new pet - viewModel.createPet(petDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { + } else { ActivityLogger.log(requireContext(), "Added new Pet: " + name); Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show(); - navigateToPetList(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } + navigateToPetList(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } /** @@ -182,9 +175,9 @@ public class PetDetailFragment extends Fragment { */ private void deletePet() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () -> - viewModel.deletePet(petId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deletePet().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) petId); + ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) viewModel.getPetId()); Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); navigateToPetList(); } else if (resource.status == Resource.Status.ERROR) { @@ -211,30 +204,23 @@ public class PetDetailFragment extends Fragment { * Handles arguments passed to the fragment to determine if it's in edit or add mode. */ private void handleArguments() { - // Pet is being edited if the bundle contains a petId if (getArguments() != null && getArguments().containsKey("petId")) { - // Get pet data from arguments and populate fields - isEditing = true; - petId = getArguments().getLong("petId"); + long petId = getArguments().getLong("petId"); + viewModel.setPetId(petId); binding.tvMode.setText("Edit Pet"); binding.tvPetId.setText("ID: " + petId); binding.tvPetId.setVisibility(View.VISIBLE); binding.btnDeletePet.setVisibility(View.VISIBLE); - // Disable species and breed fields in edit mode UIUtils.setViewsEnabled(false, binding.etPetSpecies, binding.etPetBreed); - loadPetData(); } else { - // Pet is being added - // Set default values for add a new pet - isEditing = false; + viewModel.setPetId(-1); binding.tvMode.setText("Add Pet"); binding.tvPetId.setVisibility(View.GONE); binding.btnDeletePet.setVisibility(View.GONE); binding.btnSavePet.setText("Add"); - // Enable species and breed fields in edit mode UIUtils.setViewsEnabled(true, binding.etPetSpecies, binding.etPetBreed); } } @@ -243,7 +229,7 @@ public class PetDetailFragment extends Fragment { * Fetches specific pet details from the backend using the ID. */ private void loadPetData() { - viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadPet().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { PetDTO p = resource.data; @@ -267,30 +253,6 @@ public class PetDetailFragment extends Fragment { }); } - /** - * Fetches the list of customers and populates the spinner. - */ - private void loadCustomers() { - customerViewModel.getCustomerDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data; - updateCustomerSpinnerSelection(); - } - }); - } - - /** - * Fetches the list of stores and populates the spinner. - */ - private void loadStores() { - storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data; - updateStoreSpinnerSelection(); - } - }); - } - /** * Updates the customer spinner with the current list and sets the selection if needed. */ @@ -298,7 +260,7 @@ public class PetDetailFragment extends Fragment { SpinnerUtils.populateSpinner( requireContext(), binding.spinnerCustomer, - customerList, + viewModel.getCustomerList().getValue(), DropdownDTO::getLabel, "No Owner", selectedCustomerId, @@ -313,7 +275,7 @@ public class PetDetailFragment extends Fragment { SpinnerUtils.populateSpinner( requireContext(), binding.spinnerStore, - storeList, + viewModel.getStoreList().getValue(), DropdownDTO::getLabel, "None", selectedStoreId, @@ -333,11 +295,9 @@ public class PetDetailFragment extends Fragment { public void onItemSelected(AdapterView parent, View view, int position, long id) { String status = parent.getItemAtPosition(position).toString(); - // Clear any existing error icons when status changes clearSpinnerError(binding.spinnerCustomer); clearSpinnerError(binding.spinnerStore); - //Disable the customer spinner if the status is "Available" if ("Available".equalsIgnoreCase(status)) { binding.spinnerCustomer.setSelection(0); UIUtils.setViewsEnabled(false, binding.spinnerCustomer); @@ -345,7 +305,6 @@ public class PetDetailFragment extends Fragment { UIUtils.setViewsEnabled(true, binding.spinnerCustomer); } - //Disable the store spinner if the status is "Owned" if ("Owned".equalsIgnoreCase(status)) { binding.spinnerStore.setSelection(0); UIUtils.setViewsEnabled(false, binding.spinnerStore); @@ -360,9 +319,6 @@ public class PetDetailFragment extends Fragment { }); } - /** - * Clears error messages from a Spinner's selected view. - */ private void clearSpinnerError(Spinner spinner) { View selectedView = spinner.getSelectedView(); if (selectedView instanceof TextView) { 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 d2527d71..2d07c280 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 @@ -17,7 +17,7 @@ import com.example.petstoremobile.api.*; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentProductDetailBinding; import com.example.petstoremobile.dtos.*; -import com.example.petstoremobile.viewmodels.ProductViewModel; +import com.example.petstoremobile.viewmodels.ProductDetailViewModel; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; @@ -31,7 +31,6 @@ import java.math.BigDecimal; import java.util.*; import javax.inject.Inject; - import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; @@ -46,29 +45,22 @@ import okhttp3.RequestBody; public class ProductDetailFragment extends Fragment { private FragmentProductDetailBinding binding; + private ProductDetailViewModel viewModel; + private ImagePickerHelper imagePickerHelper; - private long prodId = -1; - private boolean isEditing = false; private long preselectedCategoryId = -1; private boolean hasImage = false; private boolean isImageChanged = false; private boolean isImageRemoved = false; - - private List categoryList = new ArrayList<>(); private Uri photoUri; - private ProductViewModel viewModel; - private ImagePickerHelper imagePickerHelper; @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; - /** - * Initializes activity launchers and the ImagePickerHelper. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ProductViewModel.class); + viewModel = new ViewModelProvider(this).get(ProductDetailViewModel.class); imagePickerHelper = new ImagePickerHelper(this, "product_photo.jpg", new ImagePickerHelper.ImagePickerListener() { @Override @@ -95,9 +87,6 @@ public class ProductDetailFragment extends Fragment { }); } - /** - * Inflates the layout. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -105,14 +94,11 @@ public class ProductDetailFragment extends Fragment { return binding.getRoot(); } - /** - * Sets up UI components and listeners after the view is created. - */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - loadCategories(); + observeViewModel(); handleArguments(); binding.btnProductBack.setOnClickListener(v -> navigateBack()); @@ -121,34 +107,33 @@ public class ProductDetailFragment extends Fragment { binding.ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage)); } + private void observeViewModel() { + viewModel.getCategoryList().observe(getViewLifecycleOwner(), list -> updateCategorySpinner()); + + viewModel.loadCategories().observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setCategoryList(resource.data.getContent()); + } + }); + } + + private void updateCategorySpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, viewModel.getCategoryList().getValue(), + CategoryDTO::getCategoryName, "-- Select Category --", + preselectedCategoryId, CategoryDTO::getCategoryId); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Fetches all product categories for the selection spinner. - */ - private void loadCategories() { - viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - categoryList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList, - CategoryDTO::getCategoryName, "-- Select Category --", - preselectedCategoryId, CategoryDTO::getCategoryId); - } - }); - } - - /** - * Checks if the fragment was opened with existing product data for editing. - */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("prodId")) { - isEditing = true; - prodId = a.getLong("prodId"); + long prodId = a.getLong("prodId"); + viewModel.setProdId(prodId); binding.tvProductMode.setText("Edit Product"); binding.tvProductId.setText("ID: " + prodId); binding.tvProductId.setVisibility(View.VISIBLE); @@ -156,6 +141,7 @@ public class ProductDetailFragment extends Fragment { loadProductData(); loadProductImage(); } else { + viewModel.setProdId(-1); binding.tvProductMode.setText("Add Product"); binding.btnDeleteProduct.setVisibility(View.GONE); binding.tvProductId.setVisibility(View.GONE); @@ -163,11 +149,8 @@ public class ProductDetailFragment extends Fragment { } } - /** - * Loads the product data from the backend. - */ private void loadProductData() { - viewModel.getProductById(prodId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadProduct().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { ProductDTO p = resource.data; @@ -175,24 +158,15 @@ public class ProductDetailFragment extends Fragment { binding.etProductDesc.setText(p.getProdDesc()); binding.etProductPrice.setText(p.getProdPrice() != null ? p.getProdPrice().toString() : ""); preselectedCategoryId = p.getCategoryId() != null ? p.getCategoryId() : -1; - - // Refresh spinner selection once data is loaded - if (!categoryList.isEmpty()) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList, - CategoryDTO::getCategoryName, "-- Select Category --", - preselectedCategoryId, CategoryDTO::getCategoryId); - } + updateCategorySpinner(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load product: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } - /** - * Loads the product image from the backend. - */ private void loadProductImage() { - String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId); + String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, viewModel.getProdId()); String token = tokenManager.getToken(); GlideUtils.loadImageWithToken(requireContext(), binding.ivProductImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() { @@ -208,12 +182,9 @@ public class ProductDetailFragment extends Fragment { }); } - /** - * Performs image related actions (upload/delete) after product details are saved. - */ private void performPendingImageActions(String successMsg) { if (isImageRemoved) { - viewModel.deleteProductImage(prodId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteProductImage().observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status != Resource.Status.LOADING) { if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); @@ -231,9 +202,6 @@ public class ProductDetailFragment extends Fragment { } } - /** - * Uploads the selected image file to the server. - */ private void uploadProductImageAndNavigate(Uri uri, String successMsg) { File file = FileUtils.getFileFromUri(requireContext(), uri); if (file == null) { @@ -245,7 +213,7 @@ public class ProductDetailFragment extends Fragment { RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); - viewModel.uploadProductImage(prodId, body).observe(getViewLifecycleOwner(), resource -> { + viewModel.uploadProductImage(body).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status != Resource.Status.LOADING) { if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); @@ -257,9 +225,6 @@ public class ProductDetailFragment extends Fragment { }); } - /** - * Validates input fields and saves product information to the backend. - */ private void saveProduct() { if (!InputValidator.isNotEmpty(binding.etProductName, "Product Name")) return; @@ -276,39 +241,26 @@ public class ProductDetailFragment extends Fragment { String desc = binding.etProductDesc.getText().toString().trim(); BigDecimal price = new BigDecimal(binding.etProductPrice.getText().toString().trim()); - CategoryDTO category = categoryList.get(binding.spinnerProductCategory.getSelectedItemPosition() - 1); + CategoryDTO category = viewModel.getCategoryList().getValue().get(binding.spinnerProductCategory.getSelectedItemPosition() - 1); ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); - if (isEditing) { - viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS) { - performPendingImageActions("Updated"); - } else { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + viewModel.saveProduct(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { + if (resource.data != null) { + viewModel.setProdId(resource.data.getProdId()); } + performPendingImageActions(viewModel.isEditing() ? "Updated" : "Saved"); + } else { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } else { - viewModel.createProduct(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - prodId = resource.data.getProdId(); - performPendingImageActions("Saved"); - } else { - Toast.makeText(getContext(), "Error saving: " + resource.message, Toast.LENGTH_SHORT).show(); - } - } - }); - } + } + }); } - /** - * Displays a confirmation dialog before deleting the product. - */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () -> - viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteProduct().observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS) { navigateBack(); } else if (resource != null && resource.status == Resource.Status.ERROR) { @@ -317,9 +269,6 @@ public class ProductDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous fragment. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index aa570d13..a0e7d1a6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -15,9 +15,7 @@ import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; -import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; -import com.example.petstoremobile.viewmodels.ProductViewModel; -import com.example.petstoremobile.viewmodels.SupplierViewModel; +import com.example.petstoremobile.viewmodels.ProductSupplierDetailViewModel; import java.math.BigDecimal; import java.util.*; @@ -31,26 +29,15 @@ import dagger.hilt.android.AndroidEntryPoint; public class ProductSupplierDetailFragment extends Fragment { private FragmentProductSupplierDetailBinding binding; + private ProductSupplierDetailViewModel viewModel; - private boolean isEditing = false; - private long editProductId = -1; - private long editSupplierId = -1; private long preselectedProductId = -1; private long preselectedSupplierId = -1; - private List productList = new ArrayList<>(); - private List supplierList = new ArrayList<>(); - - private ProductSupplierViewModel psViewModel; - private ProductViewModel productViewModel; - private SupplierViewModel supplierViewModel; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - psViewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class); - productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); - supplierViewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + viewModel = new ViewModelProvider(this).get(ProductSupplierDetailViewModel.class); } @Override @@ -63,6 +50,7 @@ public class ProductSupplierDetailFragment extends Fragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + observeViewModel(); loadSpinnersData(); handleArguments(); @@ -71,81 +59,59 @@ public class ProductSupplierDetailFragment extends Fragment { binding.btnDeletePS.setOnClickListener(v -> confirmDelete()); } + private void observeViewModel() { + viewModel.getProductList().observe(getViewLifecycleOwner(), list -> refreshProductSpinner()); + viewModel.getSupplierList().observe(getViewLifecycleOwner(), list -> refreshSupplierSpinner()); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Fetches products and suppliers to populate the spinners. - */ private void loadSpinnersData() { - loadProducts(); - loadSuppliers(); - } - - /** - * Loads the list of products from the API. - */ - private void loadProducts() { - productViewModel.getAllProducts(null, null, 0, 200, "prodName").observe(getViewLifecycleOwner(), resource -> { + viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - productList = resource.data.getContent(); - refreshProductSpinner(); + viewModel.setProductList(resource.data.getContent()); + } + }); + viewModel.loadSuppliers().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setSupplierList(resource.data.getContent()); } }); } private void refreshProductSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, viewModel.getProductList().getValue(), ProductDTO::getProdName, "-- Select Product --", preselectedProductId, ProductDTO::getProdId); } - /** - * Loads the list of suppliers from the API. - */ - private void loadSuppliers() { - supplierViewModel.getAllSuppliers(0, 200, null, "supCompany").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - supplierList = resource.data.getContent(); - refreshSupplierSpinner(); - } - }); - } - private void refreshSupplierSpinner() { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, viewModel.getSupplierList().getValue(), SupplierDTO::getSupCompany, "-- Select Supplier --", preselectedSupplierId, SupplierDTO::getSupId); } - /** - * Handles arguments to determine if the fragment is in edit or add mode. - */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("productId") && a.containsKey("supplierId")) { - isEditing = true; - editProductId = a.getLong("productId"); - editSupplierId = a.getLong("supplierId"); - preselectedProductId = editProductId; - preselectedSupplierId = editSupplierId; + long productId = a.getLong("productId"); + long supplierId = a.getLong("supplierId"); + viewModel.setEditMode(productId, supplierId); + preselectedProductId = productId; + preselectedSupplierId = supplierId; binding.tvPSMode.setText("Edit Product Supplier"); binding.btnDeletePS.setVisibility(View.VISIBLE); - } else { binding.tvPSMode.setText("Add Product Supplier"); binding.btnDeletePS.setVisibility(View.GONE); } } - - /** - * Validates input and saves the product-supplier to the backend. - */ private void save() { if (binding.spinnerPSProduct.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return; @@ -159,40 +125,25 @@ public class ProductSupplierDetailFragment extends Fragment { return; } - ProductDTO product = productList.get(binding.spinnerPSProduct.getSelectedItemPosition() - 1); - SupplierDTO supplier = supplierList.get(binding.spinnerPSSupplier.getSelectedItemPosition() - 1); + ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerPSProduct.getSelectedItemPosition() - 1); + SupplierDTO supplier = viewModel.getSupplierList().getValue().get(binding.spinnerPSSupplier.getSelectedItemPosition() - 1); BigDecimal cost = new BigDecimal(binding.etPSCost.getText().toString().trim()); - ProductSupplierDTO dto = new ProductSupplierDTO( - product.getProdId(), supplier.getSupId(), cost); + ProductSupplierDTO dto = new ProductSupplierDTO(product.getProdId(), supplier.getSupId(), cost); - if (isEditing) { - psViewModel.updateProductSupplier(editProductId, editSupplierId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - psViewModel.createProductSupplier(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } + viewModel.saveProductSupplier(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), viewModel.isEditing() ? "Updated" : "Saved", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Shows a confirmation dialog before deleting a product-supplier relationship. - */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier", () -> - psViewModel.deleteProductSupplier(editProductId, editSupplierId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteProductSupplier().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); navigateBack(); @@ -202,9 +153,6 @@ public class ProductSupplierDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java index eb69bd16..90ebf645 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java @@ -14,7 +14,7 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.databinding.FragmentPurchaseOrderDetailBinding; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; +import com.example.petstoremobile.viewmodels.PurchaseOrderDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -25,13 +25,13 @@ import dagger.hilt.android.AndroidEntryPoint; public class PurchaseOrderDetailFragment extends Fragment { private FragmentPurchaseOrderDetailBinding binding; - private PurchaseOrderViewModel viewModel; + private PurchaseOrderDetailViewModel viewModel; private long purchaseOrderId; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); + viewModel = new ViewModelProvider(this).get(PurchaseOrderDetailViewModel.class); } /** @@ -67,7 +67,7 @@ public class PurchaseOrderDetailFragment extends Fragment { } private void loadPurchaseOrderData() { - viewModel.getPurchaseOrderById(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadPurchaseOrder(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { PurchaseOrderDTO po = resource.data; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java index 8d05252c..d94b0041 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java @@ -12,7 +12,8 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentRefundBinding; import com.example.petstoremobile.dtos.SaleDTO; -import com.example.petstoremobile.viewmodels.SaleViewModel; +import com.example.petstoremobile.viewmodels.RefundViewModel; +import com.example.petstoremobile.utils.Resource; import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.math.RoundingMode; @@ -22,53 +23,23 @@ import java.util.*; public class RefundFragment extends Fragment { private FragmentRefundBinding binding; - private SaleViewModel saleViewModel; - private SaleDTO currentSale; - private List allSales = new ArrayList<>(); - - // Items available to refund (after accounting for previous refunds) - private List availableItems = new ArrayList<>(); - // Items user has added to refund cart - private List refundCart = new ArrayList<>(); + private RefundViewModel viewModel; private final String[] PAYMENT_METHODS = {"Cash", "Card"}; - // Inner class to track refund items - static class RefundItem { - long prodId; - String productName; - int quantity; - BigDecimal unitPrice; - - RefundItem(long prodId, String productName, int quantity, BigDecimal unitPrice) { - this.prodId = prodId; - this.productName = productName; - this.quantity = quantity; - this.unitPrice = unitPrice; - } - - BigDecimal getTotal() { - return unitPrice != null - ? unitPrice.multiply(BigDecimal.valueOf(quantity)) - : BigDecimal.ZERO; - } - } - @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentRefundBinding.inflate(inflater, container, false); - saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); + viewModel = new ViewModelProvider(this).get(RefundViewModel.class); setupSpinner(); + observeViewModel(); loadAllSales(); - // Pre-fill sale ID if passed from SaleFragment Bundle args = getArguments(); if (args != null && args.containsKey("saleId")) { - long saleId = args.getLong("saleId"); - binding.etRefundSaleId.setText(String.valueOf(saleId)); - // Auto-load after sales are fetched + binding.etRefundSaleId.setText(String.valueOf(args.getLong("saleId"))); } binding.btnLoadSale.setOnClickListener(v -> loadSale()); @@ -83,27 +54,25 @@ public class RefundFragment extends Fragment { android.R.layout.simple_spinner_item, PAYMENT_METHODS)); } + private void observeViewModel() { + viewModel.getAvailableItems().observe(getViewLifecycleOwner(), items -> renderOriginalItems()); + viewModel.getRefundCart().observe(getViewLifecycleOwner(), cart -> { + renderRefundCart(); + updateRefundTotal(); + renderOriginalItems(); // Re-render to reflect quantities in cart + }); + } + private void loadAllSales() { - saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc") - .observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - if (resource.data != null) { - allSales = resource.data.getContent(); - // Auto-load if saleId was pre-filled - Bundle args = getArguments(); - if (args != null && args.containsKey("saleId")) { - loadSale(); - } - } - break; - case ERROR: - Log.e("Refund", "Failed to load sales: " + resource.message); - break; - } - } - }); + viewModel.loadAllSales().observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + viewModel.setAllSales(resource.data.getContent()); + Bundle args = getArguments(); + if (args != null && args.containsKey("saleId")) { + loadSale(); + } + } + }); } private void loadSale() { @@ -120,11 +89,12 @@ public class RefundFragment extends Fragment { return; } - // Find sale in loaded list SaleDTO found = null; - for (SaleDTO s : allSales) { - if (s.getSaleId() != null && s.getSaleId() == saleId) { - found = s; break; + if (viewModel.getAllSalesList() != null) { + for (SaleDTO s : viewModel.getAllSalesList()) { + if (s.getSaleId() != null && s.getSaleId() == saleId) { + found = s; break; + } } } @@ -139,9 +109,9 @@ public class RefundFragment extends Fragment { return; } - currentSale = found; + viewModel.setCurrentSale(found); + SaleDTO currentSale = viewModel.getCurrentSale(); - // Show sale info binding.tvSaleInfo.setVisibility(View.VISIBLE); binding.tvSaleInfo.setText("Sale #" + currentSale.getSaleId() + " | " + (currentSale.getSaleDate() != null @@ -151,7 +121,6 @@ public class RefundFragment extends Fragment { + " | Total: $" + currentSale.getTotalAmount() + " | Payment: " + currentSale.getPaymentMethod()); - // Pre-select payment method if (currentSale.getPaymentMethod() != null) { for (int i = 0; i < PAYMENT_METHODS.length; i++) { if (PAYMENT_METHODS[i].equalsIgnoreCase(currentSale.getPaymentMethod())) { @@ -160,85 +129,40 @@ public class RefundFragment extends Fragment { } } - // Build refundable items accounting for previous refunds - buildRefundableItems(); - - if (availableItems.isEmpty()) { - Toast.makeText(getContext(), - "This sale has no remaining refundable items", Toast.LENGTH_LONG).show(); + if (viewModel.getAvailableItems().getValue() == null || viewModel.getAvailableItems().getValue().isEmpty()) { + Toast.makeText(getContext(), "This sale has no remaining refundable items", Toast.LENGTH_LONG).show(); return; } - // Reset refund cart - refundCart.clear(); - - // Show cards binding.cardOriginalItems.setVisibility(View.VISIBLE); binding.cardRefundItems.setVisibility(View.VISIBLE); binding.cardPayment.setVisibility(View.VISIBLE); binding.btnProcessRefund.setVisibility(View.VISIBLE); - - renderOriginalItems(); - renderRefundCart(); - updateRefundTotal(); - } - - private void buildRefundableItems() { - availableItems.clear(); - if (currentSale.getItems() == null) return; - - // Find all previous refunds for this sale - Map alreadyRefunded = new HashMap<>(); - for (SaleDTO s : allSales) { - if (Boolean.TRUE.equals(s.getIsRefund()) - && currentSale.getSaleId().equals(s.getOriginalSaleId()) - && s.getItems() != null) { - for (SaleDTO.SaleItemDTO item : s.getItems()) { - if (item.getProdId() != null && item.getQuantity() != null) { - alreadyRefunded.merge(item.getProdId(), - Math.abs(item.getQuantity()), Integer::sum); - } - } - } - } - - // Build available items - for (SaleDTO.SaleItemDTO item : currentSale.getItems()) { - if (item.getProdId() == null || item.getQuantity() == null) continue; - int refunded = alreadyRefunded.getOrDefault(item.getProdId(), 0); - int remaining = item.getQuantity() - refunded; - if (remaining > 0) { - availableItems.add(new RefundItem( - item.getProdId(), - item.getProductName() != null ? item.getProductName() : "Unknown", - remaining, - item.getUnitPrice() - )); - } - } } private void renderOriginalItems() { binding.llOriginalItems.removeAllViews(); + List available = viewModel.getAvailableItems().getValue(); + if (available == null) return; - // Header addTableHeader(binding.llOriginalItems); - for (RefundItem item : availableItems) { - // Calculate pending in cart - int pendingQty = 0; - for (RefundItem r : refundCart) { - if (r.prodId == item.prodId) { pendingQty = r.quantity; break; } + for (RefundViewModel.RefundItem item : available) { + int inCart = 0; + if (viewModel.getRefundCart().getValue() != null) { + for (RefundViewModel.RefundItem r : viewModel.getRefundCart().getValue()) { + if (r.prodId == item.prodId) { inCart = r.quantity; break; } + } } - int displayQty = item.quantity - pendingQty; + int displayQty = item.quantity - inCart; if (displayQty <= 0) continue; LinearLayout row = buildItemRow( item.productName, displayQty, item.unitPrice, - true, // show add button - () -> showQuantityDialog(item) + true, + () -> showQuantityDialog(item, displayQty) ); binding.llOriginalItems.addView(row); } @@ -246,8 +170,9 @@ public class RefundFragment extends Fragment { private void renderRefundCart() { binding.llRefundItems.removeAllViews(); + List cart = viewModel.getRefundCart().getValue(); - if (refundCart.isEmpty()) { + if (cart == null || cart.isEmpty()) { TextView empty = new TextView(getContext()); empty.setText("No items added to refund yet"); empty.setTextColor(0xFF888780); @@ -258,18 +183,13 @@ public class RefundFragment extends Fragment { addTableHeader(binding.llRefundItems); - for (RefundItem item : refundCart) { + for (RefundViewModel.RefundItem item : cart) { LinearLayout row = buildItemRow( item.productName, item.quantity, item.unitPrice, - false, // show remove button - () -> { - refundCart.remove(item); - renderOriginalItems(); - renderRefundCart(); - updateRefundTotal(); - } + false, + () -> viewModel.removeFromCart(item) ); binding.llRefundItems.addView(row); } @@ -342,24 +262,10 @@ public class RefundFragment extends Fragment { return row; } - private void showQuantityDialog(RefundItem item) { - // Calculate how many are already in cart - int inCart = 0; - for (RefundItem r : refundCart) { - if (r.prodId == item.prodId) { inCart = r.quantity; break; } - } - int available = item.quantity - inCart; - if (available <= 0) { - Toast.makeText(getContext(), "All units already added to refund", - Toast.LENGTH_SHORT).show(); - return; - } - - // Build dialog + private void showQuantityDialog(RefundViewModel.RefundItem item, int available) { AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); builder.setTitle("Refund Quantity"); - builder.setMessage("Product: " + item.productName - + "\nAvailable: " + available); + builder.setMessage("Product: " + item.productName + "\nAvailable: " + available); EditText input = new EditText(getContext()); input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); @@ -377,36 +283,15 @@ public class RefundFragment extends Fragment { return; } if (qty <= 0) { - Toast.makeText(getContext(), "Quantity must be at least 1", - Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Quantity must be at least 1", Toast.LENGTH_SHORT).show(); return; } if (qty > available) { - Toast.makeText(getContext(), "Cannot exceed " + available, - Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Cannot exceed " + available, Toast.LENGTH_SHORT).show(); return; } - // Add or merge into cart - boolean merged = false; - for (int i = 0; i < refundCart.size(); i++) { - if (refundCart.get(i).prodId == item.prodId) { - RefundItem existing = refundCart.get(i); - refundCart.set(i, new RefundItem(existing.prodId, - existing.productName, - existing.quantity + qty, - existing.unitPrice)); - merged = true; break; - } - } - if (!merged) { - refundCart.add(new RefundItem(item.prodId, item.productName, - qty, item.unitPrice)); - } - - renderOriginalItems(); - renderRefundCart(); - updateRefundTotal(); + viewModel.addToCart(item, qty); }); builder.setNegativeButton("Cancel", null); @@ -415,31 +300,31 @@ public class RefundFragment extends Fragment { private void updateRefundTotal() { BigDecimal total = BigDecimal.ZERO; - for (RefundItem item : refundCart) total = total.add(item.getTotal()); + List cart = viewModel.getRefundCart().getValue(); + if (cart != null) { + for (RefundViewModel.RefundItem item : cart) total = total.add(item.getTotal()); + } binding.tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP)); } private void processRefund() { - if (currentSale == null) { + if (viewModel.getCurrentSale() == null) { Toast.makeText(getContext(), "Load a sale first", Toast.LENGTH_SHORT).show(); return; } - if (refundCart.isEmpty()) { - Toast.makeText(getContext(), "Add at least one item to refund", - Toast.LENGTH_SHORT).show(); + if (viewModel.getRefundCart().getValue() == null || viewModel.getRefundCart().getValue().isEmpty()) { + Toast.makeText(getContext(), "Add at least one item to refund", Toast.LENGTH_SHORT).show(); return; } String payment = PAYMENT_METHODS[binding.spinnerRefundPayment.getSelectedItemPosition()]; - - // Confirm dialog BigDecimal total = BigDecimal.ZERO; - for (RefundItem item : refundCart) total = total.add(item.getTotal()); + for (RefundViewModel.RefundItem item : viewModel.getRefundCart().getValue()) total = total.add(item.getTotal()); final BigDecimal finalTotal = total; new AlertDialog.Builder(requireContext()) .setTitle("Confirm Refund") - .setMessage("Process refund for Sale #" + currentSale.getSaleId() + .setMessage("Process refund for Sale #" + viewModel.getCurrentSale().getSaleId() + "?\nRefund amount: $" + finalTotal.setScale(2, RoundingMode.HALF_UP)) .setPositiveButton("Yes", (d, w) -> submitRefund(payment)) .setNegativeButton("No", null) @@ -447,41 +332,13 @@ public class RefundFragment extends Fragment { } private void submitRefund(String payment) { - // Build sale items list - List items = new ArrayList<>(); - for (RefundItem item : refundCart) { - // Backend expects negative quantity for refunds - items.add(new SaleDTO.SaleItemDTO(item.prodId, -item.quantity)); - } - - SaleDTO dto = new SaleDTO( - currentSale.getStoreId(), - payment, - items, - true, // isRefund = true - currentSale.getSaleId(), // originalSaleId - null // no customer needed - ); - - Log.d("REFUND", "Submitting refund for saleId=" + currentSale.getSaleId() - + " items=" + items.size()); - - saleViewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { + viewModel.submitRefund(payment).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { - switch (resource.status) { - case SUCCESS: - if (resource.data != null) { - Toast.makeText(getContext(), - "Refund #" + resource.data.getSaleId() + " processed successfully!", - Toast.LENGTH_LONG).show(); - navigateBack(); - } - break; - case ERROR: - Log.e("REFUND", "Error: " + resource.message); - Toast.makeText(getContext(), "Error: " + resource.message, - Toast.LENGTH_LONG).show(); - break; + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Refund processed successfully!", Toast.LENGTH_LONG).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); } } }); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java index 690a2fa1..7ae18823 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java @@ -11,7 +11,7 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentSaleDetailBinding; import com.example.petstoremobile.dtos.*; -import com.example.petstoremobile.viewmodels.*; +import com.example.petstoremobile.viewmodels.SaleDetailViewModel; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; @@ -24,18 +24,7 @@ import java.util.*; public class SaleDetailFragment extends Fragment { private FragmentSaleDetailBinding binding; - private SaleViewModel saleViewModel; - private StoreViewModel storeViewModel; - private CustomerViewModel customerViewModel; - private ProductViewModel productViewModel; - - private boolean viewOnly = false; - private long saleId = -1; - - private List storeList = new ArrayList<>(); - private List customerList = new ArrayList<>(); - private List productList = new ArrayList<>(); - private List cartItems = new ArrayList<>(); + private SaleDetailViewModel viewModel; private final String[] PAYMENT_METHODS = { "Cash", "Card"}; @@ -43,17 +32,14 @@ public class SaleDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentSaleDetailBinding.inflate(inflater, container, false); - - saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); - customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); - productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); + viewModel = new ViewModelProvider(this).get(SaleDetailViewModel.class); SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPaymentMethod, PAYMENT_METHODS); + observeViewModel(); handleArguments(); - if (!viewOnly) { + if (!viewModel.isViewOnly()) { loadData(); setupAddItem(); } @@ -65,20 +51,42 @@ public class SaleDetailFragment extends Fragment { return binding.getRoot(); } + private void observeViewModel() { + viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, list, + DropdownDTO::getLabel, "-- Select Store --", -1L, DropdownDTO::getId); + }); + + viewModel.getCustomerList().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleCustomer, list, + DropdownDTO::getLabel, "-- No Customer --", -1L, DropdownDTO::getId); + }); + + viewModel.getProductList().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleProduct, list, + ProductDTO::getProdName, "Select Product", -1L, ProductDTO::getProdId); + }); + + viewModel.getCartItems().observe(getViewLifecycleOwner(), items -> { + renderCartItems(); + updateTotal(); + }); + } + private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("saleId")) { - saleId = a.getLong("saleId"); - viewOnly = a.getBoolean("viewOnly", false); + long saleId = a.getLong("saleId"); + boolean viewOnly = a.getBoolean("viewOnly", false); + viewModel.setSaleId(saleId, viewOnly); + binding.tvSaleMode.setText("Sale #" + saleId); binding.tvSaleDetailId.setText("ID: " + saleId); - // Show refund button for existing non-refund sales if (!a.getBoolean("isRefund", false)) { binding.btnRefundSale.setVisibility(View.VISIBLE); } - // Hide save and input controls for view only if (viewOnly) { binding.btnSaveSale.setVisibility(View.GONE); UIUtils.setViewsEnabled(false, @@ -89,9 +97,9 @@ public class SaleDetailFragment extends Fragment { binding.llExtraInfo.setVisibility(View.VISIBLE); } - // Load sale details loadSaleDetails(); } else { + viewModel.setSaleId(-1, false); binding.tvSaleMode.setText("New Sale"); binding.tvSaleDetailId.setVisibility(View.GONE); binding.btnRefundSale.setVisibility(View.GONE); @@ -100,82 +108,47 @@ public class SaleDetailFragment extends Fragment { } private void loadData() { - loadStores(); - loadCustomers(); - loadProducts(); - } - - private void loadStores() { - storeViewModel.getStoreDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data; - if (binding != null) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleStore, storeList, - DropdownDTO::getLabel, "-- Select Store --", -1L, DropdownDTO::getId); - } - } + viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setStoreList(resource.data); }); - } - - private void loadCustomers() { - customerViewModel.getCustomerDropdowns().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - customerList = resource.data; - if (binding != null) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleCustomer, customerList, - DropdownDTO::getLabel, "-- No Customer --", -1L, DropdownDTO::getId); - } - } + viewModel.loadCustomers().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setCustomerList(resource.data); }); - } - - private void loadProducts() { - productViewModel.getAllProducts(null, null, 0, 200, null).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - productList = resource.data.getContent(); - if (binding != null) { - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerSaleProduct, productList, - ProductDTO::getProdName, "Select Product", -1L, ProductDTO::getProdId); - } - } + viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) viewModel.setProductList(resource.data.getContent()); }); } private void loadSaleDetails() { - saleViewModel.getSaleById(saleId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadSaleDetails().observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { SaleDTO sale = resource.data; - if (binding != null) { - binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount()); - binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount())); - - if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { - binding.llCouponDiscount.setVisibility(View.VISIBLE); - binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount()); - } else { - binding.llCouponDiscount.setVisibility(View.GONE); - } + binding.tvSaleDetailTotal.setText("Total: $" + sale.getTotalAmount()); + binding.tvSaleSubtotal.setText("$" + (sale.getSubtotalAmount() != null ? sale.getSubtotalAmount() : sale.getTotalAmount())); + + if (sale.getCouponDiscountAmount() != null && sale.getCouponDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + binding.llCouponDiscount.setVisibility(View.VISIBLE); + binding.tvSaleCouponDiscount.setText("-$" + sale.getCouponDiscountAmount()); + } else { + binding.llCouponDiscount.setVisibility(View.GONE); + } - if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { - binding.llEmployeeDiscount.setVisibility(View.VISIBLE); - binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount()); - } else { - binding.llEmployeeDiscount.setVisibility(View.GONE); - } + if (sale.getEmployeeDiscountAmount() != null && sale.getEmployeeDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + binding.llEmployeeDiscount.setVisibility(View.VISIBLE); + binding.tvSaleEmployeeDiscount.setText("-$" + sale.getEmployeeDiscountAmount()); + } else { + binding.llEmployeeDiscount.setVisibility(View.GONE); + } - binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "—"); - binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0)); + binding.tvSaleChannel.setText(sale.getChannel() != null ? sale.getChannel() : "—"); + binding.tvSalePoints.setText(String.valueOf(sale.getPointsEarned() != null ? sale.getPointsEarned() : 0)); - SpinnerUtils.setSelectionByValue(binding.spinnerPaymentMethod, sale.getPaymentMethod()); + SpinnerUtils.setSelectionByValue(binding.spinnerPaymentMethod, sale.getPaymentMethod()); - // Display items - if (sale.getItems() != null) { - binding.llSaleItems.removeAllViews(); - for (SaleDTO.SaleItemDTO item : sale.getItems()) { - addItemRow(item.getProductName(), - Math.abs(item.getQuantity()), - item.getUnitPrice()); - } + if (sale.getItems() != null) { + binding.llSaleItems.removeAllViews(); + for (SaleDTO.SaleItemDTO item : sale.getItems()) { + addItemRow(item.getProductName(), Math.abs(item.getQuantity()), item.getUnitPrice()); } } } @@ -194,31 +167,46 @@ public class SaleDetailFragment extends Fragment { return; } int qty; - try { - qty = Integer.parseInt(qtyStr); - } catch (Exception e) { + try { qty = Integer.parseInt(qtyStr); } + catch (Exception e) { binding.etSaleQuantity.setError("Invalid quantity"); return; } - ProductDTO product = productList.get(binding.spinnerSaleProduct.getSelectedItemPosition() - 1); + ProductDTO product = viewModel.getProductList().getValue().get(binding.spinnerSaleProduct.getSelectedItemPosition() - 1); - // Check if product already in cart - for (SaleDTO.SaleItemDTO existing : cartItems) { + for (SaleDTO.SaleItemDTO existing : viewModel.getCartItems().getValue()) { if (existing.getProdId().equals(product.getProdId())) { Toast.makeText(getContext(), "Product already added", Toast.LENGTH_SHORT).show(); return; } } - SaleDTO.SaleItemDTO item = new SaleDTO.SaleItemDTO(product.getProdId(), qty); - cartItems.add(item); - addItemRow(product.getProdName(), qty, product.getProdPrice()); - updateTotal(); + viewModel.addToCart(new SaleDTO.SaleItemDTO(product.getProdId(), qty)); binding.etSaleQuantity.setText(""); }); } + private void renderCartItems() { + binding.llSaleItems.removeAllViews(); + List items = viewModel.getCartItems().getValue(); + List products = viewModel.getProductList().getValue(); + if (items == null || products == null) return; + + for (SaleDTO.SaleItemDTO item : items) { + String name = "Unknown"; + BigDecimal price = BigDecimal.ZERO; + for (ProductDTO p : products) { + if (p.getProdId().equals(item.getProdId())) { + name = p.getProdName(); + price = p.getProdPrice(); + break; + } + } + addItemRow(name, item.getQuantity(), price); + } + } + private void addItemRow(String name, int qty, BigDecimal price) { if (getContext() == null) return; LinearLayout row = new LinearLayout(getContext()); @@ -226,18 +214,15 @@ public class SaleDetailFragment extends Fragment { row.setPadding(0, 8, 0, 8); TextView tvName = new TextView(getContext()); - tvName.setLayoutParams(new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)); + tvName.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)); tvName.setText(name); TextView tvQty = new TextView(getContext()); - tvQty.setLayoutParams(new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvQty.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); tvQty.setText("x" + qty); TextView tvPrice = new TextView(getContext()); - tvPrice.setLayoutParams(new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvPrice.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); tvPrice.setText(price != null ? "$" + price : ""); row.addView(tvName); @@ -247,16 +232,7 @@ public class SaleDetailFragment extends Fragment { } private void updateTotal() { - BigDecimal total = BigDecimal.ZERO; - for (SaleDTO.SaleItemDTO item : cartItems) { - for (ProductDTO p : productList) { - if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) { - total = total.add(p.getProdPrice() - .multiply(BigDecimal.valueOf(item.getQuantity()))); - break; - } - } - } + BigDecimal total = viewModel.calculateSubtotal(); binding.tvSaleSubtotal.setText("$" + total); binding.tvSaleDetailTotal.setText("Total: $" + total); } @@ -266,40 +242,28 @@ public class SaleDetailFragment extends Fragment { Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return; } - if (cartItems.isEmpty()) { + if (viewModel.getCartItems().getValue() == null || viewModel.getCartItems().getValue().isEmpty()) { Toast.makeText(getContext(), "Add at least one item", Toast.LENGTH_SHORT).show(); return; } - DropdownDTO store = storeList.get(binding.spinnerSaleStore.getSelectedItemPosition() - 1); + DropdownDTO store = viewModel.getStoreList().getValue().get(binding.spinnerSaleStore.getSelectedItemPosition() - 1); String payment = PAYMENT_METHODS[binding.spinnerPaymentMethod.getSelectedItemPosition()]; - // Optional customer Long customerId = null; if (binding.spinnerSaleCustomer.getSelectedItemPosition() > 0) { - customerId = customerList.get(binding.spinnerSaleCustomer.getSelectedItemPosition() - 1) - .getId(); + customerId = viewModel.getCustomerList().getValue().get(binding.spinnerSaleCustomer.getSelectedItemPosition() - 1).getId(); } - SaleDTO dto = new SaleDTO( - store.getId(), - payment, - cartItems, - false, - null, - customerId); + SaleDTO dto = new SaleDTO(store.getId(), payment, viewModel.getCartItems().getValue(), false, null, customerId); - saleViewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { + viewModel.createSale(dto).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { - switch (resource.status) { - case SUCCESS: - Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); - navigateBack(); - break; - case ERROR: - Log.e("SALE_SAVE", "Error: " + resource.message); - DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message); - break; + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + DialogUtils.showInfoDialog(requireContext(), "Save Error", resource.message); } } }); @@ -309,7 +273,7 @@ public class SaleDetailFragment extends Fragment { DialogUtils.showConfirmDialog(requireContext(), "Process Refund", "Are you sure you want to process a refund for this sale?", () -> { Bundle args = new Bundle(); - args.putLong("saleId", saleId); + args.putLong("saleId", viewModel.getSaleId()); NavHostFragment.findNavController(this).navigate(R.id.nav_refund, args); }); } 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 49c51141..7fdaae9b 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 @@ -20,7 +20,7 @@ import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.viewmodels.ServiceViewModel; +import com.example.petstoremobile.viewmodels.ServiceDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -31,15 +31,12 @@ import dagger.hilt.android.AndroidEntryPoint; public class ServiceDetailFragment extends Fragment { private FragmentServiceDetailBinding binding; - private long serviceId; - private boolean isEditing = false; - - private ServiceViewModel viewModel; + private ServiceDetailViewModel viewModel; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ServiceViewModel.class); + viewModel = new ViewModelProvider(this).get(ServiceDetailViewModel.class); } @Override @@ -53,10 +50,8 @@ public class ServiceDetailFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - //get controls from layout and display the view depending on the mode handleArguments(); - //set button click listeners binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSaveService.setOnClickListener(v -> saveService()); binding.btnDeleteService.setOnClickListener(v -> deleteService()); @@ -68,63 +63,44 @@ public class ServiceDetailFragment extends Fragment { binding = null; } - /** - * Handles the saving of service data (adding or updating). - */ private void saveService() { - // Validates all fields using InputValidator if (!InputValidator.isNotEmpty(binding.etServiceName, "Service Name")) return; if (!InputValidator.isNotEmpty(binding.etServiceDesc, "Description")) return; if (!InputValidator.isPositiveInteger(binding.etServiceDuration, "Duration")) return; if (!InputValidator.isPositiveDecimal(binding.etServicePrice, "Price")) return; - //get all the values from the fields String name = binding.etServiceName.getText().toString().trim(); String desc = binding.etServiceDesc.getText().toString().trim(); int duration = Integer.parseInt(binding.etServiceDuration.getText().toString().trim()); double price = Double.parseDouble(binding.etServicePrice.getText().toString().trim()); - //create a service object to send to the API ServiceDTO serviceDTO = new ServiceDTO(); serviceDTO.setServiceName(name); serviceDTO.setServiceDesc(desc); serviceDTO.setServiceDuration(duration); serviceDTO.setServicePrice(price); - //check if the service is being edited or added - if (isEditing) { - // Update existing service - serviceDTO.setServiceId(serviceId); - viewModel.updateService(serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) serviceId); + viewModel.saveService(serviceDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + if (viewModel.isEditing()) { + ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) viewModel.getServiceId()); Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - viewModel.createService(serviceDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { + } else { ActivityLogger.log(requireContext(), "Added new Service: " + name); Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Displays a confirmation dialog and handles the deletion of a service. - */ private void deleteService() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () -> - viewModel.deleteService(serviceId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteService().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) serviceId); + ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) viewModel.getServiceId()); Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -133,30 +109,20 @@ public class ServiceDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Handles arguments passed to the fragment to determine if it's in edit or add mode. - */ private void handleArguments() { - // Service is being edited if the bundle contains a serviceId if (getArguments() != null && getArguments().containsKey("serviceId")) { - // Get service data from arguments and populate fields - isEditing = true; - serviceId = getArguments().getLong("serviceId"); + long serviceId = getArguments().getLong("serviceId"); + viewModel.setServiceId(serviceId); binding.tvMode.setText("Edit Service"); binding.tvServiceId.setText("ID: " + serviceId); binding.btnDeleteService.setVisibility(View.VISIBLE); loadServiceData(); } else { - // Service is being added - // Set default values for add a new service - isEditing = false; + viewModel.setServiceId(-1); binding.tvMode.setText("Add Service"); binding.tvServiceId.setVisibility(View.GONE); binding.btnDeleteService.setVisibility(View.GONE); @@ -164,11 +130,8 @@ public class ServiceDetailFragment extends Fragment { } } - /** - * Fetches specific service details from the backend using the ID. - */ private void loadServiceData() { - viewModel.getServiceById(serviceId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadService().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { ServiceDTO s = resource.data; 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 508282bc..0013a277 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 @@ -1,7 +1,6 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; -import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; @@ -12,16 +11,15 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentStaffDetailBinding; import com.example.petstoremobile.dtos.EmployeeDTO; -import com.example.petstoremobile.viewmodels.EmployeeViewModel; +import com.example.petstoremobile.viewmodels.StaffDetailViewModel; +import com.example.petstoremobile.utils.Resource; import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class StaffDetailFragment extends Fragment { private FragmentStaffDetailBinding binding; - private EmployeeViewModel employeeViewModel; - private long employeeId = -1; - private boolean isEditing = false; + private StaffDetailViewModel viewModel; private final String[] ROLES = {"STAFF", "ADMIN"}; private final String[] STATUSES = {"Active", "Inactive"}; @@ -30,7 +28,7 @@ public class StaffDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentStaffDetailBinding.inflate(inflater, container, false); - employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class); + viewModel = new ViewModelProvider(this).get(StaffDetailViewModel.class); setupSpinners(); handleArguments(); @@ -51,8 +49,8 @@ public class StaffDetailFragment extends Fragment { private void handleArguments() { Bundle a = getArguments(); if (a != null && a.getBoolean("isEditing", false)) { - isEditing = true; - employeeId = a.getLong("employeeId", -1); + long employeeId = a.getLong("employeeId", -1); + viewModel.setEmployeeId(employeeId, true); binding.tvStaffMode.setText("Edit Staff Account"); binding.tvStaffId.setText("ID: " + employeeId); @@ -64,7 +62,6 @@ public class StaffDetailFragment extends Fragment { binding.etStaffPhone.setText(a.getString("phone", "")); binding.btnDeleteStaff.setVisibility(View.VISIBLE); - // Pre-fill role String role = a.getString("role", "STAFF"); for (int i = 0; i < ROLES.length; i++) { if (ROLES[i].equals(role)) { @@ -73,13 +70,11 @@ public class StaffDetailFragment extends Fragment { } } - // Pre-fill status boolean active = a.getBoolean("active", true); binding.spinnerStaffStatus.setSelection(active ? 0 : 1); } else { - isEditing = false; - employeeId = -1; + viewModel.setEmployeeId(-1, false); binding.tvStaffMode.setText("Add Staff Account"); binding.btnDeleteStaff.setVisibility(View.GONE); binding.tvStaffId.setVisibility(View.GONE); @@ -97,12 +92,11 @@ public class StaffDetailFragment extends Fragment { String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()]; boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0; - // Validation if (username.isEmpty()) { binding.etStaffUsername.setError("Required"); return; } - if (!isEditing && password.isEmpty()) { + if (!viewModel.isEditing() && password.isEmpty()) { binding.etStaffPassword.setError("Required for new account"); return; } - if (!isEditing && password.length() < 6) { + if (!viewModel.isEditing() && password.length() < 6) { binding.etStaffPassword.setError("At least 6 characters"); return; } if (firstName.isEmpty()) { binding.etStaffFirstName.setError("Required"); return; } @@ -121,35 +115,16 @@ public class StaffDetailFragment extends Fragment { active ); - if (isEditing && employeeId > 0) { - employeeViewModel.updateEmployee(employeeId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - Toast.makeText(getContext(), "Updated successfully", Toast.LENGTH_SHORT).show(); - navigateBack(); - break; - case ERROR: - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); - break; - } + viewModel.saveEmployee(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), viewModel.isEditing() ? "Updated successfully" : "Staff account created", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); } - }); - } else { - employeeViewModel.createEmployee(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - Toast.makeText(getContext(), "Staff account created", Toast.LENGTH_SHORT).show(); - navigateBack(); - break; - case ERROR: - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_LONG).show(); - break; - } - } - }); - } + } + }); } private void confirmDelete() { @@ -157,16 +132,13 @@ public class StaffDetailFragment extends Fragment { .setTitle("Delete Staff Account?") .setMessage("This will permanently delete this staff account.") .setPositiveButton("Yes", (d, w) -> - employeeViewModel.deleteEmployee(employeeId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteEmployee().observe(getViewLifecycleOwner(), resource -> { if (resource != null) { - switch (resource.status) { - case SUCCESS: - navigateBack(); - break; - case ERROR: - Toast.makeText(getContext(), "Delete failed: " + resource.message, - Toast.LENGTH_SHORT).show(); - break; + if (resource.status == Resource.Status.SUCCESS) { + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, + Toast.LENGTH_SHORT).show(); } } })) 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 4935cb8b..62c7c381 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 @@ -20,7 +20,7 @@ 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.SupplierViewModel; +import com.example.petstoremobile.viewmodels.SupplierDetailViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -31,15 +31,12 @@ import dagger.hilt.android.AndroidEntryPoint; public class SupplierDetailFragment extends Fragment { private FragmentSupplierDetailBinding binding; - private long supId; - private boolean isEditing = false; - - private SupplierViewModel viewModel; + private SupplierDetailViewModel viewModel; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + viewModel = new ViewModelProvider(this).get(SupplierDetailViewModel.class); } @Override @@ -53,12 +50,9 @@ public class SupplierDetailFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // Add phone number formatting (CA) and limit length to 14 characters UIUtils.formatPhoneInput(binding.etSupPhone); - handleArguments(); - //set button click listeners binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSaveSupplier.setOnClickListener(v -> saveSupplier()); binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); @@ -70,25 +64,19 @@ public class SupplierDetailFragment extends Fragment { binding = null; } - /** - * Handles the saving of supplier data (adding or updating). - */ private void saveSupplier() { - // Validates all fields using InputValidator if (!InputValidator.isNotEmpty(binding.etSupCompany, "Company Name")) return; if (!InputValidator.isNotEmpty(binding.etSupContactFirstName, "First Name")) return; if (!InputValidator.isNotEmpty(binding.etSupContactLastName, "Last Name")) return; if (!InputValidator.isValidEmail(binding.etSupEmail)) return; if (!InputValidator.isValidPhone(binding.etSupPhone)) return; - //get all the values from the fields String company = binding.etSupCompany.getText().toString().trim(); String firstName = binding.etSupContactFirstName.getText().toString().trim(); String lastName = binding.etSupContactLastName.getText().toString().trim(); String email = binding.etSupEmail.getText().toString().trim(); String phone = binding.etSupPhone.getText().toString().trim(); - //create a supplier object to send to the API SupplierDTO supplierDTO = new SupplierDTO(); supplierDTO.setSupCompany(company); supplierDTO.setSupContactFirstName(firstName); @@ -96,41 +84,27 @@ public class SupplierDetailFragment extends Fragment { supplierDTO.setSupEmail(email); supplierDTO.setSupPhone(phone); - //check if the supplier is being edited or added - if (isEditing) { - // Update existing supplier - supplierDTO.setSupId(supId); - viewModel.updateSupplier(supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) supId); + viewModel.saveSupplier(supplierDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + if (viewModel.isEditing()) { + ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) viewModel.getSupId()); Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); - } - }); - } else { - // Add new supplier - viewModel.createSupplier(supplierDTO).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS) { + } else { ActivityLogger.log(requireContext(), "Added new Supplier: " + company); Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else if (resource.status == Resource.Status.ERROR) { - Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - }); - } + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } - /** - * Displays a confirmation dialog and handles the deletion of a supplier. - */ private void deleteSupplier() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () -> - viewModel.deleteSupplier(supId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteSupplier().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) supId); + ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) viewModel.getSupId()); Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -139,31 +113,21 @@ public class SupplierDetailFragment extends Fragment { })); } - /** - * Navigates back to the previous screen. - */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - /** - * Handles arguments passed to the fragment to determine if it's in edit or add mode. - */ private void handleArguments() { - // Supplier is being edited if the bundle contains a supId if (getArguments() != null && getArguments().containsKey("supId")) { - // Get supplier data from arguments and populate fields - isEditing = true; - supId = getArguments().getLong("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); loadSupplierData(); } else { - // Supplier is being added - // Set default values for add a new supplier - isEditing = false; + viewModel.setSupId(-1); binding.tvMode.setText("Add Supplier"); binding.tvSupId.setVisibility(View.GONE); binding.btnDeleteSupplier.setVisibility(View.GONE); @@ -171,11 +135,8 @@ public class SupplierDetailFragment extends Fragment { } } - /** - * Fetches specific supplier details from the backend using the ID. - */ private void loadSupplierData() { - viewModel.getSupplierById(supId).observe(getViewLifecycleOwner(), resource -> { + viewModel.loadSupplier().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { SupplierDTO s = resource.data; 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 new file mode 100644 index 00000000..f6e24cd6 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java @@ -0,0 +1,102 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.dtos.DropdownDTO; +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.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AdoptionDetailViewModel extends ViewModel { + private final AdoptionRepository adoptionRepository; + private final PetRepository petRepository; + private final CustomerRepository customerRepository; + 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<>()); + + @Inject + public AdoptionDetailViewModel(AdoptionRepository adoptionRepository, PetRepository petRepository, + CustomerRepository customerRepository, StoreRepository storeRepository) { + this.adoptionRepository = adoptionRepository; + this.petRepository = petRepository; + this.customerRepository = customerRepository; + this.storeRepository = storeRepository; + } + + public void setAdoptionId(long id) { + this.adoptionId = id; + this.isEditing = id != -1; + } + + public long getAdoptionId() { + return adoptionId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> loadAdoption() { + return adoptionRepository.getAdoptionById(adoptionId); + } + + 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); + } + + public LiveData> saveAdoption(AdoptionDTO dto) { + if (isEditing) { + return adoptionRepository.updateAdoption(adoptionId, dto); + } else { + 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; } +} 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 new file mode 100644 index 00000000..a76785af --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java @@ -0,0 +1,79 @@ +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.InventoryDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.repositories.InventoryRepository; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class InventoryDetailViewModel extends ViewModel { + private final InventoryRepository inventoryRepository; + private final StoreRepository storeRepository; + private final ProductRepository productRepository; + + private long inventoryId = -1; + private boolean isEditing = false; + + private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public InventoryDetailViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository, ProductRepository productRepository) { + this.inventoryRepository = inventoryRepository; + this.storeRepository = storeRepository; + this.productRepository = productRepository; + } + + public void setInventoryId(long id) { + this.inventoryId = id; + this.isEditing = id != -1; + } + + public long getInventoryId() { return inventoryId; } + public boolean isEditing() { return isEditing; } + + public LiveData> loadInventory() { + return inventoryRepository.getInventoryById(inventoryId); + } + + public LiveData>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData>> loadProducts() { + return productRepository.getAllProducts(null, null, 0, 500, "prodName"); + } + + public LiveData> saveInventory(InventoryDTO dto) { + if (isEditing) { + return inventoryRepository.updateInventory(inventoryId, dto); + } else { + return inventoryRepository.createInventory(dto); + } + } + + public LiveData> deleteInventory() { + return inventoryRepository.deleteInventory(inventoryId); + } + + 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; } +} 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 new file mode 100644 index 00000000..00c074e9 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java @@ -0,0 +1,94 @@ +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.PetDTO; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class PetDetailViewModel extends ViewModel { + 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 long petId = -1; + private boolean isEditing = false; + + @Inject + public PetDetailViewModel(PetRepository petRepository, CustomerRepository customerRepository, StoreRepository storeRepository) { + this.petRepository = petRepository; + this.customerRepository = customerRepository; + this.storeRepository = storeRepository; + } + + public void setPetId(long id) { + this.petId = id; + this.isEditing = id != -1; + } + + public long getPetId() { + return petId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> loadPet() { + return petRepository.getPetById(petId); + } + + public LiveData>> loadCustomers() { + return customerRepository.getCustomerDropdowns(); + } + + public LiveData>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData> savePet(PetDTO petDTO) { + if (isEditing) { + petDTO.setPetId(petId); + return petRepository.updatePet(petId, petDTO); + } else { + 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; + } +} 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 new file mode 100644 index 00000000..9ec0628a --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java @@ -0,0 +1,85 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.repositories.CategoryRepository; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import okhttp3.MultipartBody; + +@HiltViewModel +public class ProductDetailViewModel extends ViewModel { + private final ProductRepository productRepository; + private final CategoryRepository categoryRepository; + + private final MutableLiveData> categoryList = new MutableLiveData<>(new ArrayList<>()); + private long prodId = -1; + private boolean isEditing = false; + + @Inject + public ProductDetailViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) { + this.productRepository = productRepository; + this.categoryRepository = categoryRepository; + } + + public void setProdId(long id) { + this.prodId = id; + this.isEditing = id != -1; + } + + public long getProdId() { + return prodId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData>> loadCategories() { + return categoryRepository.getAllCategories(0, 100); + } + + public LiveData> loadProduct() { + return productRepository.getProductById(prodId); + } + + public LiveData> saveProduct(ProductDTO dto) { + if (isEditing) { + return productRepository.updateProduct(prodId, dto); + } else { + return productRepository.createProduct(dto); + } + } + + public LiveData> deleteProduct() { + return productRepository.deleteProduct(prodId); + } + + public LiveData> uploadProductImage(MultipartBody.Part image) { + return productRepository.uploadProductImage(prodId, image); + } + + public LiveData> deleteProductImage() { + return productRepository.deleteProductImage(prodId); + } + + public void setCategoryList(List list) { + categoryList.setValue(list); + } + + public LiveData> getCategoryList() { + return categoryList; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java new file mode 100644 index 00000000..552a99fc --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java @@ -0,0 +1,78 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.dtos.ProductSupplierDTO; +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.repositories.ProductSupplierRepository; +import com.example.petstoremobile.repositories.SupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ProductSupplierDetailViewModel extends ViewModel { + private final ProductSupplierRepository psRepository; + private final ProductRepository productRepository; + private final SupplierRepository supplierRepository; + + private boolean isEditing = false; + private long editProductId = -1; + private long editSupplierId = -1; + + private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> supplierList = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public ProductSupplierDetailViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) { + this.psRepository = psRepository; + this.productRepository = productRepository; + this.supplierRepository = supplierRepository; + } + + public void setEditMode(long productId, long supplierId) { + this.isEditing = true; + this.editProductId = productId; + this.editSupplierId = supplierId; + } + + public boolean isEditing() { return isEditing; } + public long getEditProductId() { return editProductId; } + public long getEditSupplierId() { return editSupplierId; } + + public LiveData>> loadProducts() { + return productRepository.getAllProducts(null, null, 0, 200, "prodName"); + } + + public LiveData>> loadSuppliers() { + return supplierRepository.getAllSuppliers(0, 200, null, "supCompany"); + } + + public LiveData> saveProductSupplier(ProductSupplierDTO dto) { + if (isEditing) { + return psRepository.updateProductSupplier(editProductId, editSupplierId, dto); + } else { + return psRepository.createProductSupplier(dto); + } + } + + public LiveData> deleteProductSupplier() { + return psRepository.deleteProductSupplier(editProductId, editSupplierId); + } + + public void setProductList(List list) { productList.setValue(list); } + public LiveData> getProductList() { return productList; } + + public void setSupplierList(List list) { supplierList.setValue(list); } + public LiveData> getSupplierList() { return supplierList; } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java new file mode 100644 index 00000000..436cfa4c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java @@ -0,0 +1,26 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PurchaseOrderDTO; +import com.example.petstoremobile.repositories.PurchaseOrderRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class PurchaseOrderDetailViewModel extends ViewModel { + private final PurchaseOrderRepository repository; + + @Inject + public PurchaseOrderDetailViewModel(PurchaseOrderRepository repository) { + this.repository = repository; + } + + public LiveData> loadPurchaseOrder(long id) { + return repository.getPurchaseOrderById(id); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java new file mode 100644 index 00000000..d2b13732 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java @@ -0,0 +1,167 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.repositories.SaleRepository; +import com.example.petstoremobile.utils.Resource; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class RefundViewModel extends ViewModel { + private final SaleRepository saleRepository; + + private final MutableLiveData> allSales = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData currentSale = new MutableLiveData<>(); + private final MutableLiveData> availableItems = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> refundCart = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public RefundViewModel(SaleRepository saleRepository) { + this.saleRepository = saleRepository; + } + + public LiveData>> loadAllSales() { + return saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc"); + } + + public void setAllSales(List sales) { + allSales.setValue(sales); + } + + public List getAllSalesList() { + return allSales.getValue(); + } + + public void setCurrentSale(SaleDTO sale) { + currentSale.setValue(sale); + buildRefundableItems(); + } + + public SaleDTO getCurrentSale() { + return currentSale.getValue(); + } + + public LiveData> getAvailableItems() { + return availableItems; + } + + public LiveData> getRefundCart() { + return refundCart; + } + + private void buildRefundableItems() { + SaleDTO sale = currentSale.getValue(); + List sales = allSales.getValue(); + if (sale == null || sales == null || sale.getItems() == null) { + availableItems.setValue(new ArrayList<>()); + return; + } + + Map alreadyRefunded = new HashMap<>(); + for (SaleDTO s : sales) { + if (Boolean.TRUE.equals(s.getIsRefund()) + && sale.getSaleId().equals(s.getOriginalSaleId()) + && s.getItems() != null) { + for (SaleDTO.SaleItemDTO item : s.getItems()) { + if (item.getProdId() != null && item.getQuantity() != null) { + alreadyRefunded.merge(item.getProdId(), + Math.abs(item.getQuantity()), Integer::sum); + } + } + } + } + + List items = new ArrayList<>(); + for (SaleDTO.SaleItemDTO item : sale.getItems()) { + if (item.getProdId() == null || item.getQuantity() == null) continue; + int refunded = alreadyRefunded.getOrDefault(item.getProdId(), 0); + int remaining = item.getQuantity() - refunded; + if (remaining > 0) { + items.add(new RefundItem( + item.getProdId(), + item.getProductName() != null ? item.getProductName() : "Unknown", + remaining, + item.getUnitPrice() + )); + } + } + availableItems.setValue(items); + refundCart.setValue(new ArrayList<>()); + } + + public void addToCart(RefundItem item, int qty) { + List cart = new ArrayList<>(refundCart.getValue()); + boolean merged = false; + for (int i = 0; i < cart.size(); i++) { + if (cart.get(i).prodId == item.prodId) { + RefundItem existing = cart.get(i); + cart.set(i, new RefundItem(existing.prodId, existing.productName, existing.quantity + qty, existing.unitPrice)); + merged = true; + break; + } + } + if (!merged) { + cart.add(new RefundItem(item.prodId, item.productName, qty, item.unitPrice)); + } + refundCart.setValue(cart); + } + + public void removeFromCart(RefundItem item) { + List cart = new ArrayList<>(refundCart.getValue()); + cart.remove(item); + refundCart.setValue(cart); + } + + public LiveData> submitRefund(String paymentMethod) { + SaleDTO sale = currentSale.getValue(); + List cart = refundCart.getValue(); + if (sale == null || cart == null || cart.isEmpty()) return null; + + List items = new ArrayList<>(); + for (RefundItem item : cart) { + items.add(new SaleDTO.SaleItemDTO(item.prodId, -item.quantity)); + } + + SaleDTO dto = new SaleDTO( + sale.getStoreId(), + paymentMethod, + items, + true, + sale.getSaleId(), + null + ); + + return saleRepository.createSale(dto); + } + + public static class RefundItem { + public long prodId; + public String productName; + public int quantity; + public BigDecimal unitPrice; + + public RefundItem(long prodId, String productName, int quantity, BigDecimal unitPrice) { + this.prodId = prodId; + this.productName = productName; + this.quantity = quantity; + this.unitPrice = unitPrice; + } + + public BigDecimal getTotal() { + return unitPrice != null ? unitPrice.multiply(BigDecimal.valueOf(quantity)) : BigDecimal.ZERO; + } + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java new file mode 100644 index 00000000..bf84ab32 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java @@ -0,0 +1,109 @@ +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.ProductDTO; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.repositories.SaleRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SaleDetailViewModel extends ViewModel { + private final SaleRepository saleRepository; + private final StoreRepository storeRepository; + private final CustomerRepository customerRepository; + private final ProductRepository productRepository; + + private long saleId = -1; + private boolean viewOnly = false; + + private final MutableLiveData> storeList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> customerList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> productList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> cartItems = new MutableLiveData<>(new ArrayList<>()); + + @Inject + public SaleDetailViewModel(SaleRepository saleRepository, StoreRepository storeRepository, + CustomerRepository customerRepository, ProductRepository productRepository) { + this.saleRepository = saleRepository; + this.storeRepository = storeRepository; + this.customerRepository = customerRepository; + this.productRepository = productRepository; + } + + public void setSaleId(long id, boolean viewOnly) { + this.saleId = id; + this.viewOnly = viewOnly; + } + + public long getSaleId() { return saleId; } + public boolean isViewOnly() { return viewOnly; } + + public LiveData> loadSaleDetails() { + return saleRepository.getSaleById(saleId); + } + + public LiveData>> loadStores() { + return storeRepository.getStoreDropdowns(); + } + + public LiveData>> loadCustomers() { + return customerRepository.getCustomerDropdowns(); + } + + public LiveData>> loadProducts() { + return productRepository.getAllProducts(null, null, 0, 200, null); + } + + public LiveData> createSale(SaleDTO sale) { + return saleRepository.createSale(sale); + } + + public void setStoreList(List list) { storeList.setValue(list); } + public LiveData> getStoreList() { return storeList; } + + public void setCustomerList(List list) { customerList.setValue(list); } + public LiveData> getCustomerList() { return customerList; } + + public void setProductList(List list) { productList.setValue(list); } + public LiveData> getProductList() { return productList; } + + public void addToCart(SaleDTO.SaleItemDTO item) { + List currentCart = new ArrayList<>(cartItems.getValue()); + currentCart.add(item); + cartItems.setValue(currentCart); + } + + public LiveData> getCartItems() { return cartItems; } + + public BigDecimal calculateSubtotal() { + BigDecimal total = BigDecimal.ZERO; + List items = cartItems.getValue(); + List products = productList.getValue(); + if (items != null && products != null) { + for (SaleDTO.SaleItemDTO item : items) { + for (ProductDTO p : products) { + if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) { + total = total.add(p.getProdPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); + break; + } + } + } + } + return total; + } +} 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 new file mode 100644 index 00000000..fca74229 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java @@ -0,0 +1,54 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.repositories.ServiceRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ServiceDetailViewModel extends ViewModel { + private final ServiceRepository repository; + private long serviceId = -1; + private boolean isEditing = false; + + @Inject + public ServiceDetailViewModel(ServiceRepository repository) { + this.repository = repository; + } + + public void setServiceId(long id) { + this.serviceId = id; + this.isEditing = id != -1; + } + + public long getServiceId() { + return serviceId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> loadService() { + return repository.getServiceById(serviceId); + } + + public LiveData> saveService(ServiceDTO dto) { + if (isEditing) { + dto.setServiceId(serviceId); + return repository.updateService(serviceId, dto); + } else { + return repository.createService(dto); + } + } + + public LiveData> deleteService() { + return repository.deleteService(serviceId); + } +} 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 new file mode 100644 index 00000000..91162405 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java @@ -0,0 +1,49 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.repositories.EmployeeRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class StaffDetailViewModel extends ViewModel { + private final EmployeeRepository repository; + private long employeeId = -1; + private boolean isEditing = false; + + @Inject + public StaffDetailViewModel(EmployeeRepository repository) { + this.repository = repository; + } + + public void setEmployeeId(long id, boolean isEditing) { + this.employeeId = id; + this.isEditing = isEditing; + } + + public long getEmployeeId() { + return employeeId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> saveEmployee(EmployeeDTO dto) { + if (isEditing && employeeId > 0) { + return repository.updateEmployee(employeeId, dto); + } else { + return repository.createEmployee(dto); + } + } + + public LiveData> deleteEmployee() { + return repository.deleteEmployee(employeeId); + } +} 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 new file mode 100644 index 00000000..591beb52 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java @@ -0,0 +1,54 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.repositories.SupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SupplierDetailViewModel extends ViewModel { + private final SupplierRepository repository; + private long supId = -1; + private boolean isEditing = false; + + @Inject + public SupplierDetailViewModel(SupplierRepository repository) { + this.repository = repository; + } + + public void setSupId(long id) { + this.supId = id; + this.isEditing = id != -1; + } + + public long getSupId() { + return supId; + } + + public boolean isEditing() { + return isEditing; + } + + public LiveData> loadSupplier() { + return repository.getSupplierById(supId); + } + + public LiveData> saveSupplier(SupplierDTO dto) { + if (isEditing) { + dto.setSupId(supId); + return repository.updateSupplier(supId, dto); + } else { + return repository.createSupplier(dto); + } + } + + public LiveData> deleteSupplier() { + return repository.deleteSupplier(supId); + } +} -- 2.49.1 From 38b830509fa7810c0547b44422cab5c1c920c61d Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:44:04 -0600 Subject: [PATCH 28/46] refactored viewmodels for listfragments --- .../fragments/ChatFragment.java | 368 ++++-------------- .../listfragments/AdoptionFragment.java | 135 ++----- .../listfragments/AnalyticsFragment.java | 291 +++++--------- .../listfragments/AppointmentFragment.java | 146 ++----- .../listfragments/InventoryFragment.java | 153 ++------ .../fragments/listfragments/PetFragment.java | 158 +++----- .../listfragments/ProductFragment.java | 139 +++---- .../ProductSupplierFragment.java | 148 ++----- .../listfragments/PurchaseOrderFragment.java | 119 ++---- .../fragments/listfragments/SaleFragment.java | 109 ++---- .../listfragments/ServiceFragment.java | 94 +---- .../listfragments/StaffFragment.java | 80 ++-- .../listfragments/SupplierFragment.java | 87 +---- .../PetProfileFragment.java | 33 +- .../viewmodels/AdoptionListViewModel.java | 83 ++++ .../viewmodels/AnalyticsViewModel.java | 163 ++++++++ .../viewmodels/AppointmentListViewModel.java | 65 ++++ .../viewmodels/ChatListViewModel.java | 132 +++++++ .../viewmodels/InventoryListViewModel.java | 81 ++++ .../viewmodels/PetListViewModel.java | 69 ++++ .../viewmodels/PetProfileViewModel.java | 35 ++ .../viewmodels/ProductListViewModel.java | 60 +++ .../ProductSupplierListViewModel.java | 77 ++++ .../PurchaseOrderListViewModel.java | 60 +++ .../viewmodels/SaleListViewModel.java | 77 ++++ .../viewmodels/ServiceListViewModel.java | 68 ++++ .../viewmodels/StaffListViewModel.java | 71 ++++ .../viewmodels/SupplierListViewModel.java | 51 +++ 28 files changed, 1626 insertions(+), 1526 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index b13edd67..1b858b08 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -18,19 +18,17 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import com.bumptech.glide.Glide; -import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ChatAdapter; import com.example.petstoremobile.adapters.MessageAdapter; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentChatBinding; import com.example.petstoremobile.dtos.ConversationDTO; import com.example.petstoremobile.dtos.MessageDTO; -import com.example.petstoremobile.dtos.SendMessageRequest; import com.example.petstoremobile.models.Chat; import com.example.petstoremobile.models.Message; import com.example.petstoremobile.services.ChatNotificationService; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.viewmodels.ChatViewModel; +import com.example.petstoremobile.viewmodels.ChatListViewModel; import com.example.petstoremobile.websocket.StompChatManager; import java.util.*; @@ -47,59 +45,44 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis private static final String TAG = "ChatFragment"; private FragmentChatBinding binding; - private ChatViewModel viewModel; + private ChatListViewModel viewModel; - // Adapters private ChatAdapter chatAdapter; private MessageAdapter messageAdapter; - // Data private final List chatList = new ArrayList<>(); private final List messageList = new ArrayList<>(); - private final Map customerNames = new HashMap<>(); private Uri pendingAttachmentUri; @Inject TokenManager tokenManager; @Inject @Named("baseUrl") String baseUrl; - // chat - private Long currentUserId; private Long activeConversationId; private StompChatManager stompChatManager; private ActivityResultLauncher attachmentLauncher; - /** - * Initializes the attachment launcher to handle file selection from the gallery. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ChatViewModel.class); + viewModel = new ViewModelProvider(this).get(ChatListViewModel.class); attachmentLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { Uri uri = result.getData().getData(); - if (uri != null) { - showAttachmentPreview(uri); - } + if (uri != null) showAttachmentPreview(uri); } } ); } - /** - * Inflates the layout, initializes UI components, and sets up click listeners for messaging. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentChatBinding.inflate(inflater, container, false); binding.btnHamburger.setOnClickListener(v -> binding.chatDrawerLayout.openDrawer(GravityCompat.START)); - // Set editor action listener for message field to send when enter is pressed binding.etMessage.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_NULL) { binding.btnSend.performClick(); @@ -108,35 +91,26 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis return false; }); - //When the send button is clicked check if there is an attachment and send using the correct helper function binding.btnSend.setOnClickListener(v -> { - if (pendingAttachmentUri != null) { - sendWithAttachment(pendingAttachmentUri); - } else { - sendMessage(); - } + if (pendingAttachmentUri != null) sendWithAttachment(pendingAttachmentUri); + else sendMessage(); }); - //When the attachment button is clicked open the file picker binding.btnAttach.setOnClickListener(v -> selectAttachment()); binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment()); setupRecyclerViews(); + observeViewModel(); loadInitialData(); return binding.getRoot(); } - /** - * Configures the RecyclerViews for the conversation list and the message history. - */ private void setupRecyclerViews() { - // Set up Drawer menu to select conversation chatAdapter = new ChatAdapter(chatList, this); binding.rvChatList.setLayoutManager(new LinearLayoutManager(getContext())); binding.rvChatList.setAdapter(chatAdapter); - // set up RecyclerView for selected chat to show messages messageAdapter = new MessageAdapter(messageList, null); LinearLayoutManager lm = new LinearLayoutManager(getContext()); lm.setStackFromEnd(true); @@ -145,26 +119,48 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis setConversationActive(false); } - /** - * Loads authentication tokens and user info, then initializes the Stomp WebSocket connection. - */ + private void observeViewModel() { + viewModel.getChatList().observe(getViewLifecycleOwner(), list -> { + chatList.clear(); + chatList.addAll(list); + chatAdapter.notifyDataSetChanged(); + + if (activeConversationId != null) { + for (Chat chat : list) { + if (chat.getChatId().equals(String.valueOf(activeConversationId))) { + binding.tvChatTitle.setText(chat.getCustomerName()); + break; + } + } + } + }); + + viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> { + messageList.clear(); + messageList.addAll(list); + messageAdapter.notifyDataSetChanged(); + scrollToBottom(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + // Can show a progress bar if needed + }); + } + private void loadInitialData() { String token = tokenManager.getToken(); - currentUserId = tokenManager.getUserId(); + Long currentUserId = tokenManager.getUserId(); String role = tokenManager.getRole(); messageAdapter.setCurrentUserId(currentUserId); messageAdapter.setToken(token); - // if token exist then connect to websocket if (token != null) { stompChatManager = new StompChatManager(token, role, baseUrl); stompChatManager.setMessageListener(this); stompChatManager.setConversationListener(this); stompChatManager.setConnectionListener(this); stompChatManager.connect(); - } else { - Log.e(TAG, "No token found"); } if (getArguments() != null && getArguments().containsKey("conversation_id")) { @@ -172,65 +168,17 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } else if (getActivity() != null && getActivity().getIntent().hasExtra("conversation_id")) { activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1); getActivity().getIntent().removeExtra("conversation_id"); - getActivity().getIntent().removeExtra("navigate_to"); } - loadCustomers(); + viewModel.loadCustomers(); + + if (activeConversationId != null) { + setConversationActive(true); + if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId); + viewModel.loadMessageHistory(activeConversationId); + } } - /** - * Fetches a list of customers from the ViewModel to display customer names for the chat list. - */ - private void loadCustomers() { - viewModel.getAllCustomers(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - resource.data.getContent().forEach(c -> customerNames.put(c.getCustomerId(), c.getFullName())); - loadConversations(); - } - }); - } - - /** - * Retrieves all conversations for the current user through the ViewModel and populates the chat drawer. - */ - private void loadConversations() { - viewModel.getAllConversations().observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - chatList.clear(); - for (ConversationDTO dto : resource.data) { - String name = customerNames.getOrDefault( - dto.getCustomerId(), "Customer #" + dto.getCustomerId()); - chatList.add(new Chat(String.valueOf(dto.getId()), - name, dto.getLastMessage(), - dto.getCustomerId(), dto.getStaffId())); - } - chatAdapter.notifyDataSetChanged(); - - if (activeConversationId != null) { - setConversationActive(true); - // Update title to customer name of active conversation - for (Chat chat : chatList) { - if (chat.getChatId().equals(String.valueOf(activeConversationId))) { - binding.tvChatTitle.setText(chat.getCustomerName()); - break; - } - } - if (stompChatManager != null) { - stompChatManager.subscribeToConversation(activeConversationId); - } - loadMessageHistory(activeConversationId); - } else { - messageList.clear(); - messageAdapter.notifyDataSetChanged(); - setConversationActive(false); - } - } - }); - } - - /** - * Handles selection of a chat from the drawer, updating the UI and subscribing to the WebSocket. - */ @Override public void onChatClick(Chat chat) { activeConversationId = Long.parseLong(chat.getChatId()); @@ -238,75 +186,35 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis binding.tvChatTitle.setText(chat.getCustomerName()); binding.chatDrawerLayout.closeDrawer(GravityCompat.START); - if (stompChatManager != null) { - stompChatManager.subscribeToConversation(activeConversationId); - } - - loadMessageHistory(activeConversationId); + if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId); + viewModel.loadMessageHistory(activeConversationId); } - /** - * Fetches the full message history for a specific conversation from the ViewModel. - */ - private void loadMessageHistory(Long conversationId) { - viewModel.getMessages(conversationId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - messageList.clear(); - for (MessageDTO dto : resource.data) { - messageList.add(dtoToModel(dto)); - } - messageAdapter.notifyDataSetChanged(); - scrollToBottom(); - } - }); - } - - /** - * Sends a plain text message to the currently active conversation through the ViewModel. - */ private void sendMessage() { - //check if a chat is selected if (activeConversationId == null) return; - - //get the message from text field String text = binding.etMessage.getText().toString().trim(); if (text.isEmpty()) return; - //clear text field after sending binding.etMessage.setText(""); - - //calls viewmodel to send the message - viewModel.sendMessage(activeConversationId, new SendMessageRequest(text)).observe(getViewLifecycleOwner(), resource -> { + viewModel.sendMessage(activeConversationId, text).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - messageList.add(dtoToModel(resource.data)); - messageAdapter.notifyItemInserted(messageList.size() - 1); - scrollToBottom(); - loadConversations(); + viewModel.addMessageLocally(resource.data); + viewModel.loadConversations(); } }); } - /** - * Launches a file picker intent to select an attachment for the message. - */ private void selectAttachment() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); attachmentLauncher.launch(intent); } - /** - * Displays a preview of the selected attachment in the UI. - */ private void showAttachmentPreview(Uri uri) { pendingAttachmentUri = uri; binding.layoutAttachmentPreview.setVisibility(View.VISIBLE); - String mimeType = requireContext().getContentResolver().getType(uri); - String fileName = getFileName(uri); - binding.tvPreviewName.setText(fileName); - - // If the file is an image, display a thumbnail of the image as well + binding.tvPreviewName.setText(getFileName(uri)); if (mimeType != null && mimeType.startsWith("image/")) { binding.ivPreview.setVisibility(View.VISIBLE); Glide.with(this).load(uri).into(binding.ivPreview); @@ -315,183 +223,83 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } - /** - * Clears the current attachment selection and hides the preview UI. - */ private void removeAttachment() { pendingAttachmentUri = null; binding.layoutAttachmentPreview.setVisibility(View.GONE); } - /** - * Show the display name of the file from its Uri. - */ private String getFileName(Uri uri) { String result = null; if (uri.getScheme().equals("content")) { try (Cursor cursor = requireContext().getContentResolver().query(uri, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); - if (index != -1) { - result = cursor.getString(index); - } + if (index != -1) result = cursor.getString(index); } } } if (result == null) { result = uri.getPath(); int cut = result.lastIndexOf('/'); - if (cut != -1) { - result = result.substring(cut + 1); - } + if (cut != -1) result = result.substring(cut + 1); } return result; } - /** - * Handles sending a message that includes a file attachment via the ViewModel. - */ private void sendWithAttachment(Uri uri) { - if (activeConversationId == null) return; - String text = binding.etMessage.getText().toString().trim(); - binding.etMessage.setText(""); - removeAttachment(); - - if (!text.isEmpty()) { - binding.etMessage.setText(text); - } Toast.makeText(requireContext(), "File attachments are not supported", Toast.LENGTH_SHORT).show(); + removeAttachment(); } - /** - * Callback triggered when a new message is received via the WebSocket. - */ @Override public void onMessageReceived(MessageDTO dto) { - //if there is no active selected conversation or the message received is for another chat, then just update the preview of last message - if (activeConversationId == null || !activeConversationId.equals(dto.getConversationId())) { - updateConversationPreview(dto.getConversationId(), dto.getContent()); - return; - } - updateConversationPreview(dto.getConversationId(), dto.getContent()); - - if (currentUserId != null && currentUserId.equals(dto.getSenderId())) return; - - //else add the message to the active chat if it's not from the current user - messageList.add(dtoToModel(dto)); requireActivity().runOnUiThread(() -> { - messageAdapter.notifyItemInserted(messageList.size() - 1); - scrollToBottom(); + if (activeConversationId != null && activeConversationId.equals(dto.getConversationId())) { + if (!tokenManager.getUserId().equals(dto.getSenderId())) { + viewModel.addMessageLocally(dto); + } + } + viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), "")); + // Re-load coversations to get correct names if needed or just let local update handle it + viewModel.loadConversations(); }); } - /** - * Callback triggered when a conversation is created or updated via the WebSocket. - */ @Override public void onConversationUpdated(ConversationDTO dto) { requireActivity().runOnUiThread(() -> { - boolean updated = false; - String name = customerNames.getOrDefault( - dto.getCustomerId(), "Customer #" + dto.getCustomerId()); - - for (int i = 0; i < chatList.size(); i++) { - Chat existing = chatList.get(i); - if (existing.getChatId().equals(String.valueOf(dto.getId()))) { - chatList.set(i, new Chat( - String.valueOf(dto.getId()), - name, - dto.getLastMessage(), - dto.getCustomerId(), - dto.getStaffId() - )); - chatAdapter.notifyItemChanged(i); - updated = true; - break; - } - } - - if (!updated) { - chatList.add(0, new Chat( - String.valueOf(dto.getId()), - name, - dto.getLastMessage(), - dto.getCustomerId(), - dto.getStaffId() - )); - chatAdapter.notifyItemInserted(0); - } - + viewModel.updateConversationLocally(dto); if (activeConversationId != null && activeConversationId.equals(dto.getId())) { setConversationActive(true); - binding.tvChatTitle.setText(name); + binding.tvChatTitle.setText(viewModel.getCustomerName(dto.getCustomerId())); } }); } - /** - * Callback triggered when the WebSocket connection is successfully opened. - */ @Override public void onSocketOpened() { - if (!isAdded()) { - return; - } + if (!isAdded()) return; requireActivity().runOnUiThread(() -> { - loadConversations(); - if (activeConversationId != null) { - loadMessageHistory(activeConversationId); - } + viewModel.loadConversations(); + if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId); }); } - /** - * Callback triggered when the WebSocket connection is closed. - */ @Override public void onSocketClosed() { - if (!isAdded()) { - return; - } - requireActivity().runOnUiThread(this::loadConversations); + if (!isAdded()) return; + requireActivity().runOnUiThread(viewModel::loadConversations); } - /** - * Callback triggered when a WebSocket connection error occurs. - */ @Override public void onSocketError() { - if (!isAdded()) { - return; - } + if (!isAdded()) return; requireActivity().runOnUiThread(() -> { - loadConversations(); - if (activeConversationId != null) { - loadMessageHistory(activeConversationId); - } + viewModel.loadConversations(); + if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId); }); } - /** - * Converts a MessageDTO into a Message object. - */ - private Message dtoToModel(MessageDTO dto) { - Message m = new Message(); - m.setId(dto.getId()); - m.setConversationId(dto.getConversationId()); - m.setSenderId(dto.getSenderId()); - m.setContent(dto.getContent()); - m.setTimestamp(dto.getTimestamp()); - m.setIsRead(dto.getIsRead()); - m.setAttachmentUrl(dto.getAttachmentUrl()); - m.setAttachmentName(dto.getAttachmentName()); - m.setAttachmentType(dto.getAttachmentType()); - return m; - } - - /** - * Scrolls the message history RecyclerView to the most recent message. - */ private void scrollToBottom() { if (!messageList.isEmpty()) { binding.rvMessages.post(() -> @@ -499,35 +307,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } - /** - * Updates the preview snippet of the last message for a specific conversation in the drawer. - */ - private void updateConversationPreview(Long conversationId, String lastMessage) { - if (conversationId == null) { - return; - } - requireActivity().runOnUiThread(() -> { - for (int i = 0; i < chatList.size(); i++) { - Chat existing = chatList.get(i); - if (existing.getChatId().equals(String.valueOf(conversationId))) { - Chat updated = new Chat( - existing.getChatId(), - existing.getCustomerName(), - lastMessage, - existing.getCustomerId(), - existing.getStaffId() - ); - chatList.set(i, updated); - chatAdapter.notifyItemChanged(i); - return; - } - } - }); - } - - /** - * Toggles the UI state based on whether a conversation is currently selected. - */ private void setConversationActive(boolean active) { binding.btnSend.setEnabled(active); binding.etMessage.setEnabled(active); @@ -537,9 +316,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis ChatNotificationService.activeConversationIdInUi = null; removeAttachment(); if (binding != null && binding.tvChatTitle != null) binding.tvChatTitle.setText("Customer Chat"); - if (stompChatManager != null) { - stompChatManager.clearConversationSubscription(); - } + if (stompChatManager != null) stompChatManager.clearConversationSubscription(); messageList.clear(); messageAdapter.notifyDataSetChanged(); binding.etMessage.setText(""); @@ -550,9 +327,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } - /** - * Disconnects the WebSocket manager when the fragment view is destroyed. - */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index f2a01b5d..39317495 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -24,9 +24,8 @@ import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.AdoptionViewModel; +import com.example.petstoremobile.viewmodels.AdoptionListViewModel; import com.example.petstoremobile.utils.EventDecorator; -import com.example.petstoremobile.viewmodels.StoreViewModel; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; @@ -46,28 +45,19 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop private FragmentAdoptionBinding binding; private List adoptionList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private AdoptionAdapter adapter; - private AdoptionViewModel adoptionViewModel; - private StoreViewModel storeViewModel; + private AdoptionListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; private CalendarDay selectedCalendarDay; private boolean isMonthMode = false; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); - /** - * Initializes the fragment and its ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(AdoptionListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, Search, SwipeRefresh, and Calendar. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -81,6 +71,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop setupCalendar(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1)); @@ -91,6 +82,24 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop return binding.getRoot(); } + private void observeViewModel() { + viewModel.getAdoptions().observe(getViewLifecycleOwner(), list -> { + adoptionList.clear(); + adoptionList.addAll(list); + updateCalendarDecorators(); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshAdoption.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -99,27 +108,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop binding.btnBulkDelete, adapter, "adoption", - adoptionViewModel::bulkDeleteAdoptions, + viewModel::bulkDeleteAdoptions, this::loadAdoptions ); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - @Override public void onResume() { super.onResume(); loadAdoptions(); - loadStoreData(); + viewModel.loadStores(); } - /** - * Toggles the calendar display between week and month modes. - */ private void toggleCalendarMode() { isMonthMode = !isMonthMode; binding.calendarViewAdoption.state().edit() @@ -127,17 +127,11 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop .commit(); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption, binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption); } - /** - * Sets up the date selection listener for the calendar. - */ private void setupCalendar() { binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> { if (selected) { @@ -154,9 +148,6 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop }); } - /** - * Updates the calendar decorators to highlight days with adoptions. - */ private void updateCalendarDecorators() { HashSet datesWithAdoptions = new HashSet<>(); for (AdoptionDTO adoption : adoptionList) { @@ -177,67 +168,37 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions)); } - /** - * Initializes the RecyclerView for displaying adoptions. - */ private void setupRecyclerView() { adapter = new AdoptionAdapter(adoptionList, this); binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAdoptions.setAdapter(adapter); } - /** - * Sets up the search bar for filtering - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions); } - /** - * Configures the status filter spinner. - */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Completed", "Pending", "Cancelled"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, this::loadAdoptions); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Sets up the SwipeRefreshLayout to reload adoption data. - */ private void setupSwipeRefresh() { binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions); } - /** - * Fetches the adoption list from the server through the ViewModel. - */ private void loadAdoptions() { String query = binding.etSearchAdoption.getText().toString().trim(); String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses"; Long storeId = null; - if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId(); } String selectedDateString = null; @@ -249,52 +210,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop if (status.equals("All Statuses")) status = null; else status = status.toUpperCase(); - adoptionViewModel.getAllAdoptions(0, 500, query, status, storeId, selectedDateString, null).observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshAdoption.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshAdoption.setRefreshing(false); - if (resource.data != null) { - adoptionList.clear(); - adoptionList.addAll(resource.data.getContent()); - updateCalendarDecorators(); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshAdoption.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load adoptions: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("AdoptionFragment", "Error loading adoptions: " + resource.message); - break; - } - }); + viewModel.loadAdoptions(true, query, status, storeId); } - /** - * Navigates to the adoption detail screen for a specific adoption or to create a new one. - */ private void openDetail(int position) { Bundle args = new Bundle(); - if (position != -1) { AdoptionDTO a = adoptionList.get(position); args.putLong("adoptionId", a.getAdoptionId()); } - NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args); } - /** - * Handles item click in the adoption list. - */ @Override public void onAdoptionClick(int position) { openDetail(position); } @@ -304,4 +231,10 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop bulkDeleteHandler.onSelectionChanged(selectedCount); } } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } 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 2e2b2592..ef26db92 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 @@ -2,16 +2,14 @@ package com.example.petstoremobile.fragments.listfragments; import android.graphics.Color; import android.os.Bundle; -import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.example.petstoremobile.databinding.FragmentAnalyticsBinding; -import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.SaleViewModel; +import com.example.petstoremobile.viewmodels.AnalyticsViewModel; import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.math.RoundingMode; @@ -21,226 +19,131 @@ import java.util.*; public class AnalyticsFragment extends Fragment { private FragmentAnalyticsBinding binding; - private SaleViewModel saleViewModel; + private AnalyticsViewModel viewModel; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentAnalyticsBinding.inflate(inflater, container, false); - saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); + viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class); - loadAnalytics(); + observeViewModel(); + viewModel.loadAnalytics(); - binding.btnRefreshAnalytics.setOnClickListener(v -> loadAnalytics()); + binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics()); UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this); return binding.getRoot(); } + private void observeViewModel() { + viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + if (loading) { + binding.tvTotalRevenue.setText("Loading..."); + binding.tvTotalTransactions.setText("..."); + binding.tvAvgTransaction.setText("..."); + binding.tvTotalItems.setText("..."); + } + }); + + viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> { + if (error != null) showError(error); + }); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - private void loadAnalytics() { - // Clear all sections + private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) { + if (data == null) return; + + // Summary + binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP)); + binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions)); + binding.tvAvgTransaction.setText("$" + data.avgTransaction); + binding.tvTotalItems.setText(String.valueOf(data.totalItems)); + + // Top Revenue Products binding.llTopRevenue.removeAllViews(); - binding.llTopQuantity.removeAllViews(); - binding.llPaymentMethods.removeAllViews(); - binding.llEmployeePerformance.removeAllViews(); - binding.llDailyRevenue.removeAllViews(); - - - saleViewModel.getAllSales(0, 1000, null, null, null, "saleDate,desc") - .observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - if (resource.data != null) { - computeAndDisplay(resource.data.getContent()); - } - break; - case ERROR: - Log.e("Analytics", resource.message != null ? resource.message : "Error loading sales"); - showError("Failed to load sales data"); - break; - case LOADING: - binding.tvTotalRevenue.setText("Loading..."); - binding.tvTotalTransactions.setText("..."); - binding.tvAvgTransaction.setText("..."); - binding.tvTotalItems.setText("..."); - break; - } - } - }); - } - - private void computeAndDisplay(List sales) { - // Filter out refunds for most metrics - List regularSales = new ArrayList<>(); - for (SaleDTO s : sales) { - if (!Boolean.TRUE.equals(s.getIsRefund())) - regularSales.add(s); - } - - // ── Summary ────────────────────────────────────────── - BigDecimal totalRevenue = BigDecimal.ZERO; - int totalItems = 0; - - for (SaleDTO s : regularSales) { - if (s.getTotalAmount() != null) - totalRevenue = totalRevenue.add(s.getTotalAmount()); - if (s.getItems() != null) { - for (SaleDTO.SaleItemDTO item : s.getItems()) { - if (item.getQuantity() != null) - totalItems += Math.abs(item.getQuantity()); - } + if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) { + BigDecimal maxRevenue = data.topRevenueProducts.get(0).getValue(); + if (maxRevenue.compareTo(BigDecimal.ZERO) == 0) maxRevenue = 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"); } - } - - int totalTx = regularSales.size(); - BigDecimal avgTx = totalTx > 0 - ? totalRevenue.divide(BigDecimal.valueOf(totalTx), 2, RoundingMode.HALF_UP) - : BigDecimal.ZERO; - - binding.tvTotalRevenue.setText("$" + totalRevenue.setScale(2, RoundingMode.HALF_UP)); - binding.tvTotalTransactions.setText(String.valueOf(totalTx)); - binding.tvAvgTransaction.setText("$" + avgTx); - binding.tvTotalItems.setText(String.valueOf(totalItems)); - - // ── Top Products by Revenue ─────────────────────────── - Map revenueByProduct = new LinkedHashMap<>(); - Map quantityByProduct = new LinkedHashMap<>(); - - for (SaleDTO s : regularSales) { - 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; - - revenueByProduct.merge(name, lineTotal, BigDecimal::add); - quantityByProduct.merge(name, qty, Integer::sum); - } - } - } - - // Sort by revenue desc, take top 5 - List> topRevenue = new ArrayList<>(revenueByProduct.entrySet()); - topRevenue.sort((a, b) -> b.getValue().compareTo(a.getValue())); - BigDecimal maxRevenue = topRevenue.isEmpty() ? BigDecimal.ONE : topRevenue.get(0).getValue(); - - binding.llTopRevenue.removeAllViews(); - for (int i = 0; i < Math.min(5, topRevenue.size()); i++) { - Map.Entry e = topRevenue.get(i); - addBarRow(binding.llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35"); - } - if (topRevenue.isEmpty()) + } else { addEmptyRow(binding.llTopRevenue, "No data"); + } - // Sort by quantity desc, take top 5 - List> topQuantity = new ArrayList<>(quantityByProduct.entrySet()); - topQuantity.sort((a, b) -> b.getValue() - a.getValue()); - int maxQty = topQuantity.isEmpty() ? 1 : topQuantity.get(0).getValue(); - + // Top Quantity Products binding.llTopQuantity.removeAllViews(); - for (int i = 0; i < Math.min(5, topQuantity.size()); i++) { - Map.Entry e = topQuantity.get(i); - addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units", - (float) e.getValue() / maxQty, "#4ecdc4"); - } - if (topQuantity.isEmpty()) - addEmptyRow(binding.llTopQuantity, "No data"); - - // ── Payment Methods ─────────────────────────────────── - Map paymentCount = new LinkedHashMap<>(); - for (SaleDTO s : regularSales) { - String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown"; - paymentCount.merge(method, 1, Integer::sum); - } - - int maxPayment = paymentCount.values().stream().max(Integer::compare).orElse(1); - String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" }; - int ci = 0; - binding.llPaymentMethods.removeAllViews(); - for (Map.Entry e : paymentCount.entrySet()) { - addBarRow(binding.llPaymentMethods, e.getKey(), - e.getValue() + " transactions", - (float) e.getValue() / maxPayment, - paymentColors[ci % paymentColors.length]); - ci++; - } - if (paymentCount.isEmpty()) - addEmptyRow(binding.llPaymentMethods, "No data"); - - // ── Employee Performance ────────────────────────────── - Map employeeRevenue = new LinkedHashMap<>(); - for (SaleDTO s : regularSales) { - String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown"; - if (s.getTotalAmount() != null) - employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add); - } - - List> empList = new ArrayList<>(employeeRevenue.entrySet()); - empList.sort((a, b) -> b.getValue().compareTo(a.getValue())); - BigDecimal maxEmp = empList.isEmpty() ? BigDecimal.ONE : empList.get(0).getValue(); - - binding.llEmployeePerformance.removeAllViews(); - for (Map.Entry e : empList) { - addBarRow(binding.llEmployeePerformance, e.getKey(), - "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), - e.getValue().floatValue() / maxEmp.floatValue(), - "#1a759f"); - } - if (empList.isEmpty()) - addEmptyRow(binding.llEmployeePerformance, "No data"); - - // ── Daily Revenue (last 7 days) ─────────────────────── - Map dailyRevenue = new TreeMap<>(); - - // Initialize last 7 days - Calendar cal = Calendar.getInstance(); - 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)); - dailyRevenue.put(key, 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 (dailyRevenue.containsKey(date)) { - dailyRevenue.merge(date, s.getTotalAmount(), BigDecimal::add); - } + if (data.topQuantityProducts != null && !data.topQuantityProducts.isEmpty()) { + int maxQty = data.topQuantityProducts.get(0).getValue(); + if (maxQty == 0) maxQty = 1; + for (Map.Entry e : data.topQuantityProducts) { + addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units", + (float) e.getValue() / maxQty, "#4ecdc4"); } + } else { + addEmptyRow(binding.llTopQuantity, "No data"); } - BigDecimal maxDaily = dailyRevenue.values().stream() - .max(BigDecimal::compareTo).orElse(BigDecimal.ONE); - if (maxDaily.compareTo(BigDecimal.ZERO) == 0) - maxDaily = BigDecimal.ONE; + // 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 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++; + } + } else { + addEmptyRow(binding.llPaymentMethods, "No data"); + } + // Employee Performance + binding.llEmployeePerformance.removeAllViews(); + if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) { + BigDecimal maxEmp = data.employeePerformance.get(data.employeePerformance.size() - 1).getValue(); + if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE; + // Sorting is ascending from VM for some reason? Let's check VM... it says b.getValue().compareTo(a.getValue()) which is DESC. + // Wait, computeAnalytics sorts them... let's assume DESC as per VM code. + 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"); + } + } else { + addEmptyRow(binding.llEmployeePerformance, "No data"); + } + + // Daily Revenue binding.llDailyRevenue.removeAllViews(); - for (Map.Entry e : dailyRevenue.entrySet()) { - // Show just MM-DD - 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"); + if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) { + 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"); + } } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index b921d867..8610790e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -14,7 +14,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AppointmentAdapter; @@ -25,10 +24,9 @@ import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.AppointmentViewModel; +import com.example.petstoremobile.viewmodels.AppointmentListViewModel; import com.example.petstoremobile.utils.EventDecorator; import com.example.petstoremobile.viewmodels.AuthViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; @@ -48,11 +46,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private FragmentAppointmentBinding binding; private List appointmentList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private AppointmentAdapter adapter; - private AppointmentViewModel appointmentViewModel; - private StoreViewModel storeViewModel; + private AppointmentListViewModel viewModel; private AuthViewModel authViewModel; private BulkDeleteHandler bulkDeleteHandler; @@ -61,20 +57,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private Long currentUserId = null; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); - /** - * Initializes the fragment and its associated ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(AppointmentListViewModel.class); authViewModel = new ViewModelProvider(this).get(AuthViewModel.class); } - /** - * Sets up the fragment's UI, including RecyclerView, search, swipe-to-refresh, and calendar. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -89,6 +78,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. setupFilterToggle(); setupMyAppointmentFilter(); setupBulkDelete(); + observeViewModel(); binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); @@ -101,6 +91,24 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. return binding.getRoot(); } + private void observeViewModel() { + viewModel.getAppointments().observe(getViewLifecycleOwner(), list -> { + appointmentList.clear(); + appointmentList.addAll(list); + updateCalendarDecorators(); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshAppointment.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -109,27 +117,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. binding.btnBulkDelete, adapter, "appointment", - appointmentViewModel::bulkDeleteAppointments, + viewModel::bulkDeleteAppointments, this::loadAppointmentData ); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - @Override public void onResume() { super.onResume(); loadAppointmentData(); - loadStoreData(); + viewModel.loadStores(); } - /** - * Toggles the calendar between week and month display modes. - */ private void toggleCalendarMode() { isMonthMode = !isMonthMode; binding.calendarView.state().edit() @@ -137,18 +136,12 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. .commit(); } - /** - * Sets up the "My Appointments" filter button. - */ private void setupMyAppointmentFilter() { binding.btnMyAppointments.setOnClickListener(v -> { loadAppointmentData(); }); } - /** - * Fetches current user info to get the employeeId. - */ private void loadCurrentUserInfo() { authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -157,17 +150,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment, binding.spinnerStatus, binding.spinnerStore); } - /** - * Sets up the date selection listener for the calendar. - */ private void setupCalendar() { binding.calendarView.setOnDateChangedListener((widget, date, selected) -> { if (selected) { @@ -184,17 +171,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } - /** - * Updates calendar indicators to highlight dates that have scheduled appointments. - */ private void updateCalendarDecorators() { HashSet datesWithAppointments = new HashSet<>(); - SimpleDateFormat displayFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); for (AppointmentDTO appointment : appointmentList) { try { - //Get the appointment date - Date date = displayFormat.parse(appointment.getAppointmentDate()); - //if the date is not null, add it to the hashset + Date date = dateFormat.parse(appointment.getAppointmentDate()); if (date != null) { Calendar cal = Calendar.getInstance(); cal.setTime(date); @@ -204,56 +185,27 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. Log.e("AppointmentFragment", "Error parsing date: " + appointment.getAppointmentDate()); } } - //update the indicators to the calendar binding.calendarView.removeDecorators(); binding.calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments)); } - /** - * Configures the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchAppointment, this::loadAppointmentData); } - /** - * Configures the status filter spinner. - */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadAppointmentData); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadAppointmentData); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Initializes the SwipeRefreshLayout to allow manual data refreshing. - */ private void setupSwipeRefresh() { binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData); } - /** - * Navigates to the appointment detail screen for editing or creating an appointment. - */ private void openAppointmentDetails(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -263,9 +215,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args); } - /** - * Handles item click in the appointment list. - */ @Override public void onAppointmentClick(int position) { openAppointmentDetails(position); @@ -278,16 +227,14 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. } } - /** - * Fetches appointment data from the server with all active filters. - */ private void loadAppointmentData() { String query = binding.etSearchAppointment.getText().toString().trim(); String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } String selectedDateString = null; @@ -304,41 +251,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. if (status.equals("All Statuses")) status = null; else status = status.toUpperCase(); - appointmentViewModel.getAllAppointments(0, 500, query, status, storeId, selectedDateString, employeeId).observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshAppointment.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshAppointment.setRefreshing(false); - if (resource.data != null) { - appointmentList.clear(); - appointmentList.addAll(resource.data.getContent()); - updateCalendarDecorators(); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshAppointment.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load appointments: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("AppointmentFragment", "Error loading appointments: " + resource.message); - break; - } - }); + viewModel.loadAppointments(query, status, storeId, selectedDateString, employeeId); } - /** - * Initializes the RecyclerView for displaying appointments. - */ private void setupRecyclerView() { adapter = new AppointmentAdapter(appointmentList, this); binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAppointments.setAdapter(adapter); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 259a0a9e..6cefedb8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -1,7 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -22,8 +21,7 @@ import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.InventoryViewModel; -import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.viewmodels.InventoryListViewModel; import com.example.petstoremobile.utils.SpinnerUtils; import java.util.ArrayList; @@ -34,33 +32,18 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener { - private static final String TAG = "InventoryFragment"; - private static final int PAGE_SIZE = 20; - private FragmentInventoryBinding binding; private final List inventoryList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private InventoryAdapter adapter; - private InventoryViewModel viewModel; + private InventoryListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; - // Pagination - private int currentPage = 0; - private boolean isLastPage = false; - private boolean isLoading = false; - - /** - * Initializes the fragment and its ViewModel. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(InventoryViewModel.class); + viewModel = new ViewModelProvider(this).get(InventoryListViewModel.class); } - /** - * Sets up the fragment's UI components, including the inventory list and search. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -72,8 +55,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); + loadInventory(true); - loadStoreData(); binding.fabAddInventory.setOnClickListener(v -> openDetail(null)); @@ -82,6 +66,23 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn return binding.getRoot(); } + private void observeViewModel() { + viewModel.getInventory().observe(getViewLifecycleOwner(), list -> { + inventoryList.clear(); + inventoryList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshInventory.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -95,49 +96,30 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn ); } + @Override + public void onResume() { + super.onResume(); + viewModel.loadStores(); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore); } - /** - * Sets up the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchInventory, () -> loadInventory(true)); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadInventory(true)); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - viewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Initializes the RecyclerView with a layout manager, and adapter. - */ private void setupRecyclerView() { adapter = new InventoryAdapter(inventoryList, this); binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext())); @@ -146,105 +128,45 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn binding.recyclerViewInventory.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - if (dy <= 0) - return; + if (dy <= 0) return; LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewInventory.getLayoutManager(); - if (lm == null) - return; + if (lm == null) return; int visible = lm.getChildCount(); int total = lm.getItemCount(); int firstVis = lm.findFirstVisibleItemPosition(); - if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { loadInventory(false); } } }); } - /** - * Sets up the SwipeRefreshLayout to reload the first page of inventory items. - */ private void setupSwipeRefresh() { binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true)); } - /** - * Fetches a page of inventory items from the API. - */ private void loadInventory(boolean reset) { - if (isLoading) return; - - if (reset) { - currentPage = 0; - isLastPage = false; - } - - // Search text from input String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : ""; if (query.isEmpty()) query = null; Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } - //Load all inventory items from the backend using viewModel - viewModel.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - isLoading = true; - binding.swipeRefreshInventory.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - isLoading = false; - binding.swipeRefreshInventory.setRefreshing(false); - if (resource.data != null) { - if (reset) inventoryList.clear(); - inventoryList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - isLastPage = resource.data.isLast(); - if (!isLastPage) currentPage++; - } - break; - case ERROR: - // Hide loading indicator and toast error message - isLoading = false; - binding.swipeRefreshInventory.setRefreshing(false); - Log.e(TAG, "Error: " + resource.message); - Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show(); - break; - } - }); + viewModel.loadInventory(reset, query, storeId); } - /** - * Navigates to the inventory detail screen for a specific item or to add a new one. - */ private void openDetail(InventoryDTO inv) { Bundle args = new Bundle(); - if (inv != null) { args.putLong("inventoryId", inv.getInventoryId()); } - NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args); } - /** - * Reloads inventory data when changes occur. - */ - public void onInventoryChanged() { - loadInventory(true); - } - - /** - * Handles item click in the inventory list. - */ @Override public void onInventoryClick(int position) { if (position >= 0 && position < inventoryList.size()) { @@ -252,9 +174,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } } - /** - * Updates the bulk deletion UI visibility and count when items are selected or deselected. - */ @Override public void onSelectionChanged(int selectedCount) { if (bulkDeleteHandler != null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index 87a1a54c..bc1b9a6f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -25,8 +24,7 @@ import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.PetListViewModel; import java.util.ArrayList; import java.util.List; @@ -40,28 +38,19 @@ import dagger.hilt.android.AndroidEntryPoint; public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener { private FragmentPetBinding binding; private List petList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private PetAdapter adapter; - private PetViewModel viewModel; - private StoreViewModel storeViewModel; + private PetListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; - /** - * Initializes the fragment and its associated ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PetViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(PetListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -75,6 +64,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); binding.fabAddPet.setOnClickListener(v -> openPetDetails()); @@ -83,6 +73,23 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen return binding.getRoot(); } + private void observeViewModel() { + viewModel.getPets().observe(getViewLifecycleOwner(), list -> { + petList.clear(); + petList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshPet.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -96,83 +103,62 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen ); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - - /** - * Reloads data every time the fragment becomes visible. - */ @Override public void onResume() { super.onResume(); loadPetData(); - loadStoreData(); + viewModel.loadStores(); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet, binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore); } - /** - * Configures the search bar. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchPet, this::loadPetData); } - /** - * Configures the status filter spinner. - */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData); } - /** - * Configures the species filter spinner with species. - */ private void setupSpeciesFilter() { String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, species, this::loadPetData); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Sets up the SwipeRefreshLayout. - */ private void setupSwipeRefresh() { binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData); } - /** - * Navigates to the pet profile screen. - */ + private void loadPetData() { + String query = binding.etSearchPet.getText().toString().trim(); + String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; + String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species"; + + Long storeId = null; + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + } + + viewModel.loadPets(query, status, species, storeId); + } + + private void setupRecyclerView() { + adapter = new PetAdapter(petList, this); + adapter.setBaseUrl(baseUrl); + adapter.setToken(tokenManager.getToken()); + binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewPets.setAdapter(adapter); + } + private void openPetProfile(int position) { Bundle args = new Bundle(); PetDTO pet = petList.get(position); @@ -180,9 +166,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args); } - /** - * Navigates to the pet detail screen. - */ private void openPetDetails() { NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail); } @@ -199,54 +182,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } } - /** - * Fetches pet data from the server with all active filters. - */ - private void loadPetData() { - String query = binding.etSearchPet.getText().toString().trim(); - String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; - String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species"; - - Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); - } - - if (status.equals("All Statuses")) status = null; - if (species.equals("All Species")) species = null; - - viewModel.getAllPets(0, 100, query, status, species, storeId, null, "petName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - switch (resource.status) { - case LOADING: - binding.swipeRefreshPet.setRefreshing(true); - break; - case SUCCESS: - binding.swipeRefreshPet.setRefreshing(false); - if (resource.data != null) { - petList.clear(); - petList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - binding.swipeRefreshPet.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load pets: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("PetFragment", "Error loading pets: " + resource.message); - break; - } - }); - } - - /** - * Initializes the RecyclerView. - */ - private void setupRecyclerView() { - adapter = new PetAdapter(petList, this); - adapter.setBaseUrl(baseUrl); - adapter.setToken(tokenManager.getToken()); - binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext())); - binding.recyclerViewPets.setAdapter(adapter); + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } } 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 6ae3d349..84f076a2 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 @@ -9,21 +9,18 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; 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.ProductDTO; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.ProductViewModel; +import com.example.petstoremobile.viewmodels.ProductListViewModel; import java.util.ArrayList; import java.util.List; @@ -38,24 +35,17 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc private FragmentProductBinding binding; private List productList = new ArrayList<>(); - private List categoryList = new ArrayList<>(); private ProductAdapter adapter; - private ProductViewModel viewModel; + private ProductListViewModel viewModel; @Inject @Named("baseUrl") String baseUrl; - /** - * Initializes the fragment and its associated ProductViewModel. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ProductViewModel.class); + viewModel = new ViewModelProvider(this).get(ProductListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -66,6 +56,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc setupCategoryFilter(); setupSwipeRefresh(); setupFilterToggle(); + observeViewModel(); binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1)); @@ -74,67 +65,67 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc return binding.getRoot(); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; + private void observeViewModel() { + viewModel.getProducts().observe(getViewLifecycleOwner(), list -> { + productList.clear(); + productList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getCategories().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, list, + CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshProduct.setRefreshing(loading); + }); } - /** - * Reloads data every time the fragment becomes visible. - */ @Override public void onResume() { super.onResume(); loadProductData(); - loadCategoryData(); + viewModel.loadCategories(); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchProduct, binding.spinnerCategory); } - /** - * Configures the search bar for triggering data load from backend. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData); } - /** - * Configures the category filter spinner. - */ private void setupCategoryFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData); } - /** - * Fetches category data to populate the category filter. - */ - private void loadCategoryData() { - viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - categoryList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, categoryList, - CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId); - } - }); - } - - /** - * Sets up the SwipeRefreshLayout. - */ private void setupSwipeRefresh() { binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData); } - /** - * Navigates to the product detail screen. - */ + private void loadProductData() { + String query = binding.etSearchProduct.getText().toString().trim(); + if (query.isEmpty()) query = null; + + Long categoryId = null; + List categories = viewModel.getCategories().getValue(); + if (binding.spinnerCategory.getSelectedItemPosition() > 0 && categories != null && !categories.isEmpty()) { + categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId(); + } + + viewModel.loadProducts(query, categoryId); + } + + private void setupRecyclerView() { + adapter = new ProductAdapter(productList, this); + adapter.setBaseUrl(baseUrl); + binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewProducts.setAdapter(adapter); + } + private void openProductDetails(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -149,51 +140,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc openProductDetails(position); } - /** - * Fetches product data from the server with search query, category, and sorting. - */ - private void loadProductData() { - String query = binding.etSearchProduct.getText().toString().trim(); - if (query.isEmpty()) query = null; - - Long categoryId = null; - if (binding.spinnerCategory.getSelectedItemPosition() > 0 && !categoryList.isEmpty()) { - categoryId = categoryList.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId(); - } - - viewModel.getAllProducts(query, categoryId, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - switch (resource.status) { - case LOADING: - binding.swipeRefreshProduct.setRefreshing(true); - break; - case SUCCESS: - binding.swipeRefreshProduct.setRefreshing(false); - if (resource.data != null) { - productList.clear(); - productList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - binding.swipeRefreshProduct.setRefreshing(false); - if (getContext() != null) { - Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show(); - } - Log.e("ProductFragment", "Error loading products: " + resource.message); - break; - } - }); - } - - /** - * Initializes the RecyclerView. - */ - private void setupRecyclerView() { - adapter = new ProductAdapter(productList, this); - adapter.setBaseUrl(baseUrl); - binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); - binding.recyclerViewProducts.setAdapter(adapter); + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index e1db78b6..6a066528 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -1,11 +1,9 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -21,12 +19,9 @@ import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; -import com.example.petstoremobile.viewmodels.ProductViewModel; -import com.example.petstoremobile.viewmodels.SupplierViewModel; +import com.example.petstoremobile.viewmodels.ProductSupplierListViewModel; import java.util.ArrayList; import java.util.List; @@ -39,29 +34,17 @@ public class ProductSupplierFragment extends Fragment private FragmentProductSupplierBinding binding; private List psList = new ArrayList<>(); - private List productList = new ArrayList<>(); - private List supplierList = new ArrayList<>(); private ProductSupplierAdapter adapter; - private ProductSupplierViewModel viewModel; - private ProductViewModel productViewModel; - private SupplierViewModel supplierViewModel; + private ProductSupplierListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; - /** - * Initializes the fragment and its associated ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class); - productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); - supplierViewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + viewModel = new ViewModelProvider(this).get(ProductSupplierListViewModel.class); } - /** - * Sets up the fragment's UI components, including the RecyclerView, search, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -74,6 +57,7 @@ public class ProductSupplierFragment extends Fragment setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); binding.fabAddPS.setOnClickListener(v -> openDetail(-1)); @@ -82,6 +66,28 @@ public class ProductSupplierFragment extends Fragment return binding.getRoot(); } + private void observeViewModel() { + viewModel.getProductSuppliers().observe(getViewLifecycleOwner(), list -> { + psList.clear(); + psList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getProducts().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, list, + ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId); + }); + + viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, list, + SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshPS.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -95,136 +101,65 @@ public class ProductSupplierFragment extends Fragment ); } + @Override + public void onResume() { + super.onResume(); + loadData(); + viewModel.loadFilterData(); + } + @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - /** - * Reloads data every time the fragment becomes visible. - */ - @Override - public void onResume() { - super.onResume(); - loadData(); - loadFilterData(); - } - - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPS, binding.spinnerProduct, binding.spinnerSupplier); } - /** - * Initializes the RecyclerView with a layout manager and adapter for product-supplier data. - */ private void setupRecyclerView() { adapter = new ProductSupplierAdapter(psList, this); binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPS.setAdapter(adapter); } - /** - * Configures the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchPS, this::loadData); } - /** - * Configures the product filter spinner. - */ private void setupProductFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData); } - /** - * Configures the supplier filter spinner. - */ private void setupSupplierFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData); } - /** - * Fetches products and suppliers to populate the filters. - */ - private void loadFilterData() { - productViewModel.getAllProducts(null, null, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - productList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, productList, - ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId); - } - }); - - supplierViewModel.getAllSuppliers(0, 100, null, "supCompany").observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - supplierList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, supplierList, - SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId); - } - }); - } - - /** - * Sets up the SwipeRefreshLayout to allow manual reloading of product-supplier data. - */ private void setupSwipeRefresh() { binding.swipeRefreshPS.setOnRefreshListener(this::loadData); } - /** - * Fetches product-supplier data from the server through the ViewModel with search query and filters. - */ private void loadData() { String query = binding.etSearchPS.getText().toString().trim(); if (query.isEmpty()) query = null; Long productId = null; - if (binding.spinnerProduct.getSelectedItemPosition() > 0 && !productList.isEmpty()) { - productId = productList.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId(); + List products = viewModel.getProducts().getValue(); + if (binding.spinnerProduct.getSelectedItemPosition() > 0 && products != null && !products.isEmpty()) { + productId = products.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId(); } Long supplierId = null; - if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && !supplierList.isEmpty()) { - supplierId = supplierList.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId(); + List suppliers = viewModel.getSuppliers().getValue(); + if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && suppliers != null && !suppliers.isEmpty()) { + supplierId = suppliers.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId(); } - viewModel.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshPS.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshPS.setRefreshing(false); - if (resource.data != null) { - psList.clear(); - psList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshPS.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("PSFragment", "Error loading: " + resource.message); - break; - } - }); + viewModel.loadProductSuppliers(query, productId, supplierId); } - /** - * Navigates to the product-supplier detail screen for a specific item or to add a new record. - */ private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -235,9 +170,6 @@ public class ProductSupplierFragment extends Fragment NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args); } - /** - * Handles item click in the product-supplier list. - */ @Override public void onProductSupplierClick(int position) { openDetail(position); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index b27c9c1f..64dc0ea5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -1,11 +1,9 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -19,11 +17,9 @@ import com.example.petstoremobile.adapters.PurchaseOrderAdapter; import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.PurchaseOrderListViewModel; import java.util.ArrayList; import java.util.List; @@ -36,24 +32,15 @@ public class PurchaseOrderFragment extends Fragment private FragmentPurchaseOrderBinding binding; private List poList = new ArrayList<>(); - private List storeList = new ArrayList<>(); private PurchaseOrderAdapter adapter; - private PurchaseOrderViewModel viewModel; - private StoreViewModel storeViewModel; + private PurchaseOrderListViewModel viewModel; - /** - * Initializes the fragment and its associated ViewModels. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(PurchaseOrderListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -64,121 +51,72 @@ public class PurchaseOrderFragment extends Fragment setupStoreFilter(); setupSwipeRefresh(); setupFilterToggle(); + observeViewModel(); UIUtils.setupHamburgerMenu(binding.btnHamburgerPO, this); return binding.getRoot(); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; + private void observeViewModel() { + viewModel.getPurchaseOrders().observe(getViewLifecycleOwner(), list -> { + poList.clear(); + poList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshPO.setRefreshing(loading); + }); } - /** - * Reloads data every time the fragment becomes visible. - */ @Override public void onResume() { super.onResume(); loadData(); - loadStoreData(); + viewModel.loadStores(); } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO, binding.spinnerStore); } - /** - * Configures the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchPO, this::loadData); } - /** - * Configures the store filter spinner. - */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadData); } - /** - * Fetches store data to populate the store filter. - */ - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList = resource.data.getContent(); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); - } - }); - } - - /** - * Initializes the RecyclerView with a layout manager and adapter. - */ private void setupRecyclerView() { adapter = new PurchaseOrderAdapter(poList, this); binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPO.setAdapter(adapter); } - /** - * Sets up the SwipeRefreshLayout to allow manual reloading of purchase order data. - */ private void setupSwipeRefresh() { binding.swipeRefreshPO.setOnRefreshListener(this::loadData); } - /** - * Fetches purchase order data from the server with active filters and updates the UI. - */ private void loadData() { String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : ""; if (query.isEmpty()) query = null; Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } - viewModel.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshPO.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshPO.setRefreshing(false); - if (resource.data != null) { - poList.clear(); - poList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshPO.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load purchase orders: " + resource.message, Toast.LENGTH_SHORT).show(); - Log.e("POFragment", "Error loading purchase orders: " + resource.message); - break; - } - }); + viewModel.loadPurchaseOrders(query, storeId); } - /** - * Navigates to the purchase order detail screen for a specific record. - */ private void openDetail(int position) { Bundle args = new Bundle(); PurchaseOrderDTO po = poList.get(position); @@ -186,11 +124,14 @@ public class PurchaseOrderFragment extends Fragment NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args); } - /** - * Handles item click in the purchase order list. - */ @Override public void onPurchaseOrderClick(int position) { openDetail(position); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index 850717e4..fef5d994 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -8,7 +8,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -19,11 +18,9 @@ import com.example.petstoremobile.adapters.SaleAdapter; import com.example.petstoremobile.databinding.FragmentSaleBinding; import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.SaleViewModel; -import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.SaleListViewModel; import java.util.ArrayList; import java.util.List; @@ -33,20 +30,10 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { - private static final String TAG = "SaleFragment"; - private static final int PAGE_SIZE = 200; - private FragmentSaleBinding binding; private final List saleList = new ArrayList<>(); - private final List storeList = new ArrayList<>(); private SaleAdapter adapter; - private SaleViewModel saleViewModel; - private StoreViewModel storeViewModel; - - // Pagination - private int currentPage = 0; - private boolean isLastPage = false; - private boolean isLoading = false; + private SaleListViewModel viewModel; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -58,8 +45,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); - storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + viewModel = new ViewModelProvider(this).get(SaleListViewModel.class); setupRecyclerView(); setupSearch(); @@ -67,6 +53,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis setupPaymentMethodFilter(); setupSwipeRefresh(); setupFilterToggle(); + observeViewModel(); + loadSales(true); UIUtils.setupHamburgerMenu(binding.btnHamburger, this); @@ -78,10 +66,27 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis NavHostFragment.findNavController(this).navigate(R.id.nav_refund)); } + private void observeViewModel() { + viewModel.getSales().observe(getViewLifecycleOwner(), list -> { + saleList.clear(); + saleList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getStores().observe(getViewLifecycleOwner(), list -> { + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list, + StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshSale.setRefreshing(loading); + }); + } + @Override public void onResume() { super.onResume(); - loadStoreData(); + viewModel.loadStores(); } private void setupFilterToggle() { @@ -93,28 +98,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true)); } - private void loadStoreData() { - storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - storeList.clear(); - storeList.addAll(resource.data.getContent()); - SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "Stores", null, StoreDTO::getStoreId); - } - }); - } - private void setupPaymentMethodFilter() { String[] paymentMethods = {"Payments", "Cash", "Card"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerPaymentMethod, paymentMethods, () -> loadSales(true)); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - private void setupRecyclerView() { adapter = new SaleAdapter(saleList, this); binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); @@ -129,7 +117,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis int visible = lm.getChildCount(); int total = lm.getItemCount(); int firstVis = lm.findFirstVisibleItemPosition(); - if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { loadSales(false); } } @@ -146,13 +135,6 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } private void loadSales(boolean reset) { - if (isLoading) return; - - if (reset) { - currentPage = 0; - isLastPage = false; - } - String query = binding.etSearchSale != null ? binding.etSearchSale.getText().toString().trim() : ""; if (query.isEmpty()) query = null; @@ -162,39 +144,12 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } Long storeId = null; - if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { - storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + List stores = viewModel.getStores().getValue(); + if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) { + storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } - saleViewModel.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - switch (resource.status) { - case LOADING: - isLoading = true; - binding.swipeRefreshSale.setRefreshing(true); - break; - case SUCCESS: - isLoading = false; - binding.swipeRefreshSale.setRefreshing(false); - if (resource.data != null) { - if (reset) saleList.clear(); - saleList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - isLastPage = resource.data.isLast(); - if (!isLastPage) currentPage++; - } - break; - case ERROR: - isLoading = false; - binding.swipeRefreshSale.setRefreshing(false); - Log.e(TAG, "Error loading sales: " + resource.message); - if (getContext() != null) { - Toast.makeText(getContext(), "Failed to load sales: " + resource.message, Toast.LENGTH_SHORT).show(); - } - break; - } - }); + viewModel.loadSales(reset, query, paymentMethod, storeId); } @Override @@ -210,4 +165,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail, args); } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } } 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 77136d4e..3a1b45a1 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 @@ -1,7 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -21,7 +20,7 @@ import com.example.petstoremobile.databinding.FragmentServiceBinding; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.ServiceViewModel; +import com.example.petstoremobile.viewmodels.ServiceListViewModel; import java.util.ArrayList; import java.util.List; @@ -34,32 +33,18 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener { - private static final String TAG = "ServiceFragment"; - private static final int PAGE_SIZE = 20; - private FragmentServiceBinding binding; private final List serviceList = new ArrayList<>(); private ServiceAdapter adapter; - private ServiceViewModel viewModel; + private ServiceListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; - // Pagination - private int currentPage = 0; - private boolean isLastPage = false; - private boolean isLoading = false; - - /** - * Initializes the fragment and its associated ViewModel. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(ServiceViewModel.class); + viewModel = new ViewModelProvider(this).get(ServiceListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -70,15 +55,27 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); + loadServices(true); - binding.fabAddService.setOnClickListener(v -> openDetail(null)); - UIUtils.setupHamburgerMenu(binding.btnHamburger, this); return binding.getRoot(); } + private void observeViewModel() { + viewModel.getServices().observe(getViewLifecycleOwner(), list -> { + serviceList.clear(); + serviceList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshService.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -98,23 +95,14 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic binding = null; } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchService); } - /** - * Sets up the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchService, () -> loadServices(true)); } - /** - * Initializes the RecyclerView with a layout manager and adapter. - */ private void setupRecyclerView() { adapter = new ServiceAdapter(serviceList, this); binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext())); @@ -129,66 +117,24 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic int visible = lm.getChildCount(); int total = lm.getItemCount(); int firstVis = lm.findFirstVisibleItemPosition(); - if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + Boolean isLoading = viewModel.getIsLoading().getValue(); + if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) { loadServices(false); } } }); } - /** - * Sets up the SwipeRefreshLayout. - */ private void setupSwipeRefresh() { binding.swipeRefreshService.setOnRefreshListener(() -> loadServices(true)); } - /** - * Fetches a page of services from the API. - */ private void loadServices(boolean reset) { - if (isLoading) return; - - if (reset) { - currentPage = 0; - isLastPage = false; - } - String query = binding.etSearchService.getText().toString().trim(); if (query.isEmpty()) query = null; - - viewModel.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - switch (resource.status) { - case LOADING: - isLoading = true; - binding.swipeRefreshService.setRefreshing(true); - break; - case SUCCESS: - isLoading = false; - binding.swipeRefreshService.setRefreshing(false); - if (resource.data != null) { - if (reset) serviceList.clear(); - serviceList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - isLastPage = resource.data.isLast(); - if (!isLastPage) currentPage++; - } - break; - case ERROR: - isLoading = false; - binding.swipeRefreshService.setRefreshing(false); - Log.e(TAG, "Error: " + resource.message); - Toast.makeText(getContext(), "Failed to load services: " + resource.message, Toast.LENGTH_SHORT).show(); - break; - } - }); + viewModel.loadServices(reset, query); } - /** - * Navigates to the service detail screen. - */ private void openDetail(ServiceDTO service) { Bundle args = new Bundle(); if (service != null) { 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 383d702a..8407d3f6 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 @@ -1,7 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; @@ -14,7 +13,7 @@ import com.example.petstoremobile.adapters.EmployeeAdapter; import com.example.petstoremobile.databinding.FragmentStaffBinding; import com.example.petstoremobile.dtos.EmployeeDTO; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.EmployeeViewModel; +import com.example.petstoremobile.viewmodels.StaffListViewModel; import dagger.hilt.android.AndroidEntryPoint; import java.util.*; @@ -22,21 +21,22 @@ import java.util.*; public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener { private FragmentStaffBinding binding; - private EmployeeViewModel employeeViewModel; - private List employeeList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private StaffListViewModel viewModel; + private List staffList = new ArrayList<>(); private EmployeeAdapter adapter; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentStaffBinding.inflate(inflater, container, false); - employeeViewModel = new ViewModelProvider(this).get(EmployeeViewModel.class); + viewModel = new ViewModelProvider(this).get(StaffListViewModel.class); setupRecyclerView(); setupSearch(); setupSwipeRefresh(); - loadStaff(); + observeViewModel(); + + viewModel.loadStaff(); binding.fabAddStaff.setOnClickListener(v -> openDetail(-1)); @@ -46,70 +46,36 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye return binding.getRoot(); } + private void observeViewModel() { + viewModel.getFilteredEmployees().observe(getViewLifecycleOwner(), list -> { + staffList.clear(); + staffList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshStaff.setRefreshing(loading); + }); + } + private void setupRecyclerView() { - adapter = new EmployeeAdapter(filteredList, this); + adapter = new EmployeeAdapter(staffList, this); binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewStaff.setAdapter(adapter); } private void setupSearch() { - UIUtils.attachSearch(binding.etSearchStaff, () -> filter(binding.etSearchStaff.getText().toString())); + UIUtils.attachSearch(binding.etSearchStaff, () -> viewModel.filter(binding.etSearchStaff.getText().toString())); } private void setupSwipeRefresh() { - binding.swipeRefreshStaff.setOnRefreshListener(this::loadStaff); - } - - private void filter(String query) { - filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(employeeList); - } else { - String lower = query.toLowerCase(); - for (EmployeeDTO e : employeeList) { - if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower)) - || (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower)) - || (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower)) - || (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) { - filteredList.add(e); - } - } - } - adapter.notifyDataSetChanged(); - } - - private void loadStaff() { - binding.swipeRefreshStaff.setRefreshing(true); - employeeViewModel.getAllEmployees(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null) { - switch (resource.status) { - case SUCCESS: - binding.swipeRefreshStaff.setRefreshing(false); - if (resource.data != null) { - employeeList.clear(); - employeeList.addAll(resource.data.getContent()); - filter(binding != null ? binding.etSearchStaff.getText().toString() : ""); - } - break; - case ERROR: - binding.swipeRefreshStaff.setRefreshing(false); - if (getContext() != null) { - Toast.makeText(getContext(), resource.message != null ? resource.message : "Failed to load staff", - Toast.LENGTH_SHORT).show(); - } - break; - case LOADING: - binding.swipeRefreshStaff.setRefreshing(true); - break; - } - } - }); + binding.swipeRefreshStaff.setOnRefreshListener(viewModel::loadStaff); } private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { - EmployeeDTO e = filteredList.get(position); + EmployeeDTO e = staffList.get(position); args.putLong("employeeId", e.getEmployeeId()); args.putString("username", e.getUsername() != null ? e.getUsername() : ""); args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : ""); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index eca755bb..78d43bd6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -9,20 +9,17 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SupplierAdapter; import com.example.petstoremobile.databinding.FragmentSupplierBinding; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.BulkDeleteHandler; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.UIUtils; -import com.example.petstoremobile.viewmodels.SupplierViewModel; +import com.example.petstoremobile.viewmodels.SupplierListViewModel; import java.util.ArrayList; import java.util.List; @@ -35,21 +32,15 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp private FragmentSupplierBinding binding; private List supplierList = new ArrayList<>(); private SupplierAdapter adapter; - private SupplierViewModel viewModel; + private SupplierListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; - /** - * Initializes the fragment and its associated SupplierViewModel. - */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + viewModel = new ViewModelProvider(this).get(SupplierListViewModel.class); } - /** - * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -60,9 +51,10 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp setupSwipeRefresh(); setupFilterToggle(); setupBulkDelete(); + observeViewModel(); + loadSupplierData(); - //Add button to opens the add dialog binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1)); UIUtils.setupHamburgerMenu(binding.btnHamburger, this); @@ -70,6 +62,18 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp return binding.getRoot(); } + private void observeViewModel() { + viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> { + supplierList.clear(); + supplierList.addAll(list); + adapter.notifyDataSetChanged(); + }); + + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { + binding.swipeRefreshSupplier.setRefreshing(loading); + }); + } + private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -89,47 +93,27 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp binding = null; } - /** - * Sets up the filter toggle button to show/hide the filter layout. - */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSupplier); } - /** - * Configures the search bar for filtering. - */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchSupplier, this::loadSupplierData); } - /** - * Sets up the SwipeRefreshLayout to allow manual reloading of supplier data. - */ private void setupSwipeRefresh() { binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData); } - /** - * Navigates to the supplier detail screen for editing an existing record or adding a new one. - */ private void openSupplierDetails(int position) { - //Make a bundle to pass data to the detail fragment Bundle args = new Bundle(); - - //if editing a supplier, add the supplier id to the bundle if (position != -1) { SupplierDTO supplier = supplierList.get(position); args.putLong("supId", supplier.getSupId()); } - NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args); } - - /** - * Handles item click in the supplier list. - */ @Override public void onSupplierClick(int position) { openSupplierDetails(position); @@ -142,47 +126,12 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp } } - /** - * Fetches all supplier data from the server through the ViewModel and updates the UI. - */ private void loadSupplierData() { String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : ""; if (query.isEmpty()) query = null; - - //Load suppliers from the backend with query and default sort - viewModel.getAllSuppliers(0, 100, query, "supCompany").observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - - // Check the status to see if the resource is loaded and display the data - switch (resource.status) { - case LOADING: - // Show loading indicator - binding.swipeRefreshSupplier.setRefreshing(true); - break; - case SUCCESS: - // Hide loading indicator and display data - binding.swipeRefreshSupplier.setRefreshing(false); - if (resource.data != null) { - supplierList.clear(); - supplierList.addAll(resource.data.getContent()); - adapter.notifyDataSetChanged(); - } - break; - case ERROR: - // Hide loading indicator and toast error message - binding.swipeRefreshSupplier.setRefreshing(false); - if (getContext() != null) { - Toast.makeText(getContext(), "Failed to load suppliers: " + resource.message, Toast.LENGTH_SHORT).show(); - } - Log.e("SupplierFragment", "Error loading suppliers: " + resource.message); - break; - } - }); + viewModel.loadSuppliers(query); } - /** - * Initializes the RecyclerView with a layout manager and adapter for displaying suppliers. - */ private void setupRecyclerView() { adapter = new SupplierAdapter(supplierList, this); binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext())); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 371e5c20..ee11fb4e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -23,7 +23,7 @@ import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.viewmodels.PetViewModel; +import com.example.petstoremobile.viewmodels.PetProfileViewModel; import java.io.File; import java.util.Locale; @@ -46,17 +46,13 @@ public class PetProfileFragment extends Fragment { @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; - private PetViewModel viewModel; + private PetProfileViewModel viewModel; private ImagePickerHelper imagePickerHelper; - - /** - * Initializes activity launchers for gallery, camera, and permissions. - */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(PetViewModel.class); + viewModel = new ViewModelProvider(this).get(PetProfileViewModel.class); imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() { @Override @@ -71,34 +67,27 @@ public class PetProfileFragment extends Fragment { }); } - /** - * Inflates the layout using view binding, initializes views, and sets up click listeners. - */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentPetProfileBinding.inflate(inflater, container, false); - // Set pet details to display if (getArguments() != null) { petId = getArguments().getLong("petId"); loadPetData(); loadPetImage((int) petId); } - //set button click listeners binding.btnBack.setOnClickListener(v -> { NavHostFragment.findNavController(this).popBackStack(); }); - //Make the edit button go to the pet detail view binding.btnEditPet.setOnClickListener(v -> { Bundle args = new Bundle(); args.putLong("petId", petId); NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, args); }); - //Make change photo button ask user to select a new photo binding.btnChangePhoto.setOnClickListener(v -> { imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage); }); @@ -112,9 +101,6 @@ public class PetProfileFragment extends Fragment { binding = null; } - /** - * Fetches current pet data from the backend and updates the UI. - */ private void loadPetData() { viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -133,7 +119,6 @@ public class PetProfileFragment extends Fragment { String status = pet.getPetStatus(); - // Display owner name only if the pet is Adopted or Owned if ("Adopted".equalsIgnoreCase(status) || "Owned".equalsIgnoreCase(status)) { binding.layoutPetOwner.setVisibility(View.VISIBLE); if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) { @@ -145,7 +130,6 @@ public class PetProfileFragment extends Fragment { binding.layoutPetOwner.setVisibility(View.GONE); } - // Display store name only if the pet is Adopted or Available if ("Available".equalsIgnoreCase(status) || "Adopted".equalsIgnoreCase(status)) { binding.layoutPetStore.setVisibility(View.VISIBLE); if (pet.getStoreName() != null && !pet.getStoreName().isEmpty()) { @@ -162,9 +146,6 @@ public class PetProfileFragment extends Fragment { }); } - /** - * Fetches and displays the pet\'s image from the server. - */ private void loadPetImage(int petId) { String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId); String token = tokenManager.getToken(); @@ -182,19 +163,14 @@ public class PetProfileFragment extends Fragment { }); } - /** - * Uploads a selected or captured image a pet photo through the ViewModel. - */ private void uploadPetImage(Uri uri) { try { File file = FileUtils.getFileFromUri(requireContext(), uri); if (file == null) return; - // Create RequestBody for file upload RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); - // Use ViewModel to upload image viewModel.uploadPetImage(petId, body).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status != Resource.Status.LOADING) { if (resource.status == Resource.Status.SUCCESS) { @@ -210,9 +186,6 @@ public class PetProfileFragment extends Fragment { } } - /** - * Sends a request to the ViewModel to remove the current pet photo. - */ private void deletePetImage() { viewModel.deletePetImage(petId).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status != Resource.Status.LOADING) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java new file mode 100644 index 00000000..683c79b4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java @@ -0,0 +1,83 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.AdoptionRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AdoptionListViewModel extends ViewModel { + private final AdoptionRepository adoptionRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> adoptions = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + + @Inject + public AdoptionListViewModel(AdoptionRepository adoptionRepository, StoreRepository storeRepository) { + this.adoptionRepository = adoptionRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getAdoptions() { return adoptions; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadAdoptions(boolean reset, String query, String status, Long storeId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + if ("All Statuses".equals(status)) status = null; + + isLoading.setValue(true); + adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, "adoptionDate,desc").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + List currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue()); + currentList.addAll(resource.data.getContent()); + adoptions.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeleteAdoptions(List ids) { + return adoptionRepository.bulkDeleteAdoptions(new BulkDeleteRequest(ids)); + } +} 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 new file mode 100644 index 00000000..76c039ac --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java @@ -0,0 +1,163 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.repositories.SaleRepository; +import com.example.petstoremobile.utils.Resource; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AnalyticsViewModel extends ViewModel { + private final SaleRepository saleRepository; + + private final MutableLiveData analyticsData = new MutableLiveData<>(); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + private final MutableLiveData errorMessage = new MutableLiveData<>(); + + @Inject + public AnalyticsViewModel(SaleRepository saleRepository) { + this.saleRepository = saleRepository; + } + + public LiveData getAnalyticsData() { return analyticsData; } + public LiveData getIsLoading() { return isLoading; } + public LiveData getErrorMessage() { return errorMessage; } + + public void loadAnalytics() { + isLoading.setValue(true); + errorMessage.setValue(null); + saleRepository.getAllSales(0, 1000, null, null, null, "saleDate,desc").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + computeAnalytics(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + errorMessage.setValue(resource.message); + isLoading.setValue(false); + } + } + }); + } + + private void computeAnalytics(List sales) { + List regularSales = new ArrayList<>(); + for (SaleDTO s : sales) { + if (!Boolean.TRUE.equals(s.getIsRefund())) + regularSales.add(s); + } + + AnalyticsData data = new AnalyticsData(); + + // Summary + BigDecimal totalRevenue = BigDecimal.ZERO; + int totalItems = 0; + for (SaleDTO s : regularSales) { + if (s.getTotalAmount() != null) totalRevenue = totalRevenue.add(s.getTotalAmount()); + if (s.getItems() != null) { + for (SaleDTO.SaleItemDTO item : s.getItems()) { + if (item.getQuantity() != null) totalItems += Math.abs(item.getQuantity()); + } + } + } + data.totalRevenue = totalRevenue; + data.totalTransactions = regularSales.size(); + data.avgTransaction = data.totalTransactions > 0 + ? totalRevenue.divide(BigDecimal.valueOf(data.totalTransactions), 2, RoundingMode.HALF_UP) + : 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; + revenueByProduct.merge(name, lineTotal, BigDecimal::add); + quantityByProduct.merge(name, qty, Integer::sum); + } + } + } + + // Sort Top Revenue + 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); + + // 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); + + // Payment Stats + data.paymentMethodStats = new ArrayList<>(paymentCount.entrySet()); + + // 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); + } + 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); + } + } + data.dailyRevenue = new ArrayList<>(dailyMap.entrySet()); + + analyticsData.setValue(data); + } + + public static class AnalyticsData { + public BigDecimal totalRevenue; + public int totalTransactions; + public BigDecimal avgTransaction; + public int totalItems; + public List> topRevenueProducts; + public List> topQuantityProducts; + public List> paymentMethodStats; + public List> employeePerformance; + public List> dailyRevenue; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java new file mode 100644 index 00000000..8bdaf699 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java @@ -0,0 +1,65 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.AppointmentRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AppointmentListViewModel extends ViewModel { + private final AppointmentRepository appointmentRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> appointments = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public AppointmentListViewModel(AppointmentRepository appointmentRepository, StoreRepository storeRepository) { + this.appointmentRepository = appointmentRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getAppointments() { return appointments; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + + public void loadAppointments(String query, String status, Long storeId, String date, Long employeeId) { + isLoading.setValue(true); + appointmentRepository.getAllAppointments(0, 500, query, status, storeId, date, employeeId).observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + appointments.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeleteAppointments(List ids) { + return appointmentRepository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java new file mode 100644 index 00000000..0aa8021f --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java @@ -0,0 +1,132 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.ConversationDTO; +import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.MessageDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SendMessageRequest; +import com.example.petstoremobile.models.Chat; +import com.example.petstoremobile.models.Message; +import com.example.petstoremobile.repositories.ChatRepository; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ChatListViewModel extends ViewModel { + private final ChatRepository chatRepository; + private final CustomerRepository customerRepository; + + private final MutableLiveData> chatList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> messageList = new MutableLiveData<>(new ArrayList<>()); + private final Map customerNames = new HashMap<>(); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public ChatListViewModel(ChatRepository chatRepository, CustomerRepository customerRepository) { + this.chatRepository = chatRepository; + this.customerRepository = customerRepository; + } + + public LiveData> getChatList() { return chatList; } + public LiveData> getMessageList() { return messageList; } + public LiveData getIsLoading() { return isLoading; } + + public void loadCustomers() { + customerRepository.getAllCustomers(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + for (CustomerDTO c : resource.data.getContent()) { + customerNames.put(c.getCustomerId(), c.getFullName()); + } + loadConversations(); + } + }); + } + + public void loadConversations() { + isLoading.setValue(true); + chatRepository.getAllConversations().observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + List chats = new ArrayList<>(); + for (ConversationDTO dto : resource.data) { + String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + chats.add(new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId())); + } + chatList.setValue(chats); + isLoading.setValue(false); + } else if (resource != null && resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + }); + } + + public void loadMessageHistory(Long conversationId) { + chatRepository.getMessages(conversationId).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + List messages = new ArrayList<>(); + for (MessageDTO dto : resource.data) { + messages.add(dtoToModel(dto)); + } + messageList.setValue(messages); + } + }); + } + + public LiveData> sendMessage(Long conversationId, String text) { + return chatRepository.sendMessage(conversationId, new SendMessageRequest(text)); + } + + public void addMessageLocally(MessageDTO dto) { + List current = new ArrayList<>(messageList.getValue()); + current.add(dtoToModel(dto)); + messageList.setValue(current); + } + + public void updateConversationLocally(ConversationDTO dto) { + List current = new ArrayList<>(chatList.getValue()); + boolean updated = false; + String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + + for (int i = 0; i < current.size(); i++) { + if (current.get(i).getChatId().equals(String.valueOf(dto.getId()))) { + current.set(i, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId())); + updated = true; + break; + } + } + if (!updated) { + current.add(0, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId())); + } + chatList.setValue(current); + } + + private Message dtoToModel(MessageDTO dto) { + Message m = new Message(); + m.setId(dto.getId()); + m.setConversationId(dto.getConversationId()); + m.setSenderId(dto.getSenderId()); + m.setContent(dto.getContent()); + m.setTimestamp(dto.getTimestamp()); + m.setIsRead(dto.getIsRead()); + m.setAttachmentUrl(dto.getAttachmentUrl()); + m.setAttachmentName(dto.getAttachmentName()); + m.setAttachmentType(dto.getAttachmentType()); + return m; + } + + public String getCustomerName(Long customerId) { + return customerNames.getOrDefault(customerId, "Customer #" + customerId); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java new file mode 100644 index 00000000..c91f8337 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java @@ -0,0 +1,81 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.InventoryRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class InventoryListViewModel extends ViewModel { + private final InventoryRepository inventoryRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> inventory = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + + @Inject + public InventoryListViewModel(InventoryRepository inventoryRepository, StoreRepository storeRepository) { + this.inventoryRepository = inventoryRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getInventory() { return inventory; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadInventory(boolean reset, String query, Long storeId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + isLoading.setValue(true); + inventoryRepository.getAllInventory(query, storeId, currentPage, PAGE_SIZE, "product.prodName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + List currentList = reset ? new ArrayList<>() : new ArrayList<>(inventory.getValue()); + currentList.addAll(resource.data.getContent()); + inventory.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeleteInventory(List ids) { + return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java new file mode 100644 index 00000000..8a567450 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java @@ -0,0 +1,69 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class PetListViewModel extends ViewModel { + private final PetRepository petRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> pets = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public PetListViewModel(PetRepository petRepository, StoreRepository storeRepository) { + this.petRepository = petRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getPets() { return pets; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + + public void loadPets(String query, String status, String species, Long storeId) { + if ("All Statuses".equals(status)) status = null; + if ("All Species".equals(species)) species = null; + + isLoading.setValue(true); + petRepository.getAllPets(0, 100, query, status, species, storeId, null, "petName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + pets.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeletePets(List ids) { + return petRepository.bulkDeletePets(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java new file mode 100644 index 00000000..1fb75f1c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java @@ -0,0 +1,35 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import okhttp3.MultipartBody; + +@HiltViewModel +public class PetProfileViewModel extends ViewModel { + private final PetRepository repository; + + @Inject + public PetProfileViewModel(PetRepository repository) { + this.repository = repository; + } + + public LiveData> getPetById(Long id) { + return repository.getPetById(id); + } + + public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { + return repository.uploadPetImage(id, image); + } + + public LiveData> deletePetImage(Long id) { + return repository.deletePetImage(id); + } +} 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 new file mode 100644 index 00000000..ecd2d238 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java @@ -0,0 +1,60 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.repositories.CategoryRepository; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ProductListViewModel extends ViewModel { + private final ProductRepository productRepository; + private final CategoryRepository categoryRepository; + + private final MutableLiveData> products = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> categories = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public ProductListViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) { + this.productRepository = productRepository; + this.categoryRepository = categoryRepository; + } + + public LiveData> getProducts() { return products; } + public LiveData> getCategories() { return categories; } + public LiveData getIsLoading() { return isLoading; } + + public void loadProducts(String query, Long categoryId) { + isLoading.setValue(true); + productRepository.getAllProducts(query, categoryId, 0, 100, "prodName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + products.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadCategories() { + categoryRepository.getAllCategories(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + categories.setValue(resource.data.getContent()); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java new file mode 100644 index 00000000..cad846c4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java @@ -0,0 +1,77 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.dtos.ProductSupplierDTO; +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.repositories.ProductSupplierRepository; +import com.example.petstoremobile.repositories.SupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ProductSupplierListViewModel extends ViewModel { + private final ProductSupplierRepository psRepository; + private final ProductRepository productRepository; + private final SupplierRepository supplierRepository; + + private final MutableLiveData> productSuppliers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> products = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> suppliers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public ProductSupplierListViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) { + this.psRepository = psRepository; + this.productRepository = productRepository; + this.supplierRepository = supplierRepository; + } + + public LiveData> getProductSuppliers() { return productSuppliers; } + public LiveData> getProducts() { return products; } + public LiveData> getSuppliers() { return suppliers; } + public LiveData getIsLoading() { return isLoading; } + + public void loadProductSuppliers(String query, Long productId, Long supplierId) { + isLoading.setValue(true); + psRepository.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + productSuppliers.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadFilterData() { + productRepository.getAllProducts(null, null, 0, 100, "prodName").observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + products.setValue(resource.data.getContent()); + } + }); + + supplierRepository.getAllSuppliers(0, 100, null, "supCompany").observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + suppliers.setValue(resource.data.getContent()); + } + }); + } + + public LiveData> bulkDeleteProductSuppliers(List ids) { + return psRepository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java new file mode 100644 index 00000000..438f4198 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java @@ -0,0 +1,60 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PurchaseOrderDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.PurchaseOrderRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class PurchaseOrderListViewModel extends ViewModel { + private final PurchaseOrderRepository purchaseOrderRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> purchaseOrders = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public PurchaseOrderListViewModel(PurchaseOrderRepository purchaseOrderRepository, StoreRepository storeRepository) { + this.purchaseOrderRepository = purchaseOrderRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getPurchaseOrders() { return purchaseOrders; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + + public void loadPurchaseOrders(String query, Long storeId) { + isLoading.setValue(true); + purchaseOrderRepository.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + purchaseOrders.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java new file mode 100644 index 00000000..a364a7d8 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java @@ -0,0 +1,77 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.SaleRepository; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SaleListViewModel extends ViewModel { + private final SaleRepository saleRepository; + private final StoreRepository storeRepository; + + private final MutableLiveData> sales = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> stores = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + + @Inject + public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository) { + this.saleRepository = saleRepository; + this.storeRepository = storeRepository; + } + + public LiveData> getSales() { return sales; } + public LiveData> getStores() { return stores; } + public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadSales(boolean reset, String query, String paymentMethod, Long storeId) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + isLoading.setValue(true); + saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, "saleDate,desc").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + List currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue()); + currentList.addAll(resource.data.getContent()); + sales.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void loadStores() { + storeRepository.getAllStores(0, 100).observeForever(resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + stores.setValue(resource.data.getContent()); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java new file mode 100644 index 00000000..d0fa121b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java @@ -0,0 +1,68 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.repositories.ServiceRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ServiceListViewModel extends ViewModel { + private final ServiceRepository repository; + + private final MutableLiveData> services = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + private int currentPage = 0; + private boolean isLastPage = false; + private static final int PAGE_SIZE = 20; + + @Inject + public ServiceListViewModel(ServiceRepository repository) { + this.repository = repository; + } + + public LiveData> getServices() { return services; } + public LiveData getIsLoading() { return isLoading; } + public boolean isLastPage() { return isLastPage; } + + public void loadServices(boolean reset, String query) { + if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + isLoading.setValue(true); + repository.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + List currentList = reset ? new ArrayList<>() : new ArrayList<>(services.getValue()); + currentList.addAll(resource.data.getContent()); + services.setValue(currentList); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public LiveData> bulkDeleteServices(List ids) { + return repository.bulkDeleteServices(new BulkDeleteRequest(ids)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java new file mode 100644 index 00000000..1bd317ca --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java @@ -0,0 +1,71 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.repositories.EmployeeRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class StaffListViewModel extends ViewModel { + private final EmployeeRepository repository; + + private final MutableLiveData> employees = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> filteredEmployees = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + private String lastQuery = ""; + + @Inject + public StaffListViewModel(EmployeeRepository repository) { + this.repository = repository; + } + + public LiveData> getFilteredEmployees() { return filteredEmployees; } + public LiveData getIsLoading() { return isLoading; } + + public void loadStaff() { + isLoading.setValue(true); + repository.getAllEmployees(0, 100).observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + employees.setValue(resource.data.getContent()); + filter(lastQuery); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public void filter(String query) { + this.lastQuery = query; + List all = employees.getValue(); + if (all == null) return; + + if (query.isEmpty()) { + filteredEmployees.setValue(new ArrayList<>(all)); + } else { + List filtered = new ArrayList<>(); + String lower = query.toLowerCase(); + for (EmployeeDTO e : all) { + if ((e.getFullName() != null && e.getFullName().toLowerCase().contains(lower)) + || (e.getUsername() != null && e.getUsername().toLowerCase().contains(lower)) + || (e.getEmail() != null && e.getEmail().toLowerCase().contains(lower)) + || (e.getPhone() != null && e.getPhone().toLowerCase().contains(lower))) { + filtered.add(e); + } + } + filteredEmployees.setValue(filtered); + } + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java new file mode 100644 index 00000000..072ad3bd --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java @@ -0,0 +1,51 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.repositories.SupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SupplierListViewModel extends ViewModel { + private final SupplierRepository repository; + + private final MutableLiveData> suppliers = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + + @Inject + public SupplierListViewModel(SupplierRepository repository) { + this.repository = repository; + } + + public LiveData> getSuppliers() { return suppliers; } + public LiveData getIsLoading() { return isLoading; } + + public void loadSuppliers(String query) { + isLoading.setValue(true); + repository.getAllSuppliers(0, 100, query, "supCompany").observeForever(resource -> { + if (resource != null) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + suppliers.setValue(resource.data.getContent()); + isLoading.setValue(false); + } else if (resource.status == Resource.Status.ERROR) { + isLoading.setValue(false); + } + } + }); + } + + public LiveData> bulkDeleteSuppliers(List ids) { + return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids)); + } +} -- 2.49.1 From 75341c93d89f003f5f804f6396b7294349e752b5 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:17:11 -0600 Subject: [PATCH 29/46] deleted unused viewmodels --- .../viewmodels/AdoptionViewModel.java | 68 ------------- .../viewmodels/AppointmentViewModel.java | 42 -------- .../viewmodels/ChatViewModel.java | 58 ----------- .../viewmodels/CustomerViewModel.java | 47 --------- .../viewmodels/EmployeeViewModel.java | 43 -------- .../viewmodels/InventoryViewModel.java | 90 ----------------- .../viewmodels/PetViewModel.java | 98 ------------------- .../viewmodels/ProductSupplierViewModel.java | 58 ----------- .../viewmodels/ProductViewModel.java | 84 ---------------- .../viewmodels/PurchaseOrderViewModel.java | 37 ------- .../viewmodels/SaleViewModel.java | 35 ------- .../viewmodels/ServiceViewModel.java | 68 ------------- .../viewmodels/StoreViewModel.java | 47 --------- .../viewmodels/SupplierViewModel.java | 68 ------------- .../viewmodels/UserViewModel.java | 27 ----- 15 files changed, 870 deletions(-) delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/EmployeeViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java deleted file mode 100644 index 12eb9779..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.AdoptionDTO; -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.repositories.AdoptionRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class AdoptionViewModel extends ViewModel { - private final AdoptionRepository repository; - - @Inject - public AdoptionViewModel(AdoptionRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all adoptions with filters. - */ - public LiveData>> getAllAdoptions(int page, int size, String query, String status, Long storeId, String date, Long employeeId) { - return repository.getAllAdoptions(page, size, query, status, storeId, date, employeeId); - } - - /** - * Retrieves a single adoption by its ID. - */ - public LiveData> getAdoptionById(Long id) { - return repository.getAdoptionById(id); - } - - /** - * Creates a new adoption record. - */ - public LiveData> createAdoption(AdoptionDTO adoption) { - return repository.createAdoption(adoption); - } - - /** - * Updates an existing adoption record by ID. - */ - public LiveData> updateAdoption(Long id, AdoptionDTO adoption) { - return repository.updateAdoption(id, adoption); - } - - /** - * Deletes an adoption record by ID. - */ - public LiveData> deleteAdoption(Long id) { - return repository.deleteAdoption(id); - } - - /** - * Deletes multiple adoption records. - */ - public LiveData> bulkDeleteAdoptions(List ids) { - return repository.bulkDeleteAdoptions(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java deleted file mode 100644 index 5605a8d1..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.AppointmentDTO; -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.repositories.AppointmentRepository; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class AppointmentViewModel extends ViewModel { - private final AppointmentRepository repository; - - @Inject - public AppointmentViewModel(AppointmentRepository repository) { - this.repository = repository; - } - - // API CRUD - - /** - * Fetches a paginated list of all appointments with optional filters. - */ - public LiveData>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date, Long employeeId) { - return repository.getAllAppointments(page, size, query, status, storeId, date, employeeId); - } - - /** - * Deletes multiple appointment records. - */ - public LiveData> bulkDeleteAppointments(List ids) { - return repository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java deleted file mode 100644 index 2b516490..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.ConversationDTO; -import com.example.petstoremobile.dtos.CustomerDTO; -import com.example.petstoremobile.dtos.MessageDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.SendMessageRequest; -import com.example.petstoremobile.repositories.ChatRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; -/** - * ViewModel for managing chat-related UI state and data operations. - */ -@HiltViewModel -public class ChatViewModel extends ViewModel { - private final ChatRepository repository; - - @Inject - public ChatViewModel(ChatRepository repository) { - this.repository = repository; - } - - /** - * Retrieves all chat conversations for the current user. - */ - public LiveData>> getAllConversations() { - return repository.getAllConversations(); - } - - /** - * Retrieves the message history for a specific conversation. - */ - public LiveData>> getMessages(Long conversationId) { - return repository.getMessages(conversationId); - } - - /** - * Sends a plain text message to a conversation. - */ - public LiveData> sendMessage(Long conversationId, SendMessageRequest request) { - return repository.sendMessage(conversationId, request); - } - - /** - * Fetches a paginated list of customers. - */ - public LiveData>> getAllCustomers(int page, int size) { - return repository.getAllCustomers(page, size); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java deleted file mode 100644 index d916ff75..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.CustomerDTO; -import com.example.petstoremobile.dtos.DropdownDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.repositories.CustomerRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class CustomerViewModel extends ViewModel { - private final CustomerRepository repository; - - @Inject - public CustomerViewModel(CustomerRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all customers. - */ - public LiveData>> getAllCustomers(int page, int size) { - return repository.getAllCustomers(page, size); - } - - /** - * Retrieves a single customer by their ID. - */ - public LiveData> getCustomerById(Long id) { - return repository.getCustomerById(id); - } - - /** - * Retrieves a list of customer dropdowns from the repository. - */ - public LiveData>> getCustomerDropdowns() { - return repository.getCustomerDropdowns(); - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/EmployeeViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/EmployeeViewModel.java deleted file mode 100644 index 5454269e..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/EmployeeViewModel.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.EmployeeDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.repositories.EmployeeRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class EmployeeViewModel extends ViewModel { - private final EmployeeRepository employeeRepository; - - @Inject - public EmployeeViewModel(EmployeeRepository employeeRepository) { - this.employeeRepository = employeeRepository; - } - - public LiveData>> getAllEmployees(int page, int size) { - return employeeRepository.getAllEmployees(page, size); - } - - public LiveData> getEmployeeById(Long id) { - return employeeRepository.getEmployeeById(id); - } - - public LiveData> createEmployee(EmployeeDTO dto) { - return employeeRepository.createEmployee(dto); - } - - public LiveData> updateEmployee(Long id, EmployeeDTO dto) { - return employeeRepository.updateEmployee(id, dto); - } - - public LiveData> deleteEmployee(Long id) { - return employeeRepository.deleteEmployee(id); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java deleted file mode 100644 index c7ccc070..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.CategoryDTO; -import com.example.petstoremobile.dtos.InventoryDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.StoreDTO; -import com.example.petstoremobile.repositories.CategoryRepository; -import com.example.petstoremobile.repositories.InventoryRepository; -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; - -@HiltViewModel -public class InventoryViewModel extends ViewModel { - private final InventoryRepository inventoryRepository; - private final CategoryRepository categoryRepository; - private final StoreRepository storeRepository; - - @Inject - public InventoryViewModel(InventoryRepository inventoryRepository, CategoryRepository categoryRepository, StoreRepository storeRepository) { - this.inventoryRepository = inventoryRepository; - this.categoryRepository = categoryRepository; - this.storeRepository = storeRepository; - } - - /** - * Retrieves a paginated list of inventory items, with optional filtering and sorting. - */ - public LiveData>> getAllInventory(String query, Long storeId, int page, int size, String sort) { - return inventoryRepository.getAllInventory(query, storeId, page, size, sort); - } - - /** - * Retrieves a single inventory item by its ID. - */ - public LiveData> getInventoryById(Long id) { - return inventoryRepository.getInventoryById(id); - } - - /** - * Creates a new inventory record. - */ - public LiveData> createInventory(InventoryDTO request) { - return inventoryRepository.createInventory(request); - } - - /** - * Updates an existing inventory record by ID. - */ - public LiveData> updateInventory(Long id, InventoryDTO request) { - return inventoryRepository.updateInventory(id, request); - } - - /** - * Deletes an inventory record by ID. - */ - public LiveData> deleteInventory(Long id) { - return inventoryRepository.deleteInventory(id); - } - - /** - * Deletes multiple inventory records in a single request. - */ - public LiveData> bulkDeleteInventory(List ids) { - return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids)); - } - - /** - * Retrieves a paginated list of categories. - */ - public LiveData>> getAllCategories(int page, int size) { - return categoryRepository.getAllCategories(page, size); - } - - /** - * Retrieves a paginated list of stores. - */ - public LiveData>> getAllStores(int page, int size) { - return storeRepository.getAllStores(page, size); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java deleted file mode 100644 index 76770392..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.DropdownDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.PetDTO; -import com.example.petstoremobile.repositories.PetRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; -import okhttp3.MultipartBody; - -@HiltViewModel -public class PetViewModel extends ViewModel { - private final PetRepository repository; - - @Inject - public PetViewModel(PetRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of pets with filters. - */ - public LiveData>> getAllPets(int page, int size, String query, String status, String species, Long storeId, Long customerId, String sort) { - return repository.getAllPets(page, size, query, status, species, storeId, customerId, sort); - } - - /** - * Retrieves a list of pets for a specific customer from the repository. - */ - public LiveData>> getCustomerPets(Long customerId) { - return repository.getCustomerPets(customerId); - } - - /** - * Retrieves a list of pets available for adoption from the repository. - */ - public LiveData>> getAdoptionPets() { - return repository.getAdoptionPets(); - } - - /** - * Retrieves a single pet by its ID. - */ - public LiveData> getPetById(Long id) { - return repository.getPetById(id); - } - - /** - * Creates a new pet record. - */ - public LiveData> createPet(PetDTO pet) { - return repository.createPet(pet); - } - - /** - * Updates an existing pet record by ID. - */ - public LiveData> updatePet(Long id, PetDTO pet) { - return repository.updatePet(id, pet); - } - - /** - * Deletes a pet record by ID. - */ - public LiveData> deletePet(Long id) { - return repository.deletePet(id); - } - - /** - * Deletes multiple pet records. - */ - public LiveData> bulkDeletePets(List ids) { - return repository.bulkDeletePets(new BulkDeleteRequest(ids)); - } - - /** - * Uploads an image for a specific pet. - */ - public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { - return repository.uploadPetImage(id, image); - } - - /** - * Deletes the image associated with a specific pet. - */ - public LiveData> deletePetImage(Long id) { - return repository.deletePetImage(id); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java deleted file mode 100644 index f4302225..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.ProductSupplierDTO; -import com.example.petstoremobile.repositories.ProductSupplierRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class ProductSupplierViewModel extends ViewModel { - private final ProductSupplierRepository repository; - - @Inject - public ProductSupplierViewModel(ProductSupplierRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all product-supplier relationships. - */ - public LiveData>> getAllProductSuppliers(int page, int size, String query, Long productId, Long supplierId, String sort) { - return repository.getAllProductSuppliers(page, size, query, productId, supplierId, sort); - } - - /** - * Creates a new product-supplier relationship. - */ - public LiveData> createProductSupplier(ProductSupplierDTO dto) { - return repository.createProductSupplier(dto); - } - - /** - * Updates an existing product-supplier relationship. - */ - public LiveData> updateProductSupplier(Long productId, Long supplierId, ProductSupplierDTO dto) { - return repository.updateProductSupplier(productId, supplierId, dto); - } - - /** - * Deletes a product-supplier relationship by product and supplier IDs. - */ - public LiveData> deleteProductSupplier(Long productId, Long supplierId) { - return repository.deleteProductSupplier(productId, supplierId); - } - - public LiveData> bulkDeleteProductSuppliers(List ids) { - return repository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java deleted file mode 100644 index b44c08eb..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.CategoryDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.ProductDTO; -import com.example.petstoremobile.repositories.CategoryRepository; -import com.example.petstoremobile.repositories.ProductRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; -import okhttp3.MultipartBody; - -@HiltViewModel -public class ProductViewModel extends ViewModel { - private final ProductRepository productRepository; - private final CategoryRepository categoryRepository; - - @Inject - public ProductViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) { - this.productRepository = productRepository; - this.categoryRepository = categoryRepository; - } - - /** - * Retrieves a paginated list of products, optionally filtered by a query string, category and sorted. - */ - public LiveData>> getAllProducts(String query, Long categoryId, int page, int size, String sort) { - return productRepository.getAllProducts(query, categoryId, page, size, sort); - } - - /** - * Retrieves a single product by its ID. - */ - public LiveData> getProductById(Long id) { - return productRepository.getProductById(id); - } - - /** - * Creates a new product. - */ - public LiveData> createProduct(ProductDTO product) { - return productRepository.createProduct(product); - } - - /** - * Updates an existing product by ID. - */ - public LiveData> updateProduct(Long id, ProductDTO product) { - return productRepository.updateProduct(id, product); - } - - /** - * Deletes a product by its ID. - */ - public LiveData> deleteProduct(Long id) { - return productRepository.deleteProduct(id); - } - - /** - * Uploads an image for a specific product. - */ - public LiveData> uploadProductImage(Long id, MultipartBody.Part image) { - return productRepository.uploadProductImage(id, image); - } - - /** - * Deletes the image associated with a specific product. - */ - public LiveData> deleteProductImage(Long id) { - return productRepository.deleteProductImage(id); - } - - /** - * Retrieves a paginated list of all product categories. - */ - public LiveData>> getAllCategories(int page, int size) { - return categoryRepository.getAllCategories(page, size); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java deleted file mode 100644 index d9a24e5e..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.PurchaseOrderDTO; -import com.example.petstoremobile.repositories.PurchaseOrderRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class PurchaseOrderViewModel extends ViewModel { - private final PurchaseOrderRepository repository; - - @Inject - public PurchaseOrderViewModel(PurchaseOrderRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all purchase orders. - */ - public LiveData>> getAllPurchaseOrders(int page, int size, String query, Long storeId, String sort) { - return repository.getAllPurchaseOrders(page, size, query, storeId, sort); - } - - /** - * Retrieves a single purchase order by its ID. - */ - public LiveData> getPurchaseOrderById(Long id) { - return repository.getPurchaseOrderById(id); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java deleted file mode 100644 index a02d3382..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.SaleDTO; -import com.example.petstoremobile.repositories.SaleRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class SaleViewModel extends ViewModel { - private final SaleRepository saleRepository; - - @Inject - public SaleViewModel(SaleRepository saleRepository) { - this.saleRepository = saleRepository; - } - - public LiveData>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, String sortBy) { - return saleRepository.getAllSales(page, size, query, paymentMethod, storeId, sortBy); - } - - public LiveData> getSaleById(Long id) { - return saleRepository.getSaleById(id); - } - - public LiveData> createSale(SaleDTO sale) { - return saleRepository.createSale(sale); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java deleted file mode 100644 index ebd5c3b6..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.ServiceDTO; -import com.example.petstoremobile.repositories.ServiceRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class ServiceViewModel extends ViewModel { - private final ServiceRepository repository; - - @Inject - public ServiceViewModel(ServiceRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all services. - */ - public LiveData>> getAllServices(int page, int size, String query, String sort) { - return repository.getAllServices(page, size, query, sort); - } - - /** - * Retrieves a single service by its ID. - */ - public LiveData> getServiceById(Long id) { - return repository.getServiceById(id); - } - - /** - * Creates a new service. - */ - public LiveData> createService(ServiceDTO service) { - return repository.createService(service); - } - - /** - * Updates an existing service by ID. - */ - public LiveData> updateService(Long id, ServiceDTO service) { - return repository.updateService(id, service); - } - - /** - * Deletes a service by ID. - */ - public LiveData> deleteService(Long id) { - return repository.deleteService(id); - } - - /** - * Deletes multiple services. - */ - public LiveData> bulkDeleteServices(List ids) { - return repository.bulkDeleteServices(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java deleted file mode 100644 index 0388c646..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.DropdownDTO; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.StoreDTO; -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; - -@HiltViewModel -public class StoreViewModel extends ViewModel { - private final StoreRepository repository; - - @Inject - public StoreViewModel(StoreRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all stores. - */ - public LiveData>> getAllStores(int page, int size) { - return repository.getAllStores(page, size); - } - - /** - * Fetches a list of store dropdowns from the repository. - */ - public LiveData>> getStoreDropdowns() { - return repository.getStoreDropdowns(); - } - - /** - * Fetches a list of employees for a specific store. - */ - public LiveData>> getStoreEmployees(Long storeId) { - return repository.getStoreEmployees(storeId); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java deleted file mode 100644 index 1486a562..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.BulkDeleteRequest; -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.SupplierDTO; -import com.example.petstoremobile.repositories.SupplierRepository; -import com.example.petstoremobile.utils.Resource; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class SupplierViewModel extends ViewModel { - private final SupplierRepository repository; - - @Inject - public SupplierViewModel(SupplierRepository repository) { - this.repository = repository; - } - - /** - * Fetches a paginated list of all suppliers. - */ - public LiveData>> getAllSuppliers(int page, int size, String query, String sort) { - return repository.getAllSuppliers(page, size, query, sort); - } - - /** - * Retrieves a single supplier by its ID. - */ - public LiveData> getSupplierById(Long id) { - return repository.getSupplierById(id); - } - - /** - * Creates a new supplier record. - */ - public LiveData> createSupplier(SupplierDTO supplier) { - return repository.createSupplier(supplier); - } - - /** - * Updates an existing supplier record by ID. - */ - public LiveData> updateSupplier(Long id, SupplierDTO supplier) { - return repository.updateSupplier(id, supplier); - } - - /** - * Deletes a supplier record by ID. - */ - public LiveData> deleteSupplier(Long id) { - return repository.deleteSupplier(id); - } - - /** - * Deletes multiple supplier records. - */ - public LiveData> bulkDeleteSuppliers(List ids) { - return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids)); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java deleted file mode 100644 index d839f6c4..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.petstoremobile.viewmodels; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModel; - -import com.example.petstoremobile.dtos.PageResponse; -import com.example.petstoremobile.dtos.UserDTO; -import com.example.petstoremobile.repositories.UserRepository; -import com.example.petstoremobile.utils.Resource; - -import javax.inject.Inject; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -@HiltViewModel -public class UserViewModel extends ViewModel { - private final UserRepository userRepository; - - @Inject - public UserViewModel(UserRepository userRepository) { - this.userRepository = userRepository; - } - - public LiveData>> getUsers(String role, int page, int size) { - return userRepository.getUsers(role, page, size); - } -} -- 2.49.1 From bc555808318e491c663055a20755774a51b62063 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:24:20 -0600 Subject: [PATCH 30/46] bug fix --- .../com/example/petstoremobile/dtos/ConversationDTO.java | 8 ++++++++ .../petstoremobile/viewmodels/AdoptionListViewModel.java | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java index 316aa467..3a7ea42e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java @@ -12,6 +12,14 @@ public class ConversationDTO { public ConversationDTO() {} + public ConversationDTO(Long id, Long customerId, Long staffId, String lastMessage, String status) { + this.id = id; + this.customerId = customerId; + this.staffId = staffId; + this.lastMessage = lastMessage; + this.status = status; + } + public Long getId() { return id; } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java index 683c79b4..6ebe7a99 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java @@ -53,7 +53,7 @@ public class AdoptionListViewModel extends ViewModel { if ("All Statuses".equals(status)) status = null; isLoading.setValue(true); - adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, "adoptionDate,desc").observeForever(resource -> { + adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, "adoptionDate,desc", null).observeForever(resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { List currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue()); -- 2.49.1 From 3db45bde6c734f97b6a64d0a57712c762f6c0588 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:37:24 -0600 Subject: [PATCH 31/46] fixed bug again --- .../fragments/listfragments/AdoptionFragment.java | 2 +- .../petstoremobile/viewmodels/AdoptionListViewModel.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index 39317495..2d687c6e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -210,7 +210,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop if (status.equals("All Statuses")) status = null; else status = status.toUpperCase(); - viewModel.loadAdoptions(true, query, status, storeId); + viewModel.loadAdoptions(true, query, status, storeId, selectedDateString, null); } private void openDetail(int position) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java index 6ebe7a99..7c0b72b8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java @@ -42,7 +42,7 @@ public class AdoptionListViewModel extends ViewModel { public LiveData getIsLoading() { return isLoading; } public boolean isLastPage() { return isLastPage; } - public void loadAdoptions(boolean reset, String query, String status, Long storeId) { + public void loadAdoptions(boolean reset, String query, String status, Long storeId, String date, Long employeeId) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; if (reset) { @@ -53,7 +53,7 @@ public class AdoptionListViewModel extends ViewModel { if ("All Statuses".equals(status)) status = null; isLoading.setValue(true); - adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, "adoptionDate,desc", null).observeForever(resource -> { + adoptionRepository.getAllAdoptions(currentPage, PAGE_SIZE, query, status, storeId, date, employeeId).observeForever(resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { List currentList = reset ? new ArrayList<>() : new ArrayList<>(adoptions.getValue()); -- 2.49.1 From 1205459e53ed4a70339878633dd8d68c05802187 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Thu, 9 Apr 2026 15:51:21 -0600 Subject: [PATCH 32/46] Refine GUI Behavior --- .../controllers/AppointmentController.java | 15 ++++++++++++- .../controllers/SaleController.java | 19 ++++++++++++++++ .../AppointmentDialogController.java | 19 +++++++++++++--- .../petshopdesktop/util/TableViewSupport.java | 2 -- .../modelviews/adoption-view.fxml | 2 +- .../modelviews/analytics-view.fxml | 2 +- .../modelviews/appointment-view.fxml | 2 +- .../modelviews/inventory-view.fxml | 2 +- .../petshopdesktop/modelviews/pet-view.fxml | 2 +- .../modelviews/product-supplier-view.fxml | 2 +- .../modelviews/product-view.fxml | 2 +- .../modelviews/purchase-order-view.fxml | 2 +- .../petshopdesktop/modelviews/sale-view.fxml | 22 +++++++++---------- .../modelviews/service-view.fxml | 2 +- .../modelviews/staff-accounts-view.fxml | 2 +- .../modelviews/supplier-view.fxml | 2 +- 16 files changed, 71 insertions(+), 28 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index 3cc4a904..8027bfe7 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -263,7 +263,20 @@ public class AppointmentController { response.getEmployeeName() != null ? response.getEmployeeName() : "", response.getAppointmentDate() != null ? response.getAppointmentDate().toString() : "", response.getAppointmentTime() != null ? response.getAppointmentTime().toString() : "", - response.getAppointmentStatus() != null ? response.getAppointmentStatus() : "" + normalizeAppointmentStatus(response.getAppointmentStatus()) ); } + + private String normalizeAppointmentStatus(String status) { + if (status == null) { + return "Booked"; + } + return switch (status.trim().toLowerCase()) { + case "booked" -> "Booked"; + case "completed" -> "Completed"; + case "missed" -> "Missed"; + case "cancelled", "canceled" -> "Cancelled"; + default -> "Booked"; + }; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index 7e6573de..9bff8ac6 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -11,6 +11,7 @@ import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; +import javafx.scene.control.TableCell; import javafx.scene.control.Label; import javafx.scene.control.SelectionMode; import javafx.scene.control.Spinner; @@ -156,6 +157,8 @@ public class SaleController { colCartQty.setCellValueFactory(new PropertyValueFactory<>("quantity")); colCartUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); colCartTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + colCartUnitPrice.setCellFactory(column -> currencyCell()); + colCartTotal.setCellFactory(column -> currencyCell()); tvCart.setItems(cartItems); tvCart.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); @@ -169,6 +172,8 @@ public class SaleController { colSaleUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); colSaleTotal.setCellValueFactory(new PropertyValueFactory<>("total")); colSalePaymentType.setCellValueFactory(new PropertyValueFactory<>("paymentMethod")); + colSaleUnitPrice.setCellFactory(column -> currencyCell()); + colSaleTotal.setCellFactory(column -> currencyCell()); filteredSales = new FilteredList<>(saleItems, s -> true); TableViewSupport.bindSortedItems(tvSales, filteredSales); @@ -539,6 +544,20 @@ public class SaleController { lblCartTotal.setText(currency.format(total)); } + private TableCell currencyCell() { + return new TableCell<>() { + @Override + protected void updateItem(Double value, boolean empty) { + super.updateItem(value, empty); + if (empty || value == null) { + setText(null); + } else { + setText(currency.format(value)); + } + } + }; + } + private void setCreateSaleControlsDisabled(boolean disabled) { cbProduct.setDisable(disabled); spQuantity.setDisable(disabled); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java index 495813ac..1629187a 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -49,7 +49,7 @@ public class AppointmentDialogController { private ObservableList statusList = FXCollections.observableArrayList( - "Booked", "Completed", "Cancelled", "Missed" + "Booked", "Completed", "Missed", "Cancelled" ); public void setMode(String mode) { @@ -182,7 +182,7 @@ public class AppointmentDialogController { "Parsing appointment date"); } - cbAppointmentStatus.setValue(appt.getAppointmentStatus()); + cbAppointmentStatus.setValue(normalizeAppointmentStatus(appt.getAppointmentStatus())); try { LocalTime time = LocalTime.parse(appt.getAppointmentTime()); @@ -230,7 +230,7 @@ public class AppointmentDialogController { request.setEmployeeId(cbEmployee.getValue().getId()); request.setAppointmentDate(dpAppointmentDate.getValue()); request.setAppointmentTime(appointmentTime); - request.setAppointmentStatus(cbAppointmentStatus.getValue()); + request.setAppointmentStatus(normalizeAppointmentStatus(cbAppointmentStatus.getValue())); new Thread(() -> { try { @@ -451,4 +451,17 @@ public class AppointmentDialogController { } }).start(); } + + private String normalizeAppointmentStatus(String status) { + if (status == null) { + return "Booked"; + } + return switch (status.trim().toLowerCase()) { + case "booked" -> "Booked"; + case "completed" -> "Completed"; + case "missed" -> "Missed"; + case "cancelled", "canceled" -> "Cancelled"; + default -> "Booked"; + }; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java b/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java index bd18548e..80d19dde 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java +++ b/desktop/src/main/java/org/example/petshopdesktop/util/TableViewSupport.java @@ -32,11 +32,9 @@ public final class TableViewSupport { } label.setText(message); label.setVisible(true); - label.setManaged(true); PauseTransition delay = new PauseTransition(Duration.seconds(1.5)); delay.setOnFinished(event -> { label.setVisible(false); - label.setManaged(false); }); delay.playFromStart(); } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml index 0c9c5e4a..0a7f00bd 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml @@ -73,7 +73,7 @@ -