diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java index 729d1749..40b7fe41 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java @@ -10,16 +10,16 @@ import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.databinding.ItemInventoryBinding; import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.utils.BulkDeleteHandler; +import com.example.petstoremobile.utils.SelectionHelper; -import java.util.ArrayList; import java.util.List; -public class InventoryAdapter extends RecyclerView.Adapter { +public class InventoryAdapter extends RecyclerView.Adapter implements BulkDeleteHandler.SelectableAdapter { private final List inventoryList; private final OnInventoryClickListener clickListener; - private final List selectedIds = new ArrayList<>(); - private boolean selectionMode = false; + private final SelectionHelper selectionHelper; public interface OnInventoryClickListener { void onInventoryClick(int position); @@ -30,6 +30,27 @@ public class InventoryAdapter extends RecyclerView.Adapter inventoryList, OnInventoryClickListener clickListener) { this.inventoryList = inventoryList; this.clickListener = clickListener; + this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() { + @Override + public void onSelectionChanged(int count) { + clickListener.onSelectionChanged(count); + } + + @Override + public void onSelectionModeToggle(boolean selectionMode) { + notifyDataSetChanged(); + } + }); + } + + @Override + public List getSelectedIds() { + return selectionHelper.getSelectedIds(); + } + + @Override + public void clearSelection() { + selectionHelper.clearSelection(); } public static class InventoryViewHolder extends RecyclerView.ViewHolder { @@ -71,66 +92,33 @@ public class InventoryAdapter extends RecyclerView.Adapter { - if (selectionMode) { - toggleSelection(inv.getInventoryId(), binding.cbSelectInventory); + if (selectionHelper.isInSelectionMode()) { + selectionHelper.toggleSelection(inv.getInventoryId()); + notifyItemChanged(position); } else { clickListener.onInventoryClick(holder.getAdapterPosition()); } }); holder.itemView.setOnLongClickListener(v -> { - if (!selectionMode) { - selectionMode = true; - toggleSelection(inv.getInventoryId(), binding.cbSelectInventory); - notifyDataSetChanged(); + if (!selectionHelper.isInSelectionMode()) { + selectionHelper.startSelection(inv.getInventoryId()); } return true; }); } - private void toggleSelection(Long id, android.widget.CheckBox checkBox) { - if (id == null) - return; - if (selectedIds.contains(id)) { - selectedIds.remove(id); - checkBox.setChecked(false); - } else { - selectedIds.add(id); - checkBox.setChecked(true); - } - clickListener.onSelectionChanged(selectedIds.size()); - if (selectedIds.isEmpty()) { - selectionMode = false; - notifyDataSetChanged(); - } - } - - public List getSelectedIds() { - return new ArrayList<>(selectedIds); - } - - public void clearSelection() { - selectedIds.clear(); - selectionMode = false; - notifyDataSetChanged(); - } - - public boolean isInSelectionMode() { - return selectionMode; - } - @Override public int getItemCount() { return inventoryList.size(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java index 11a90b16..87dd95b5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java @@ -2,6 +2,7 @@ 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.recyclerview.widget.RecyclerView; @@ -10,25 +11,41 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.databinding.ItemPetBinding; import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.GlideUtils; +import com.example.petstoremobile.utils.SelectionHelper; + import java.util.List; -public class PetAdapter extends RecyclerView.Adapter { +public class PetAdapter extends RecyclerView.Adapter implements BulkDeleteHandler.SelectableAdapter { private List petList; private OnPetClickListener petClickListener; private String baseUrl; private String token; + private final SelectionHelper selectionHelper; // Interface for pet click on recycler view public interface OnPetClickListener { void onPetClick(int position); + void onSelectionChanged(int selectedCount); } //Constructor public PetAdapter(List petList, OnPetClickListener petClickListener) { this.petList = petList; this.petClickListener = petClickListener; + this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() { + @Override + public void onSelectionChanged(int count) { + petClickListener.onSelectionChanged(count); + } + + @Override + public void onSelectionModeToggle(boolean selectionMode) { + notifyDataSetChanged(); + } + }); } public void setBaseUrl(String baseUrl) { @@ -39,6 +56,16 @@ public class PetAdapter extends RecyclerView.Adapter { this.token = token; } + @Override + public List getSelectedIds() { + return selectionHelper.getSelectedIds(); + } + + @Override + public void clearSelection() { + selectionHelper.clearSelection(); + } + // Get the controls of each row in recycler view public static class PetViewHolder extends RecyclerView.ViewHolder { private final ItemPetBinding binding; @@ -91,8 +118,31 @@ public class PetAdapter extends RecyclerView.Adapter { binding.ivPetProfile.setImageResource(R.drawable.placeholder); } + // Bulk delete selection mode + if (selectionHelper.isInSelectionMode()) { + binding.cbSelectPet.setVisibility(View.VISIBLE); + binding.cbSelectPet.setChecked(selectionHelper.isSelected(pet.getPetId())); + } else { + binding.cbSelectPet.setVisibility(View.GONE); + binding.cbSelectPet.setChecked(false); + } + //when a row is clicked, open the detail view - holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position)); + holder.itemView.setOnClickListener(v -> { + if (selectionHelper.isInSelectionMode()) { + selectionHelper.toggleSelection(pet.getPetId()); + notifyItemChanged(position); + } else { + petClickListener.onPetClick(position); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!selectionHelper.isInSelectionMode()) { + selectionHelper.startSelection(pet.getPetId()); + } + return true; + }); } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java index 7db2c1e3..acae1b51 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -1,5 +1,6 @@ package com.example.petstoremobile.api; +import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; @@ -8,6 +9,7 @@ import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.DELETE; import retrofit2.http.GET; +import retrofit2.http.HTTP; import retrofit2.http.Multipart; import retrofit2.http.POST; import retrofit2.http.PUT; @@ -48,6 +50,10 @@ public interface PetApi { @DELETE("api/v1/pets/{id}") Call deletePet(@Path("id") Long id); + // Bulk delete pets + @HTTP(method = "DELETE", path = "api/v1/pets", hasBody = true) + Call bulkDeletePets(@Body BulkDeleteRequest request); + // Upload pet image @Multipart @POST("api/v1/pets/{id}/image") 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 1fe9d188..0c0e07de 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 @@ -24,6 +24,7 @@ import com.example.petstoremobile.databinding.FragmentInventoryBinding; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.viewmodels.InventoryViewModel; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; @@ -44,6 +45,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn private List storeList = new ArrayList<>(); private InventoryAdapter adapter; private InventoryViewModel viewModel; + private BulkDeleteHandler bulkDeleteHandler; // Pagination private int currentPage = 0; @@ -72,6 +74,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn setupStoreFilter(); setupSwipeRefresh(); setupFilterToggle(); + setupBulkDelete(); loadInventory(true); loadStoreData(); @@ -87,11 +90,25 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } }); - binding.btnBulkDelete.setOnClickListener(v -> confirmBulkDelete()); - return binding.getRoot(); } + private void setupBulkDelete() { + bulkDeleteHandler = new BulkDeleteHandler( + this, + binding.layoutBulkDelete, + binding.tvSelectionCount, + binding.btnBulkDelete, + new BulkDeleteHandler.SelectableAdapter() { + @Override public List getSelectedIds() { return adapter.getSelectedIds(); } + @Override public void clearSelection() { adapter.clearSelection(); } + }, + "inventory item", + viewModel::bulkDeleteInventory, + () -> loadInventory(true) + ); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -243,50 +260,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn }); } - /** - * Displays a confirmation dialog before performing a bulk deletion of selected items. - */ - private void confirmBulkDelete() { - List ids = adapter.getSelectedIds(); - if (ids.isEmpty()) - return; - - new androidx.appcompat.app.AlertDialog.Builder(requireContext()) - .setTitle("Delete " + ids.size() + " item(s)?") - .setMessage("This cannot be undone.") - .setPositiveButton("Delete", (d, w) -> bulkDelete(ids)) - .setNegativeButton("Cancel", null) - .show(); - } - - /** - * Executes the bulk deletion of inventory items through the ViewModel. - */ - private void bulkDelete(List ids) { - viewModel.bulkDeleteInventory(ids).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS) { - adapter.clearSelection(); - hideBulkDeleteBar(); - loadInventory(true); - Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); - } - } - }); - } - - /** - * Hides the bulk deletion UI bar. - */ - private void hideBulkDeleteBar() { - if (binding != null) { - binding.btnBulkDelete.setVisibility(View.GONE); - binding.tvSelectionCount.setVisibility(View.GONE); - } - } - /** * Navigates to the inventory detail screen for a specific item or to add a new one. */ @@ -322,12 +295,8 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn */ @Override public void onSelectionChanged(int selectedCount) { - if (selectedCount > 0) { - binding.btnBulkDelete.setVisibility(View.VISIBLE); - binding.tvSelectionCount.setVisibility(View.VISIBLE); - binding.tvSelectionCount.setText(selectedCount + " selected"); - } else { - hideBulkDeleteBar(); + if (bulkDeleteHandler != null) { + bulkDeleteHandler.onSelectionChanged(selectedCount); } } } 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 f6b46b0e..d0fdfa3e 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 @@ -25,6 +25,7 @@ import com.example.petstoremobile.databinding.FragmentPetBinding; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.PetViewModel; @@ -46,6 +47,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private PetAdapter adapter; private PetViewModel viewModel; private StoreViewModel storeViewModel; + private BulkDeleteHandler bulkDeleteHandler; @Inject @Named("baseUrl") String baseUrl; @@ -74,6 +76,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen setupStoreFilter(); setupSwipeRefresh(); setupFilterToggle(); + setupBulkDelete(); binding.fabAddPet.setOnClickListener(v -> openPetDetails()); @@ -90,6 +93,22 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen return binding.getRoot(); } + private void setupBulkDelete() { + bulkDeleteHandler = new BulkDeleteHandler( + this, + binding.layoutBulkDelete, + binding.tvSelectionCount, + binding.btnBulkDelete, + new BulkDeleteHandler.SelectableAdapter() { + @Override public List getSelectedIds() { return adapter.getSelectedIds(); } + @Override public void clearSelection() { adapter.clearSelection(); } + }, + "pet", + viewModel::bulkDeletePets, + this::loadPetData + ); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -231,6 +250,13 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen openPetProfile(position); } + @Override + public void onSelectionChanged(int selectedCount) { + if (bulkDeleteHandler != null) { + bulkDeleteHandler.onSelectionChanged(selectedCount); + } + } + /** * Fetches pet data from the server with all active filters. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index 88ac2295..019b5884 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -3,6 +3,7 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.PetApi; +import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.Resource; @@ -57,6 +58,13 @@ public class PetRepository extends BaseRepository { return executeCall(petApi.deletePet(id)); } + /** + * Sends a request to the API to delete multiple pet records. + */ + public LiveData> bulkDeletePets(BulkDeleteRequest request) { + return executeCall(petApi.bulkDeletePets(request)); + } + /** * Uploads an image file for a specific pet via the API. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/BulkDeleteHandler.java b/android/app/src/main/java/com/example/petstoremobile/utils/BulkDeleteHandler.java new file mode 100644 index 00000000..1e777f5d --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/BulkDeleteHandler.java @@ -0,0 +1,108 @@ +package com.example.petstoremobile.utils; + +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.fragment.app.Fragment; +import androidx.lifecycle.LiveData; + +import java.util.List; + +/** + * A helper class to handle the UI and logic for bulk deletion across different fragments. + */ +public class BulkDeleteHandler { + + /** + * Interface that adapters must implement to support bulk selection. + */ + public interface SelectableAdapter { + List getSelectedIds(); + void clearSelection(); + } + + /** + * Functional interface for the API call execution. + */ + public interface BulkDeleteOperation { + LiveData> execute(List ids); + } + + private final Fragment fragment; + private final View layoutBar; + private final TextView tvCount; + private final SelectableAdapter adapter; + private final BulkDeleteOperation operation; + private final Runnable onSuccess; + private final String itemName; + + public BulkDeleteHandler(Fragment fragment, + View layoutBar, + TextView tvCount, + Button btnDelete, + SelectableAdapter adapter, + String itemName, + BulkDeleteOperation operation, + Runnable onSuccess) { + this.fragment = fragment; + this.layoutBar = layoutBar; + this.tvCount = tvCount; + this.adapter = adapter; + this.operation = operation; + this.onSuccess = onSuccess; + this.itemName = itemName; + + btnDelete.setOnClickListener(v -> confirmDelete()); + } + + /** + * Updates the UI when the selection count changes. + */ + public void onSelectionChanged(int selectedCount) { + if (selectedCount > 0) { + layoutBar.setVisibility(View.VISIBLE); + tvCount.setText(selectedCount + " selected"); + } else { + hideBar(); + } + } + + /** + * Hides the bulk delete bar and resets state. + */ + public void hideBar() { + if (layoutBar != null) { + layoutBar.setVisibility(View.GONE); + } + } + + /** + * Shows the confirmation dialog. + */ + private void confirmDelete() { + List ids = adapter.getSelectedIds(); + if (ids.isEmpty()) return; + + DialogUtils.showBulkDeleteConfirmDialog(fragment.requireContext(), ids.size(), () -> performDelete(ids)); + } + + /** + * Executes the deletion via the provided operation. + */ + private void performDelete(List ids) { + operation.execute(ids).observe(fragment.getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { + adapter.clearSelection(); + hideBar(); + onSuccess.run(); + Toast.makeText(fragment.getContext(), ids.size() + " " + itemName + "(s) deleted", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(fragment.getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/DialogUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/DialogUtils.java index 55436846..bf304d1c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/DialogUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/DialogUtils.java @@ -34,6 +34,18 @@ public class DialogUtils { showConfirmDialog(context, "Delete " + itemName + "?", "Are you sure you want to delete this " + itemName.toLowerCase() + "? This action cannot be undone.", callback); } + /** + * Shows a confirmation dialog with specific "Delete" and "Cancel" buttons. + */ + public static void showBulkDeleteConfirmDialog(Context context, int count, DialogCallback callback) { + new AlertDialog.Builder(context) + .setTitle("Delete " + count + " item(s)?") + .setMessage("This cannot be undone.") + .setPositiveButton("Delete", (dialog, which) -> callback.onConfirm()) + .setNegativeButton("Cancel", null) + .show(); + } + /** * Shows a simple information or error dialog with an "OK" button. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/SelectionHelper.java b/android/app/src/main/java/com/example/petstoremobile/utils/SelectionHelper.java new file mode 100644 index 00000000..cbda2267 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SelectionHelper.java @@ -0,0 +1,66 @@ +package com.example.petstoremobile.utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to manage selection state in Adapters for bulk operations. + */ +public class SelectionHelper { + + private final List selectedIds = new ArrayList<>(); + private boolean selectionMode = false; + private final SelectionListener listener; + + public interface SelectionListener { + void onSelectionChanged(int count); + void onSelectionModeToggle(boolean selectionMode); + } + + public SelectionHelper(SelectionListener listener) { + this.listener = listener; + } + + public void toggleSelection(Long id) { + if (id == null) return; + + if (selectedIds.contains(id)) { + selectedIds.remove(id); + } else { + selectedIds.add(id); + } + + listener.onSelectionChanged(selectedIds.size()); + + if (selectedIds.isEmpty() && selectionMode) { + selectionMode = false; + listener.onSelectionModeToggle(false); + } + } + + public void startSelection(Long id) { + selectionMode = true; + selectedIds.add(id); + listener.onSelectionChanged(selectedIds.size()); + listener.onSelectionModeToggle(true); + } + + public boolean isSelected(Long id) { + return selectedIds.contains(id); + } + + public boolean isInSelectionMode() { + return selectionMode; + } + + public List getSelectedIds() { + return new ArrayList<>(selectedIds); + } + + public void clearSelection() { + selectedIds.clear(); + selectionMode = false; + listener.onSelectionChanged(0); + listener.onSelectionModeToggle(false); + } +} 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 index b0af57c8..4866b79a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java @@ -3,11 +3,14 @@ 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.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; @@ -57,6 +60,13 @@ public class PetViewModel extends ViewModel { 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. */ diff --git a/android/app/src/main/res/layout/fragment_pet.xml b/android/app/src/main/res/layout/fragment_pet.xml index 9273c0f0..fcd9ccba 100644 --- a/android/app/src/main/res/layout/fragment_pet.xml +++ b/android/app/src/main/res/layout/fragment_pet.xml @@ -132,6 +132,37 @@ + + + + +