diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java index 6dd8eeb4..cb3eeaf2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java @@ -1,79 +1,128 @@ package com.example.petstoremobile.adapters; import android.graphics.Color; -import android.view.*; -import android.widget.TextView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemAdoptionBinding; import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.utils.BulkDeleteHandler; +import com.example.petstoremobile.utils.SelectionHelper; import java.util.List; -public class AdoptionAdapter extends RecyclerView.Adapter { +public class AdoptionAdapter extends RecyclerView.Adapter implements BulkDeleteHandler.SelectableAdapter { private List adoptionList; private OnAdoptionClickListener listener; + private final SelectionHelper selectionHelper; public interface OnAdoptionClickListener { void onAdoptionClick(int position); + void onSelectionChanged(int count); } public AdoptionAdapter(List adoptionList, OnAdoptionClickListener listener) { this.adoptionList = adoptionList; this.listener = listener; + this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() { + @Override + public void onSelectionChanged(int count) { + listener.onSelectionChanged(count); + } + + @Override + public void onSelectionModeToggle(boolean selectionMode) { + notifyDataSetChanged(); + } + }); + } + + @Override + public List getSelectedKeys() { + return selectionHelper.getSelectedKeys(); + } + + @Override + public void clearSelection() { + selectionHelper.clearSelection(); } public static class AdoptionViewHolder extends RecyclerView.ViewHolder { - TextView tvCustomerName, tvPetName, tvDate, tvFee, tvStatus; + final ItemAdoptionBinding binding; - public AdoptionViewHolder(@NonNull View v) { - super(v); - tvCustomerName = v.findViewById(R.id.tvAdoptionCustomerName); - tvPetName = v.findViewById(R.id.tvAdoptionPetName); - tvDate = v.findViewById(R.id.tvAdoptionDate); - tvFee = v.findViewById(R.id.tvAdoptionFee); - tvStatus = v.findViewById(R.id.tvAdoptionStatus); + public AdoptionViewHolder(@NonNull ItemAdoptionBinding binding) { + super(binding.getRoot()); + this.binding = binding; } } @NonNull @Override public AdoptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_adoption, parent, false); - return new AdoptionViewHolder(v); + ItemAdoptionBinding binding = ItemAdoptionBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new AdoptionViewHolder(binding); } @Override public void onBindViewHolder(@NonNull AdoptionViewHolder holder, int position) { AdoptionDTO a = adoptionList.get(position); + ItemAdoptionBinding binding = holder.binding; + + binding.tvAdoptionCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : ""); + binding.tvAdoptionPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : "")); + binding.tvAdoptionStaffName.setText("Staff: " + (a.getEmployeeName() != null ? a.getEmployeeName() : "N/A")); + binding.tvAdoptionDate.setText("Date: " + (a.getAdoptionDate() != null ? a.getAdoptionDate() : "")); + binding.tvAdoptionFee.setText(a.getAdoptionFee() != null ? "$" + a.getAdoptionFee() : ""); - holder.tvCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : ""); - holder.tvPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : "")); - holder.tvDate.setText("Date: " + (a.getAdoptionDate() != null ? a.getAdoptionDate() : "")); - holder.tvFee.setText(a.getAdoptionFee() != null ? "$" + a.getAdoptionFee() : ""); String status = a.getAdoptionStatus() != null ? a.getAdoptionStatus() : ""; - holder.tvStatus.setText(status); + binding.tvAdoptionStatus.setText(status); switch (status) { case "Completed": - holder.tvStatus.setBackgroundColor(Color.parseColor("#4CAF50")); + binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#4CAF50")); break; case "Pending": - holder.tvStatus.setBackgroundColor(Color.parseColor("#FF9800")); + binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#FF9800")); break; - case "Rejected": - holder.tvStatus.setBackgroundColor(Color.parseColor("#F44336")); + case "Cancelled": + binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#F44336")); break; default: - holder.tvStatus.setBackgroundColor(Color.parseColor("#9E9E9E")); + binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#9E9E9E")); break; } - holder.itemView.setOnClickListener(v -> listener.onAdoptionClick(position)); + String key = String.valueOf(a.getAdoptionId()); + + // Bulk delete selection mode + if (selectionHelper.isInSelectionMode()) { + binding.cbSelectAdoption.setVisibility(View.VISIBLE); + binding.cbSelectAdoption.setChecked(selectionHelper.isSelected(key)); + } else { + binding.cbSelectAdoption.setVisibility(View.GONE); + binding.cbSelectAdoption.setChecked(false); + } + + holder.itemView.setOnClickListener(v -> { + if (selectionHelper.isInSelectionMode()) { + selectionHelper.toggleSelection(key); + notifyItemChanged(position); + } else { + listener.onAdoptionClick(position); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!selectionHelper.isInSelectionMode()) { + selectionHelper.startSelection(key); + } + return true; + }); } @Override public int getItemCount() { return adoptionList.size(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java index fb260541..cb292c1a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java @@ -4,77 +4,124 @@ import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemAppointmentBinding; import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.utils.BulkDeleteHandler; +import com.example.petstoremobile.utils.SelectionHelper; import java.util.List; -public class AppointmentAdapter extends RecyclerView.Adapter { +public class AppointmentAdapter extends RecyclerView.Adapter implements BulkDeleteHandler.SelectableAdapter { private List appointmentList; private OnAppointmentClickListener appointmentClickListener; + private final SelectionHelper selectionHelper; public interface OnAppointmentClickListener { void onAppointmentClick(int position); + void onSelectionChanged(int count); } public AppointmentAdapter(List appointmentList, OnAppointmentClickListener appointmentClickListener) { this.appointmentList = appointmentList; this.appointmentClickListener = appointmentClickListener; + this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() { + @Override + public void onSelectionChanged(int count) { + appointmentClickListener.onSelectionChanged(count); + } + + @Override + public void onSelectionModeToggle(boolean selectionMode) { + notifyDataSetChanged(); + } + }); + } + + @Override + public List getSelectedKeys() { + return selectionHelper.getSelectedKeys(); + } + + @Override + public void clearSelection() { + selectionHelper.clearSelection(); } public static class AppointmentViewHolder extends RecyclerView.ViewHolder { - TextView tvCustomerName, tvPetName, tvServiceType, tvDateTime, tvAppointmentStatus; + private final ItemAppointmentBinding binding; - public AppointmentViewHolder(@NonNull View v) { - super(v); - tvCustomerName = v.findViewById(R.id.tvCustomerName); - tvPetName = v.findViewById(R.id.tvApptPetName); - tvServiceType = v.findViewById(R.id.tvServiceType); - tvDateTime = v.findViewById(R.id.tvDateTime); - tvAppointmentStatus = v.findViewById(R.id.tvAppointmentStatus); + public AppointmentViewHolder(@NonNull ItemAppointmentBinding binding) { + super(binding.getRoot()); + this.binding = binding; } } @NonNull @Override public AppointmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_appointment, parent, false); - return new AppointmentViewHolder(v); + ItemAppointmentBinding binding = ItemAppointmentBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new AppointmentViewHolder(binding); } @Override public void onBindViewHolder(@NonNull AppointmentViewHolder holder, int position) { AppointmentDTO a = appointmentList.get(position); + ItemAppointmentBinding binding = holder.binding; - holder.tvCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : ""); - holder.tvPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : "")); - holder.tvServiceType.setText(a.getServiceType() != null ? a.getServiceType() : ""); - holder.tvDateTime.setText((a.getAppointmentDate() != null ? a.getAppointmentDate() : "") + + binding.tvCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : ""); + binding.tvApptPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : "")); + binding.tvServiceType.setText(a.getServiceType() != null ? a.getServiceType() : ""); + binding.tvStaffName.setText("Staff: " + (a.getEmployeeName() != null ? a.getEmployeeName() : "Unassigned")); + binding.tvDateTime.setText((a.getAppointmentDate() != null ? a.getAppointmentDate() : "") + " at " + (a.getAppointmentTime() != null ? a.getAppointmentTime() : "")); String status = a.getStatus() != null ? a.getStatus() : ""; - holder.tvAppointmentStatus.setText(status); + binding.tvAppointmentStatus.setText(status); switch (status.toUpperCase()) { case "BOOKED": - holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#2196F3")); // blue + binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#2196F3")); // blue break; case "COMPLETED": - holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50")); // green + binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50")); // green break; case "CANCELLED": - holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336")); // red + binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336")); // red break; default: - holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#9E9E9E")); // gray + binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#9E9E9E")); // gray break; } - holder.itemView.setOnClickListener(v -> appointmentClickListener.onAppointmentClick(position)); + String key = String.valueOf(a.getAppointmentId()); + + // Bulk delete selection mode + if (selectionHelper.isInSelectionMode()) { + binding.cbSelectAppointment.setVisibility(View.VISIBLE); + binding.cbSelectAppointment.setChecked(selectionHelper.isSelected(key)); + } else { + binding.cbSelectAppointment.setVisibility(View.GONE); + binding.cbSelectAppointment.setChecked(false); + } + + holder.itemView.setOnClickListener(v -> { + if (selectionHelper.isInSelectionMode()) { + selectionHelper.toggleSelection(key); + notifyItemChanged(position); + } else { + appointmentClickListener.onAppointmentClick(position); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!selectionHelper.isInSelectionMode()) { + selectionHelper.startSelection(key); + } + return true; + }); } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java index 93e6d902..972ac56d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java @@ -1,14 +1,12 @@ package com.example.petstoremobile.adapters; import android.view.LayoutInflater; -import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemChatBinding; import com.example.petstoremobile.models.Chat; import java.util.List; @@ -30,15 +28,15 @@ public class ChatAdapter extends RecyclerView.Adapter listener.onChatClick(chat)); } @@ -48,12 +46,11 @@ public class ChatAdapter extends RecyclerView.Adapter { + + private List list; + private OnEmployeeClickListener listener; + + public interface OnEmployeeClickListener { + void onEmployeeClick(int position); + } + + public EmployeeAdapter(List list, OnEmployeeClickListener listener) { + this.list = list; + this.listener = listener; + } + + public static class EmployeeViewHolder extends RecyclerView.ViewHolder { + TextView tvFullName, tvUsername, tvEmail, tvPhone, tvRole, tvStatus; + + public EmployeeViewHolder(@NonNull View v) { + super(v); + tvFullName = v.findViewById(R.id.tvEmployeeFullName); + tvUsername = v.findViewById(R.id.tvEmployeeUsername); + tvEmail = v.findViewById(R.id.tvEmployeeEmail); + tvPhone = v.findViewById(R.id.tvEmployeePhone); + tvRole = v.findViewById(R.id.tvEmployeeRole); + tvStatus = v.findViewById(R.id.tvEmployeeStatus); + } + } + + @NonNull + @Override + public EmployeeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_employee, parent, false); + return new EmployeeViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull EmployeeViewHolder holder, int position) { + EmployeeDTO e = list.get(position); + + holder.tvFullName.setText(e.getFullName() != null ? e.getFullName() : ""); + holder.tvUsername.setText("@" + (e.getUsername() != null ? e.getUsername() : "")); + holder.tvEmail.setText(e.getEmail() != null ? e.getEmail() : ""); + holder.tvPhone.setText(e.getPhone() != null ? e.getPhone() : ""); + + // Role badge + String role = e.getRole() != null ? e.getRole() : ""; + holder.tvRole.setText(role); + holder.tvRole.setBackgroundColor( + "ADMIN".equals(role) ? Color.parseColor("#1a759f") : Color.parseColor("#577590")); + + // Status badge + boolean active = Boolean.TRUE.equals(e.getActive()); + holder.tvStatus.setText(active ? "Active" : "Inactive"); + holder.tvStatus.setBackgroundColor( + active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336")); + + holder.itemView.setOnClickListener(v -> listener.onEmployeeClick(position)); + } + + @Override + public int getItemCount() { return list.size(); } +} 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 1e455cc6..9fbc1481 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 @@ -4,24 +4,22 @@ import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; +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); @@ -32,119 +30,97 @@ 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 getSelectedKeys() { + return selectionHelper.getSelectedKeys(); + } + + @Override + public void clearSelection() { + selectionHelper.clearSelection(); } public static class InventoryViewHolder extends RecyclerView.ViewHolder { - // Matches desktop table columns: Inventory ID, Product ID, Product Name, - // Quantity - TextView tvInventoryId, tvProdId, tvProductName, tvQuantity; - CheckBox checkBox; + final ItemInventoryBinding binding; - public InventoryViewHolder(@NonNull View v) { - super(v); - tvInventoryId = v.findViewById(R.id.tvInventoryId); - tvProdId = v.findViewById(R.id.tvProdId); - tvProductName = v.findViewById(R.id.tvProductName); - tvQuantity = v.findViewById(R.id.tvQuantity); - checkBox = v.findViewById(R.id.cbSelectInventory); + public InventoryViewHolder(@NonNull ItemInventoryBinding binding) { + super(binding.getRoot()); + this.binding = binding; } } @NonNull @Override public InventoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_inventory, parent, false); - return new InventoryViewHolder(v); + ItemInventoryBinding binding = ItemInventoryBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new InventoryViewHolder(binding); } @Override public void onBindViewHolder(@NonNull InventoryViewHolder holder, int position) { InventoryDTO inv = inventoryList.get(position); - - // Column: Inventory ID - String invIdStr = inv.getInventoryId() != null ? String.valueOf(inv.getInventoryId()) : "—"; - holder.tvInventoryId.setText("Inv ID: " + invIdStr); - - // Column: Product ID - String prodIdStr = inv.getProdId() != null ? String.valueOf(inv.getProdId()) : "—"; - holder.tvProdId.setText("Prod ID: " + prodIdStr); + ItemInventoryBinding binding = holder.binding; // Column: Product Name - holder.tvProductName.setText(inv.getProductName() != null ? inv.getProductName() : "—"); + binding.tvProductName.setText(inv.getProductName() != null ? inv.getProductName() : "—"); + + // Column: Store Name + binding.tvInventoryStore.setText("Store: " + (inv.getStoreName() != null ? inv.getStoreName() : "—")); // Column: Quantity int qty = inv.getQuantity() != null ? inv.getQuantity() : 0; - holder.tvQuantity.setText(String.valueOf(qty)); + binding.tvQuantity.setText("Stock: " + qty); // Low stock = red, normal = green (like desktop reorder concept) if (qty <= 5) { - holder.tvQuantity.setTextColor(Color.parseColor("#F44336")); + binding.tvQuantity.setTextColor(Color.parseColor("#F44336")); } else { - holder.tvQuantity.setTextColor(Color.parseColor("#4CAF50")); + binding.tvQuantity.setTextColor(Color.parseColor("#4CAF50")); } + String key = String.valueOf(inv.getInventoryId()); + // Bulk delete selection mode - if (selectionMode) { - holder.checkBox.setVisibility(View.VISIBLE); - holder.checkBox.setChecked(inv.getInventoryId() != null - && selectedIds.contains(inv.getInventoryId())); + if (selectionHelper.isInSelectionMode()) { + binding.cbSelectInventory.setVisibility(View.VISIBLE); + binding.cbSelectInventory.setChecked(selectionHelper.isSelected(key)); } else { - holder.checkBox.setVisibility(View.GONE); - holder.checkBox.setChecked(false); + binding.cbSelectInventory.setVisibility(View.GONE); + binding.cbSelectInventory.setChecked(false); } holder.itemView.setOnClickListener(v -> { - if (selectionMode) { - toggleSelection(inv.getInventoryId(), holder.checkBox); + if (selectionHelper.isInSelectionMode()) { + selectionHelper.toggleSelection(key); + notifyItemChanged(position); } else { clickListener.onInventoryClick(holder.getAdapterPosition()); } }); holder.itemView.setOnLongClickListener(v -> { - if (!selectionMode) { - selectionMode = true; - toggleSelection(inv.getInventoryId(), holder.checkBox); - notifyDataSetChanged(); + if (!selectionHelper.isInSelectionMode()) { + selectionHelper.startSelection(key); } return true; }); } - private void toggleSelection(Long id, 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/MessageAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java index de6ccc04..ee58941a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java @@ -12,6 +12,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemMessageReceivedBinding; +import com.example.petstoremobile.databinding.ItemMessageSentBinding; import com.example.petstoremobile.models.Message; import java.util.List; @@ -51,11 +53,11 @@ public class MessageAdapter 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) { @@ -41,19 +56,23 @@ public class PetAdapter extends RecyclerView.Adapter { this.token = token; } + @Override + public List getSelectedKeys() { + return selectionHelper.getSelectedKeys(); + } + + @Override + public void clearSelection() { + selectionHelper.clearSelection(); + } + // Get the controls of each row in recycler view public static class PetViewHolder extends RecyclerView.ViewHolder { - TextView tvPetName, tvPetSpeciesBreed, tvPetAge, tvPetPrice, tvPetStatus; - ImageView ivPetProfile; + private final ItemPetBinding binding; - public PetViewHolder(@NonNull View v) { - super(v); - tvPetName = v.findViewById(R.id.tvPetName); - tvPetSpeciesBreed = v.findViewById(R.id.tvPetSpeciesBreed); - tvPetAge = v.findViewById(R.id.tvPetAge); - tvPetPrice = v.findViewById(R.id.tvPetPrice); - tvPetStatus = v.findViewById(R.id.tvPetStatus); - ivPetProfile = v.findViewById(R.id.ivPetProfile); + public PetViewHolder(@NonNull ItemPetBinding binding) { + super(binding.getRoot()); + this.binding = binding; } } @@ -61,49 +80,75 @@ public class PetAdapter extends RecyclerView.Adapter { @NonNull @Override public PetViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_pet, parent, false); - return new PetViewHolder(v); + ItemPetBinding binding = ItemPetBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new PetViewHolder(binding); } //populate the row with pet data @Override public void onBindViewHolder(@NonNull PetViewHolder holder, int position) { PetDTO pet = petList.get(position); + ItemPetBinding binding = holder.binding; - holder.tvPetName.setText(pet.getPetName()); - holder.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed()); - holder.tvPetAge.setText("Age: " + pet.getPetAge() + " yr(s)"); + binding.tvPetName.setText(pet.getPetName()); + binding.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed()); + binding.tvPetAge.setText("Age: " + pet.getPetAge() + " yr(s)"); Double price = pet.getPetPrice(); if (price != null) { - holder.tvPetPrice.setText("$" + String.format("%.2f", price)); + binding.tvPetPrice.setText("$" + String.format("%.2f", price)); } else { - holder.tvPetPrice.setText("$0.00"); + binding.tvPetPrice.setText("$0.00"); } - holder.tvPetStatus.setText(pet.getPetStatus()); + binding.tvPetStatus.setText(pet.getPetStatus()); //Set the status color depending on availability. If available, green, otherwise red if (pet.getPetStatus() != null && pet.getPetStatus().equals("Available")) { - holder.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50")); + binding.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50")); } else { - holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336")); + binding.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336")); } // Load pet image using Glide if (baseUrl != null) { String imageUrl = baseUrl + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId()); - GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), holder.ivPetProfile, imageUrl, token, R.drawable.placeholder); + GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), binding.ivPetProfile, imageUrl, token, R.drawable.placeholder); } else { - holder.ivPetProfile.setImageResource(R.drawable.placeholder); + binding.ivPetProfile.setImageResource(R.drawable.placeholder); + } + + String key = String.valueOf(pet.getPetId()); + + // Bulk delete selection mode + if (selectionHelper.isInSelectionMode()) { + binding.cbSelectPet.setVisibility(View.VISIBLE); + binding.cbSelectPet.setChecked(selectionHelper.isSelected(key)); + } 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(key); + notifyItemChanged(position); + } else { + petClickListener.onPetClick(position); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!selectionHelper.isInSelectionMode()) { + selectionHelper.startSelection(key); + } + return true; + }); } @Override public int getItemCount() { return petList.size(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java index ad1cf678..f5f897cc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java @@ -1,13 +1,12 @@ package com.example.petstoremobile.adapters; 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.api.ProductApi; +import com.example.petstoremobile.databinding.ItemProductBinding; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.GlideUtils; import java.util.List; @@ -37,41 +36,37 @@ public class ProductAdapter extends RecyclerView.Adapter listener.onProductClick(position)); diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java index 4c6377e1..231af741 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java @@ -1,55 +1,109 @@ package com.example.petstoremobile.adapters; -import android.view.*; -import android.widget.TextView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; + +import com.example.petstoremobile.databinding.ItemProductSupplierBinding; import com.example.petstoremobile.dtos.ProductSupplierDTO; +import com.example.petstoremobile.utils.BulkDeleteHandler; +import com.example.petstoremobile.utils.SelectionHelper; + import java.util.List; -public class ProductSupplierAdapter extends RecyclerView.Adapter { +public class ProductSupplierAdapter extends RecyclerView.Adapter implements BulkDeleteHandler.SelectableAdapter { - private List list; - private OnProductSupplierClickListener listener; + private final List list; + private final OnProductSupplierClickListener listener; + private final SelectionHelper selectionHelper; public interface OnProductSupplierClickListener { void onProductSupplierClick(int position); + void onSelectionChanged(int count); } public ProductSupplierAdapter(List list, OnProductSupplierClickListener listener) { this.list = list; this.listener = listener; + this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() { + @Override + public void onSelectionChanged(int count) { + listener.onSelectionChanged(count); + } + + @Override + public void onSelectionModeToggle(boolean selectionMode) { + notifyDataSetChanged(); + } + }); + } + + @Override + public List getSelectedKeys() { + return selectionHelper.getSelectedKeys(); + } + + @Override + public void clearSelection() { + selectionHelper.clearSelection(); } public static class PSViewHolder extends RecyclerView.ViewHolder { - TextView tvProductName, tvSupplierName, tvCost; + final ItemProductSupplierBinding binding; - public PSViewHolder(@NonNull View v) { - super(v); - tvProductName = v.findViewById(R.id.tvPSProductName); - tvSupplierName = v.findViewById(R.id.tvPSSupplierName); - tvCost = v.findViewById(R.id.tvPSCost); + public PSViewHolder(@NonNull ItemProductSupplierBinding binding) { + super(binding.getRoot()); + this.binding = binding; } } @NonNull @Override public PSViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_product_supplier, parent, false); - return new PSViewHolder(v); + ItemProductSupplierBinding binding = ItemProductSupplierBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new PSViewHolder(binding); } @Override public void onBindViewHolder(@NonNull PSViewHolder holder, int position) { ProductSupplierDTO ps = list.get(position); - holder.tvProductName.setText(ps.getProductName() != null ? ps.getProductName() : ""); - holder.tvSupplierName.setText("Supplier: " + (ps.getSupplierName() != null ? ps.getSupplierName() : "")); - holder.tvCost.setText(ps.getCost() != null ? "Cost: $" + ps.getCost() : ""); - holder.itemView.setOnClickListener(v -> listener.onProductSupplierClick(position)); + ItemProductSupplierBinding binding = holder.binding; + + binding.tvPSProductName.setText(ps.getProductName() != null ? ps.getProductName() : ""); + binding.tvPSSupplierName.setText("Supplier: " + (ps.getSupplierName() != null ? ps.getSupplierName() : "")); + binding.tvPSCost.setText(ps.getCost() != null ? "Cost: $" + ps.getCost() : ""); + + String key = ps.getProductId() + "-" + ps.getSupplierId(); + + // Bulk delete selection mode + if (selectionHelper.isInSelectionMode()) { + binding.cbSelectProductSupplier.setVisibility(View.VISIBLE); + binding.cbSelectProductSupplier.setChecked(selectionHelper.isSelected(key)); + } else { + binding.cbSelectProductSupplier.setVisibility(View.GONE); + binding.cbSelectProductSupplier.setChecked(false); + } + + holder.itemView.setOnClickListener(v -> { + if (selectionHelper.isInSelectionMode()) { + selectionHelper.toggleSelection(key); + notifyItemChanged(position); + } else { + listener.onProductSupplierClick(position); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!selectionHelper.isInSelectionMode()) { + selectionHelper.startSelection(key); + } + return true; + }); } @Override public int getItemCount() { return list.size(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java index 2d66e672..a31a3d0a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java @@ -1,11 +1,11 @@ package com.example.petstoremobile.adapters; import android.graphics.Color; -import android.view.*; -import android.widget.TextView; +import android.view.LayoutInflater; +import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemPurchaseOrderBinding; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import java.util.List; @@ -24,47 +24,49 @@ public class PurchaseOrderAdapter extends RecyclerView.Adapter { - private List saleList; - private OnSaleClickListener saleClickListener; + private List saleList; + private OnSaleClickListener listener; public interface OnSaleClickListener { void onSaleClick(int position); } - public SaleAdapter(List saleList, OnSaleClickListener saleClickListener) { + public SaleAdapter(List saleList, OnSaleClickListener listener) { this.saleList = saleList; - this.saleClickListener = saleClickListener; + this.listener = listener; } public static class SaleViewHolder extends RecyclerView.ViewHolder { - TextView tvSaleId, tvItemName, tvEmployeeName, tvSaleDate, tvTotal, tvPaymentMethod, tvRefundBadge; + final ItemSaleBinding binding; - public SaleViewHolder(@NonNull View v) { - super(v); - tvSaleId = v.findViewById(R.id.tvSaleId); - tvItemName = v.findViewById(R.id.tvSaleItemName); - tvEmployeeName = v.findViewById(R.id.tvSaleEmployee); - tvSaleDate = v.findViewById(R.id.tvSaleDate); - tvTotal = v.findViewById(R.id.tvSaleTotal); - tvPaymentMethod = v.findViewById(R.id.tvSalePayment); - tvRefundBadge = v.findViewById(R.id.tvRefundBadge); + public SaleViewHolder(@NonNull ItemSaleBinding binding) { + super(binding.getRoot()); + this.binding = binding; } } @NonNull @Override public SaleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_sale, parent, false); - return new SaleViewHolder(v); + ItemSaleBinding binding = ItemSaleBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new SaleViewHolder(binding); } @Override public void onBindViewHolder(@NonNull SaleViewHolder holder, int position) { - Sale sale = saleList.get(position); + SaleDTO s = saleList.get(position); + ItemSaleBinding binding = holder.binding; - holder.tvSaleId.setText("ID: " + sale.getSaleId()); - holder.tvItemName.setText(sale.getItemName()); - holder.tvEmployeeName.setText("By: " + sale.getEmployeeName()); - holder.tvSaleDate.setText(sale.getSaleDate()); - holder.tvTotal.setText("$" + String.format("%.2f", sale.getTotal())); - holder.tvPaymentMethod.setText(sale.getPaymentMethod()); + binding.tvSaleId.setText("Sale #" + (s.getSaleId() != null ? s.getSaleId() : "")); + binding.tvSaleEmployee.setText("By: " + (s.getEmployeeName() != null ? s.getEmployeeName() : "")); + binding.tvSaleDate.setText(s.getSaleDate() != null ? s.getSaleDate().substring(0, Math.min(10, s.getSaleDate().length())) : ""); + binding.tvSalePayment.setText(s.getPaymentMethod() != null ? s.getPaymentMethod() : ""); + binding.tvSaleTotal.setText(s.getTotalAmount() != null ? "$" + s.getTotalAmount() : ""); - // Show refund badge if it's a refund - if (sale.isRefund()) { - holder.tvRefundBadge.setVisibility(View.VISIBLE); - holder.tvRefundBadge.setBackgroundColor(Color.parseColor("#F44336")); - holder.tvTotal.setTextColor(Color.parseColor("#F44336")); + if (Boolean.TRUE.equals(s.getIsRefund())) { + binding.tvSaleRefundBadge.setVisibility(View.VISIBLE); + binding.tvSaleRefundBadge.setBackgroundColor(Color.parseColor("#F44336")); + binding.tvSaleTotal.setTextColor(Color.parseColor("#F44336")); } else { - holder.tvRefundBadge.setVisibility(View.GONE); - holder.tvTotal.setTextColor(Color.parseColor("#4CAF50")); + binding.tvSaleRefundBadge.setVisibility(View.GONE); + binding.tvSaleTotal.setTextColor(Color.parseColor("#4CAF50")); } - holder.itemView.setOnClickListener(v -> saleClickListener.onSaleClick(position)); + holder.itemView.setOnClickListener(v -> listener.onSaleClick(position)); } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java index e3cc6d1c..30a5b8d7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java @@ -3,39 +3,69 @@ package com.example.petstoremobile.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; + import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; + +import com.example.petstoremobile.databinding.ItemServiceBinding; import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.utils.BulkDeleteHandler; +import com.example.petstoremobile.utils.SelectionHelper; + import java.util.List; -public class ServiceAdapter extends RecyclerView.Adapter { +/** + * Adapter class for displaying a list of services in a RecyclerView. + */ +public class ServiceAdapter extends RecyclerView.Adapter implements BulkDeleteHandler.SelectableAdapter { - private List serviceList; - private OnServiceClickListener serviceClickListener; + private final List serviceList; + private final OnServiceClickListener clickListener; + private final SelectionHelper selectionHelper; - // Interface for service click on recycler view + /** + * Interface for handling clicks on service items. + */ public interface OnServiceClickListener { void onServiceClick(int position); + void onSelectionChanged(int count); } - //Constructor - public ServiceAdapter(List serviceList, OnServiceClickListener serviceClickListener) { - this.serviceList = serviceList; - this.serviceClickListener = serviceClickListener; + public ServiceAdapter(List serviceList, OnServiceClickListener clickListener) { + this.serviceList = serviceList; + 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(); + } + }); } - // Get the controls of each row in recycler view + @Override + public List getSelectedKeys() { + return selectionHelper.getSelectedKeys(); + } + + @Override + public void clearSelection() { + selectionHelper.clearSelection(); + } + + /** + * ViewHolder class for service items. + */ public static class ServiceViewHolder extends RecyclerView.ViewHolder { - TextView tvServiceName, tvServiceDesc, tvServiceDuration, tvServicePrice; + final ItemServiceBinding binding; - public ServiceViewHolder(@NonNull View v) { - super(v); - tvServiceName = v.findViewById(R.id.tvServiceName); - tvServiceDesc = v.findViewById(R.id.tvServiceDesc); - tvServiceDuration = v.findViewById(R.id.tvServiceDuration); - tvServicePrice = v.findViewById(R.id.tvServicePrice); + public ServiceViewHolder(@NonNull ItemServiceBinding binding) { + super(binding.getRoot()); + this.binding = binding; } } @@ -43,26 +73,51 @@ public class ServiceAdapter extends RecyclerView.Adapter serviceClickListener.onServiceClick(position)); + String key = String.valueOf(service.getServiceId()); + + // Bulk delete selection mode + if (selectionHelper.isInSelectionMode()) { + binding.cbSelectService.setVisibility(View.VISIBLE); + binding.cbSelectService.setChecked(selectionHelper.isSelected(key)); + } else { + binding.cbSelectService.setVisibility(View.GONE); + binding.cbSelectService.setChecked(false); + } + + holder.itemView.setOnClickListener(v -> { + if (selectionHelper.isInSelectionMode()) { + selectionHelper.toggleSelection(key); + notifyItemChanged(position); + } else { + clickListener.onServiceClick(position); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!selectionHelper.isInSelectionMode()) { + selectionHelper.startSelection(key); + } + return true; + }); } @Override public int getItemCount() { return serviceList.size(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java index e134f5b2..980dc071 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java @@ -3,39 +3,63 @@ package com.example.petstoremobile.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; + import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; + +import com.example.petstoremobile.databinding.ItemSupplierBinding; import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.utils.BulkDeleteHandler; +import com.example.petstoremobile.utils.SelectionHelper; + import java.util.List; -public class SupplierAdapter extends RecyclerView.Adapter { +public class SupplierAdapter extends RecyclerView.Adapter implements BulkDeleteHandler.SelectableAdapter { - private List supplierList; - private OnSupplierClickListener supplierClickListener; + private final List supplierList; + private final OnSupplierClickListener supplierClickListener; + private final SelectionHelper selectionHelper; // Interface for supplier click on recycler view public interface OnSupplierClickListener { void onSupplierClick(int position); + void onSelectionChanged(int count); } //Constructor public SupplierAdapter(List supplierList, OnSupplierClickListener supplierClickListener) { this.supplierList = supplierList; this.supplierClickListener = supplierClickListener; + this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() { + @Override + public void onSelectionChanged(int count) { + supplierClickListener.onSelectionChanged(count); + } + + @Override + public void onSelectionModeToggle(boolean selectionMode) { + notifyDataSetChanged(); + } + }); + } + + @Override + public List getSelectedKeys() { + return selectionHelper.getSelectedKeys(); + } + + @Override + public void clearSelection() { + selectionHelper.clearSelection(); } // Get the controls of each row in recycler view public static class SupplierViewHolder extends RecyclerView.ViewHolder { - TextView tvSupCompany, tvSupContactName, tvSupEmail, tvSupPhone; + final ItemSupplierBinding binding; - public SupplierViewHolder(@NonNull View v) { - super(v); - tvSupCompany = v.findViewById(R.id.tvSupCompany); - tvSupContactName = v.findViewById(R.id.tvSupContactName); - tvSupEmail = v.findViewById(R.id.tvSupEmail); - tvSupPhone = v.findViewById(R.id.tvSupPhone); + public SupplierViewHolder(@NonNull ItemSupplierBinding binding) { + super(binding.getRoot()); + this.binding = binding; } } @@ -43,26 +67,52 @@ public class SupplierAdapter extends RecyclerView.Adapter supplierClickListener.onSupplierClick(position)); + holder.itemView.setOnClickListener(v -> { + if (selectionHelper.isInSelectionMode()) { + selectionHelper.toggleSelection(key); + notifyItemChanged(position); + } else { + supplierClickListener.onSupplierClick(position); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!selectionHelper.isInSelectionMode()) { + selectionHelper.startSelection(key); + } + return true; + }); } @Override public int getItemCount() { return supplierList.size(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java b/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java index 2f704a41..ec397909 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java @@ -1,12 +1,14 @@ package com.example.petstoremobile.api; import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.PageResponse; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.DELETE; import retrofit2.http.GET; +import retrofit2.http.HTTP; import retrofit2.http.POST; import retrofit2.http.PUT; import retrofit2.http.Path; @@ -17,7 +19,12 @@ public interface AdoptionApi { @GET("api/v1/adoptions") Call> getAllAdoptions( @Query("page") int page, - @Query("size") int size); + @Query("size") int size, + @Query("q") String query, + @Query("status") String status, + @Query("storeId") Long storeId, + @Query("date") String date, + @Query("employeeId") Long employeeId); @GET("api/v1/adoptions/{id}") Call getAdoptionById(@Path("id") Long id); @@ -30,5 +37,7 @@ public interface AdoptionApi { @DELETE("api/v1/adoptions/{id}") Call deleteAdoption(@Path("id") Long id); -} + @HTTP(method = "DELETE", path = "api/v1/adoptions", hasBody = true) + Call bulkDeleteAdoptions(@Body BulkDeleteRequest request); +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java index d811b2e0..5b8a37a7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java @@ -1,12 +1,14 @@ package com.example.petstoremobile.api; import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.PageResponse; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.DELETE; import retrofit2.http.GET; +import retrofit2.http.HTTP; import retrofit2.http.POST; import retrofit2.http.PUT; import retrofit2.http.Path; @@ -35,4 +37,7 @@ public interface AppointmentApi { @DELETE("api/v1/appointments/{id}") Call deleteAppointment(@Path("id") Long id); + + @HTTP(method = "DELETE", path = "api/v1/appointments", hasBody = true) + Call bulkDeleteAppointments(@Body BulkDeleteRequest request); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/EmployeeApi.java b/android/app/src/main/java/com/example/petstoremobile/api/EmployeeApi.java new file mode 100644 index 00000000..bbd873c3 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/EmployeeApi.java @@ -0,0 +1,32 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.dtos.PageResponse; +import retrofit2.Call; +import retrofit2.http.*; + +public interface EmployeeApi { + + @GET("api/v1/employees") + Call> getAllEmployees( + @Query("page") int page, + @Query("size") int size); + + @GET("api/v1/employees/{id}") + Call getEmployeeById(@Path("id") Long id); + + @POST("api/v1/employees") + Call createEmployee(@Body EmployeeDTO employee); + + @PUT("api/v1/employees/{id}") + Call updateEmployee(@Path("id") Long id, @Body EmployeeDTO employee); + + @DELETE("api/v1/employees/{id}") + Call deleteEmployee(@Path("id") Long id); +} + + + + + + diff --git a/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java index 99618112..fa1e17b4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java @@ -2,13 +2,13 @@ package com.example.petstoremobile.api; import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.InventoryDTO; -import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.DELETE; import retrofit2.http.GET; +import retrofit2.http.HTTP; import retrofit2.http.POST; import retrofit2.http.PUT; import retrofit2.http.Path; @@ -30,17 +30,17 @@ public interface InventoryApi { // POST /api/v1/inventory @POST("api/v1/inventory") - Call createInventory(@Body InventoryRequest request); + Call createInventory(@Body InventoryDTO request); // PUT /api/v1/inventory/{id} @PUT("api/v1/inventory/{id}") - Call updateInventory(@Path("id") Long id, @Body InventoryRequest request); + Call updateInventory(@Path("id") Long id, @Body InventoryDTO request); // DELETE /api/v1/inventory/{id} @DELETE("api/v1/inventory/{id}") Call deleteInventory(@Path("id") Long id); // DELETE /api/v1/inventory (bulk delete) - @DELETE("api/v1/inventory") + @HTTP(method = "DELETE", path = "api/v1/inventory", hasBody = true) Call bulkDeleteInventory(@Body BulkDeleteRequest request); } 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/api/ProductSupplierApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java index 67a0e7f2..b4414be5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.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.ProductSupplierDTO; import retrofit2.Call; @@ -34,4 +35,6 @@ public interface ProductSupplierApi { Call deleteProductSupplier( @Path("productId") Long productId, @Path("supplierId") Long supplierId); + @HTTP(method = "DELETE", path = "api/v1/product-suppliers", hasBody = true) + Call bulkDeleteProductSuppliers(@Body BulkDeleteRequest request); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/RefundApi.java b/android/app/src/main/java/com/example/petstoremobile/api/RefundApi.java new file mode 100644 index 00000000..d7ba9575 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/RefundApi.java @@ -0,0 +1,25 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.RefundDTO; +import retrofit2.Call; +import retrofit2.http.*; + +import java.util.List; + +public interface RefundApi { + + @GET("api/v1/refunds") + Call> getAllRefunds(); + + @GET("api/v1/refunds/{id}") + Call getRefundById(@Path("id") Long id); + + @POST("api/v1/refunds") + Call createRefund(@Body RefundDTO refund); + + @PUT("api/v1/refunds/{id}") + Call updateRefund(@Path("id") Long id, @Body RefundDTO refund); + + @DELETE("api/v1/refunds/{id}") + Call deleteRefund(@Path("id") Long id); +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java index 971e7306..0bdf14d1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java @@ -132,4 +132,12 @@ public class RetrofitClient { return getClient(context).create(CategoryApi.class); } + public static RefundApi getRefundApi(Context context) { + return getClient(context).create(RefundApi.class); + } + + public static EmployeeApi getEmployeeApi(Context context) { + return getClient(context).create(EmployeeApi.class); + } + } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java index b659a95f..43014a53 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.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.ServiceDTO; @@ -7,6 +8,7 @@ import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.DELETE; import retrofit2.http.GET; +import retrofit2.http.HTTP; import retrofit2.http.POST; import retrofit2.http.PUT; import retrofit2.http.Path; @@ -38,4 +40,8 @@ public interface ServiceApi { // Delete service @DELETE("api/v1/services/{id}") Call deleteService(@Path("id") Long id); + + // Bulk delete services + @HTTP(method = "DELETE", path = "api/v1/services", hasBody = true) + Call bulkDeleteServices(@Body BulkDeleteRequest request); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java b/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java index 9f870a51..38e7675c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.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.SupplierDTO; @@ -7,6 +8,7 @@ import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.DELETE; import retrofit2.http.GET; +import retrofit2.http.HTTP; import retrofit2.http.POST; import retrofit2.http.PUT; import retrofit2.http.Path; @@ -38,4 +40,8 @@ public interface SupplierApi { // Delete supplier @DELETE("api/v1/suppliers/{id}") Call deleteSupplier(@Path("id") Long id); + + // Bulk delete suppliers + @HTTP(method = "DELETE", path = "api/v1/suppliers", hasBody = true) + Call bulkDeleteSuppliers(@Body BulkDeleteRequest request); } diff --git a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java index b5f1ca64..21ea11b8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java +++ b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java @@ -179,4 +179,16 @@ public class NetworkModule { public static UserApi provideUserApi(Retrofit retrofit) { return retrofit.create(UserApi.class); } + + @Provides + @Singleton + public static EmployeeApi provideEmployeeApi(Retrofit retrofit) { + return retrofit.create(EmployeeApi.class); + } + + @Provides + @Singleton + public static RefundApi provideRefundApi(Retrofit retrofit) { + return retrofit.create(RefundApi.class); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java index d8bf2b6b..d48bc942 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java @@ -3,6 +3,7 @@ package com.example.petstoremobile.dtos; import java.math.BigDecimal; public class AdoptionDTO { + private Long adoptionId; private Long petId; private String petName; @@ -18,80 +19,58 @@ public class AdoptionDTO { private String createdAt; private String updatedAt; - public AdoptionDTO(Long petId, Long customerId, Long sourceStoreId, String adoptionDate, String adoptionStatus) { - this.petId = petId; - this.customerId = customerId; - this.sourceStoreId = sourceStoreId; - this.adoptionDate = adoptionDate; - this.adoptionStatus = adoptionStatus; - } + public AdoptionDTO() {} - public AdoptionDTO(Long petId, Long customerId, Long employeeId, Long sourceStoreId, String adoptionDate, String adoptionStatus) { + public AdoptionDTO(Long petId, Long customerId, Long employeeId, Long sourceStoreId, + String adoptionDate, String adoptionStatus, BigDecimal adoptionFee) { this.petId = petId; this.customerId = customerId; this.employeeId = employeeId; this.sourceStoreId = sourceStoreId; this.adoptionDate = adoptionDate; this.adoptionStatus = adoptionStatus; + this.adoptionFee = adoptionFee; } - public Long getAdoptionId() { - return adoptionId; - } + public Long getAdoptionId() { return adoptionId; } + public void setAdoptionId(Long adoptionId) { this.adoptionId = adoptionId; } - public Long getPetId() { - return petId; - } + public Long getPetId() { return petId; } + public void setPetId(Long petId) { this.petId = petId; } - public String getPetName() { - return petName; - } + public String getPetName() { return petName; } + public void setPetName(String petName) { this.petName = petName; } - public Long getCustomerId() { - return customerId; - } + public Long getCustomerId() { return customerId; } + public void setCustomerId(Long customerId) { this.customerId = customerId; } - public String getCustomerName() { - return customerName; - } + public String getCustomerName() { return customerName; } + public void setCustomerName(String customerName) { this.customerName = customerName; } - public Long getEmployeeId() { - return employeeId; - } + public Long getEmployeeId() { return employeeId; } + public void setEmployeeId(Long employeeId) { this.employeeId = employeeId; } - public String getEmployeeName() { - return employeeName; - } + public String getEmployeeName() { return employeeName; } + public void setEmployeeName(String employeeName) { this.employeeName = employeeName; } - public Long getSourceStoreId() { - return sourceStoreId; - } + public Long getSourceStoreId() { return sourceStoreId; } + public void setSourceStoreId(Long sourceStoreId) { this.sourceStoreId = sourceStoreId; } - public String getSourceStoreName() { - return sourceStoreName; - } + public String getSourceStoreName() { return sourceStoreName; } + public void setSourceStoreName(String sourceStoreName) { this.sourceStoreName = sourceStoreName; } - public String getAdoptionDate() { - return adoptionDate; - } + public String getAdoptionDate() { return adoptionDate; } + public void setAdoptionDate(String adoptionDate) { this.adoptionDate = adoptionDate; } - public String getAdoptionStatus() { - return adoptionStatus; - } + public String getAdoptionStatus() { return adoptionStatus; } + public void setAdoptionStatus(String adoptionStatus) { this.adoptionStatus = adoptionStatus; } - public String getStatus() { - return adoptionStatus; - } + public BigDecimal getAdoptionFee() { return adoptionFee; } + public void setAdoptionFee(BigDecimal adoptionFee) { this.adoptionFee = adoptionFee; } - public BigDecimal getAdoptionFee() { - return adoptionFee; - } + public String getCreatedAt() { return createdAt; } + public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } - public String getCreatedAt() { - return createdAt; - } - - public String getUpdatedAt() { - return updatedAt; - } + public String getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java index 8505753e..37f6640f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java @@ -1,7 +1,7 @@ package com.example.petstoremobile.dtos; public class AppointmentDTO { - + private Long appointmentId; private Long customerId; private String customerName; @@ -38,83 +38,43 @@ public class AppointmentDTO { this.petId = petId; } - public Long getAppointmentId() { - return appointmentId; - } + public Long getAppointmentId() { return appointmentId; } - public Long getCustomerId() { - return customerId; - } + public Long getCustomerId() { return customerId; } - public String getCustomerName() { - return customerName; - } + public String getCustomerName() { return customerName; } - public Long getStoreId() { - return storeId; - } + public Long getStoreId() { return storeId; } - public String getStoreName() { - return storeName; - } + public String getStoreName() { return storeName; } - public Long getServiceId() { - return serviceId; - } + public Long getServiceId() { return serviceId; } - public String getServiceName() { - return serviceName; - } + public String getServiceName() { return serviceName; } - public Long getEmployeeId() { - return employeeId; - } + public Long getEmployeeId() { return employeeId; } - public String getEmployeeName() { - return employeeName; - } + public String getEmployeeName() { return employeeName; } - public String getAppointmentDate() { - return appointmentDate; - } + public String getAppointmentDate() { return appointmentDate; } - public String getAppointmentTime() { - return appointmentTime; - } + public String getAppointmentTime() { return appointmentTime; } - public String getAppointmentStatus() { - return appointmentStatus; - } + public String getAppointmentStatus() { return appointmentStatus; } - public String getPetName() { - return petName; - } + public String getPetName() { return petName; } - public Long getPetId() { - return petId; - } + public Long getPetId() { return petId; } - public String getCreatedAt() { - return createdAt; - } + public String getCreatedAt() { return createdAt; } - public String getUpdatedAt() { - return updatedAt; - } + public String getUpdatedAt() { return updatedAt; } - public Long getPetID() { - return petId; - } + public Long getPetID() { return petId; } - public String getServiceType() { - return serviceName; - } + public String getServiceType() { return serviceName; } - public Long getServiceID() { - return serviceId; - } + public Long getServiceID() { return serviceId; } - public String getStatus() { - return appointmentStatus; - } + public String getStatus() { return appointmentStatus; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java index 49f92f06..e53c8369 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java @@ -3,20 +3,20 @@ package com.example.petstoremobile.dtos; import java.util.List; public class BulkDeleteRequest { - private List ids; + private List ids; public BulkDeleteRequest() { } - public BulkDeleteRequest(List ids) { + public BulkDeleteRequest(List ids) { this.ids = ids; } - public List getIds() { + public List getIds() { return ids; } - public void setIds(List ids) { + public void setIds(List ids) { this.ids = ids; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java new file mode 100644 index 00000000..21577a25 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java @@ -0,0 +1,102 @@ +package com.example.petstoremobile.dtos; + +public class EmployeeDTO { + + private long EmployeeId; + private Long userId; + private String username; + private String firstName; + private String lastName; + private String fullName; + private String email; + private String phone; + private String role; + private Boolean active; + private String createAt; + private String updatedAt; + + + // Constructor for create and update the employee + + + public EmployeeDTO(String username, String password, String firstName, String lastName, + String email, String phone, String role, boolean active) { + this.username = username; + this.password = password; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.phone = phone; + this.role = role; + this.active = active; + } + // password field for request only + private String password; + + + public long getEmployeeId() { + + return EmployeeId; + } + + public Long getUserId() { + + return userId; + } + + public String getUsername() { + + return username; + } + + public String getFirstName() { + + return firstName; + } + + public String getLastName() { + + return lastName; + } + + public String getFullName() { + + return fullName; + } + + public String getEmail() { + + return email; + } + public String getPhone() { + + return phone; + } + + public String getRole() { + + return role; + } + + public Boolean getActive() { + + return active; + } + + public String getCreateAt() { + + return createAt; + } + + public String getUpdatedAt() { + + return updatedAt; + } + + public String getPassword() { + + return password; + } + + +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java index fe2ec542..ddafd045 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java @@ -6,6 +6,8 @@ public class InventoryDTO { private Long prodId; private String productName; private String categoryName; + private Long storeId; + private String storeName; private Integer quantity; private String createdAt; private String updatedAt; @@ -14,8 +16,9 @@ public class InventoryDTO { } // Constructor for create/update requests (matches InventoryRequest) - public InventoryDTO(Long prodId, Integer quantity) { + public InventoryDTO(Long prodId, Long storeId, Integer quantity) { this.prodId = prodId; + this.storeId = storeId; this.quantity = quantity; } @@ -51,6 +54,22 @@ public class InventoryDTO { this.categoryName = categoryName; } + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + public Integer getQuantity() { return quantity; } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java deleted file mode 100644 index f84dfb5f..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.petstoremobile.dtos; - -public class InventoryRequest { - private Long prodId; - private Integer quantity; - - public InventoryRequest() { - } - - public InventoryRequest(Long prodId, Integer quantity) { - this.prodId = prodId; - this.quantity = quantity; - } - - public Long getProdId() { - return prodId; - } - - public void setProdId(Long prodId) { - this.prodId = prodId; - } - - public Integer getQuantity() { - return quantity; - } - - public void setQuantity(Integer quantity) { - this.quantity = quantity; - } -} - diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/PurchaseOrderDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/PurchaseOrderDTO.java index d7a392ea..813633c9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/PurchaseOrderDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/PurchaseOrderDTO.java @@ -4,6 +4,8 @@ public class PurchaseOrderDTO { private Long purchaseOrderId; private Long supId; private String supplierName; + private Long storeId; + private String storeName; private String orderDate; private String status; private String createdAt; @@ -21,6 +23,14 @@ public class PurchaseOrderDTO { return supplierName; } + public Long getStoreId() { + return storeId; + } + + public String getStoreName() { + return storeName; + } + public String getOrderDate() { return orderDate; } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/RefundDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/RefundDTO.java new file mode 100644 index 00000000..5f47e1ce --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/RefundDTO.java @@ -0,0 +1,58 @@ +package com.example.petstoremobile.dtos; + +import java.math.BigDecimal; + +public class RefundDTO { + // Response fields + private Long id; + private Long saleId; + private Long customerId; + private BigDecimal amount; + private String reason; + private String status; + private String createdAt; + private String updatedAt; + + // Constructor for create request + public RefundDTO(Long saleId, String reason) { + this.saleId = saleId; + this.reason = reason; + } + + // Constructor for update request + public RefundDTO(String status) { + this.status = status; + } + + public Long getId() { + return id; + } + + public Long getSaleId() { + return saleId; + } + + public Long getCustomerId() { + return customerId; + } + + public BigDecimal getAmount() { + return amount; + } + + public String getReason() { + return reason; + } + + public String getStatus() { + return status; + } + + public String getCreatedAt() { + return createdAt; + } + + public String getUpdatedAt() { + return updatedAt; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java index 2cdb625f..2de34a38 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java @@ -1,82 +1,121 @@ package com.example.petstoremobile.dtos; +import java.math.BigDecimal; +import java.util.List; + public class SaleDTO { - + // Response fields private Long saleId; - - private Long productId; - private String productName; - - private Integer quantity; - private Double price; - private Double totalAmount; - private String saleDate; - private String customerName; + private Long employeeId; + private String employeeName; + private Long storeId; + private String storeName; + private BigDecimal totalAmount; + private String paymentMethod; + private Boolean isRefund; + private Long originalSaleId; + private List items; + private String createdAt; - public SaleDTO() {} + // Request fields + private Long customerId; + + // Constructor for create request + public SaleDTO(Long storeId, String paymentMethod, List items, + Boolean isRefund, Long originalSaleId, Long customerId) { + this.storeId = storeId; + this.paymentMethod = paymentMethod; + this.items = items; + this.isRefund = isRefund; + this.originalSaleId = originalSaleId; + this.customerId = customerId; + } public Long getSaleId() { return saleId; } - public void setSaleId(Long saleId) { - this.saleId = saleId; - } - - public Long getProductId() { - return productId; - } - - public void setProductId(Long productId) { - this.productId = productId; - } - - public String getProductName() { - return productName; - } - - public void setProductName(String productName) { - this.productName = productName; - } - - public Integer getQuantity() { - return quantity; - } - - public void setQuantity(Integer quantity) { - this.quantity = quantity; - } - - public Double getPrice() { - return price; - } - - public void setPrice(Double price) { - this.price = price; - } - - public Double getTotalAmount() { - return totalAmount; - } - - public void setTotalAmount(Double totalAmount) { - this.totalAmount = totalAmount; - } - public String getSaleDate() { return saleDate; } - public void setSaleDate(String saleDate) { - this.saleDate = saleDate; + public Long getEmployeeId() { + return employeeId; } - public String getCustomerName() { - return customerName; + public String getEmployeeName() { + return employeeName; } - public void setCustomerName(String customerName) { - this.customerName = customerName; + public Long getStoreId() { + return storeId; + } + + public String getStoreName() { + return storeName; + } + + public BigDecimal getTotalAmount() { + return totalAmount; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public Boolean getIsRefund() { + return isRefund; + } + + public Long getOriginalSaleId() { + return originalSaleId; + } + + public List getItems() { + return items; + } + + public String getCreatedAt() { + return createdAt; + } + + public Long getCustomerId() { + return customerId; + } + + // Nested SaleItemDTO + public static class SaleItemDTO { + private Long saleItemId; + private Long prodId; + private String productName; + private Integer quantity; + private BigDecimal unitPrice; + + // Constructor for request + public SaleItemDTO(Long prodId, Integer quantity) { + this.prodId = prodId; + this.quantity = quantity; + } + + public Long getSaleItemId() { + return saleItemId; + } + + public Long getProdId() { + return prodId; + } + + public String getProductName() { + return productName; + } + + public Integer getQuantity() { + return quantity; + } + + public BigDecimal getUnitPrice() { + return unitPrice; + } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java index f2621091..901a2af5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java @@ -47,6 +47,13 @@ public class ListFragment extends Fragment { binding.drawerInventory.setVisibility(View.GONE); } + // Only show for ADMIN + if ("ADMIN".equalsIgnoreCase(role)) { + binding.drawerStaff.setVisibility(View.VISIBLE); + } else { + binding.drawerStaff.setVisibility(View.GONE); + } + //add Listeners to the drawer so user won't be able to interact with the innerContainer (the list fragments) //while the drawer is open binding.drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { @@ -83,6 +90,8 @@ public class ListFragment extends Fragment { binding.drawerProductSupplier.setOnClickListener(v -> navigateTo(R.id.nav_product_supplier)); binding.drawerPurchaseOrderView.setOnClickListener(v -> navigateTo(R.id.nav_purchase_order)); binding.drawerSale.setOnClickListener(v -> navigateTo(R.id.nav_sale)); + binding.drawerStaff.setOnClickListener(v -> navigateTo(R.id.nav_staff)); + binding.drawerAnalytics.setOnClickListener(v -> navigateTo(R.id.nav_analytics)); return binding.getRoot(); } 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 fafd0d93..450b7dc8 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 @@ -1,252 +1,142 @@ package com.example.petstoremobile.fragments.listfragments; -import android.graphics.Color; import android.os.Bundle; +import android.text.*; import android.util.Log; import android.view.*; import android.widget.*; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; 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 androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AdoptionAdapter; -import com.example.petstoremobile.databinding.FragmentAdoptionBinding; +import com.example.petstoremobile.api.AdoptionApi; +import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.viewmodels.AdoptionViewModel; -import com.example.petstoremobile.utils.EventDecorator; -import com.prolificinteractive.materialcalendarview.CalendarDay; -import com.prolificinteractive.materialcalendarview.CalendarMode; -import com.prolificinteractive.materialcalendarview.MaterialCalendarView; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import com.example.petstoremobile.fragments.listfragments.detailfragments.AdoptionDetailFragment; +import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.*; +import retrofit2.*; -import dagger.hilt.android.AndroidEntryPoint; - -@AndroidEntryPoint public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener { - private FragmentAdoptionBinding binding; private List adoptionList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private AdoptionAdapter adapter; - private AdoptionViewModel viewModel; - private CalendarDay selectedCalendarDay; - private boolean isMonthMode = false; - private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + private AdoptionApi api; + private SwipeRefreshLayout swipeRefresh; + private EditText etSearch; + private ImageButton hamburger; - /** - * Initializes the fragment and its ViewModel. - */ @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); - } - - /** - * Sets up the fragment's UI components, including RecyclerView, Search, SwipeRefresh, and Calendar. - */ - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentAdoptionBinding.inflate(inflater, container, false); + View view = inflater.inflate(R.layout.fragment_adoption, container, false); - setupRecyclerView(); - setupSearch(); - setupSwipeRefresh(); - setupCalendar(); + api = RetrofitClient.getAdoptionApi(requireContext()); + hamburger = view.findViewById(R.id.btnHamburgerAdoption); + + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); loadAdoptions(); - binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1)); + FloatingActionButton fab = view.findViewById(R.id.fabAddAdoption); + fab.setOnClickListener(v -> openDetail(-1)); - binding.btnHamburgerAdoption.setOnClickListener(v -> { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } - } + hamburger.setOnClickListener(v -> { + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.openDrawer(); }); - binding.btnToggleCalendarModeAdoption.setOnClickListener(v -> toggleCalendarMode()); - - return binding.getRoot(); + return view; } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - - /** - * Toggles the calendar display between week and month modes. - */ - private void toggleCalendarMode() { - isMonthMode = !isMonthMode; - binding.calendarViewAdoption.state().edit() - .setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS) - .commit(); - } - - /** - * Sets up the date selection listener for the calendar. - */ - private void setupCalendar() { - binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> { - if (selected) { - if (date.equals(selectedCalendarDay)) { - selectedCalendarDay = null; - binding.calendarViewAdoption.clearSelection(); - } else { - selectedCalendarDay = date; - } - } else { - selectedCalendarDay = null; - } - filter(binding.etSearchAdoption.getText().toString()); - }); - } - - /** - * Updates the calendar decorators to highlight days with adoptions. - */ - private void updateCalendarDecorators() { - HashSet datesWithAdoptions = new HashSet<>(); - for (AdoptionDTO adoption : adoptionList) { - try { - if (adoption.getAdoptionDate() != null) { - Date date = dateFormat.parse(adoption.getAdoptionDate()); - if (date != null) { - Calendar cal = Calendar.getInstance(); - cal.setTime(date); - datesWithAdoptions.add(CalendarDay.from(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH))); - } - } - } catch (ParseException e) { - Log.e("AdoptionFragment", "Error parsing date: " + adoption.getAdoptionDate()); - } - } - binding.calendarViewAdoption.removeDecorators(); - binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions)); - } - - /** - * Initializes the RecyclerView for displaying adoptions. - */ - private void setupRecyclerView() { + private void setupRecyclerView(View view) { + RecyclerView rv = view.findViewById(R.id.recyclerViewAdoptions); adapter = new AdoptionAdapter(filteredList, this); - binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext())); - binding.recyclerViewAdoptions.setAdapter(adapter); + rv.setLayoutManager(new LinearLayoutManager(getContext())); + rv.setAdapter(adapter); } - /** - * Sets up the search bar for filtering - */ - private void setupSearch() { - binding.etSearchAdoption.addTextChangedListener(new android.text.TextWatcher() { + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchAdoption); + etSearch.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int a, int b, int c) {} - public void afterTextChanged(android.text.Editable s) {} + public void afterTextChanged(Editable s) {} public void onTextChanged(CharSequence s, int a, int b, int c) { filter(s.toString()); } }); } - /** - * Sets up the SwipeRefreshLayout to reload adoption data. - */ - private void setupSwipeRefresh() { - binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions); + private void setupSwipeRefresh(View view) { + swipeRefresh = view.findViewById(R.id.swipeRefreshAdoption); + swipeRefresh.setOnRefreshListener(this::loadAdoptions); } - /** - * Filters the adoption list based on search query and selected calendar date. - */ private void filter(String query) { filteredList.clear(); - String lowerQuery = query.toLowerCase(); - - String selectedDateString = null; - if (selectedCalendarDay != null) { - selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d", - selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay()); - } - - for (AdoptionDTO a : adoptionList) { - boolean matchesSearch = query.isEmpty() || - (a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lowerQuery)) || - (a.getPetName() != null && a.getPetName().toLowerCase().contains(lowerQuery)) || - (a.getAdoptionStatus() != null && a.getAdoptionStatus().toLowerCase().contains(lowerQuery)); - - boolean matchesDate = (selectedDateString == null) || - (a.getAdoptionDate() != null && a.getAdoptionDate().startsWith(selectedDateString)); - - if (matchesSearch && matchesDate) { - filteredList.add(a); + if (query.isEmpty()) { + filteredList.addAll(adoptionList); + } else { + String lower = query.toLowerCase(); + for (AdoptionDTO a : adoptionList) { + if ((a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lower)) + || (a.getPetName() != null && a.getPetName().toLowerCase().contains(lower)) + || (a.getAdoptionStatus() != null && a.getAdoptionStatus().toLowerCase().contains(lower))) { + filteredList.add(a); + } } } adapter.notifyDataSetChanged(); } - /** - * Fetches the adoption list from the server through the ViewModel. - */ private void loadAdoptions() { - //Load all adoptions from the backend using viewModel - viewModel.getAllAdoptions(0, 500).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(); - filter(binding.etSearchAdoption != null ? binding.etSearchAdoption.getText().toString() : ""); - } - 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; + if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + api.getAllAdoptions(0, 100, null, null, null, null, null).enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + if (r.isSuccessful() && r.body() != null) { + adoptionList.clear(); + adoptionList.addAll(r.body().getContent()); + filter(etSearch != null ? etSearch.getText().toString() : ""); + } else { + Toast.makeText(getContext(), "Failed to load adoptions", Toast.LENGTH_SHORT).show(); + Log.e("AdoptionFragment", "Error: " + r.message()); + } + } + public void onFailure(Call> c, Throwable t) { + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + Log.e("AdoptionFragment", t.getMessage()); } }); } - /** - * 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 = filteredList.get(position); args.putLong("adoptionId", a.getAdoptionId()); - } - + args.putLong("petId", a.getPetId() != null ? a.getPetId() : -1); + args.putLong("customerId", a.getCustomerId() != null ? a.getCustomerId() : -1); + args.putString("adoptionDate", a.getAdoptionDate()); + args.putString("adoptionStatus", a.getAdoptionStatus()); + if (a.getEmployeeId() != null) + args.putLong("employeeId", a.getEmployeeId());} 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); } + + @Override + public void onSelectionChanged(int count) {} } 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 new file mode 100644 index 00000000..c9eca2dd --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java @@ -0,0 +1,334 @@ +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.fragment.app.Fragment; +import com.example.petstoremobile.R; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.fragments.ListFragment; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import retrofit2.*; + +public class AnalyticsFragment extends Fragment { + + private TextView tvTotalRevenue, tvTotalTransactions, tvAvgTransaction, tvTotalItems; + private LinearLayout llTopRevenue, llTopQuantity, llPaymentMethods, llEmployeePerformance, llDailyRevenue; + private Button btnRefresh; + private ImageButton hamburger; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_analytics, container, false); + + initViews(view); + loadAnalytics(); + + btnRefresh.setOnClickListener(v -> loadAnalytics()); + hamburger.setOnClickListener(v -> openDrawer()); + + return view; + } + + private void openDrawer() { + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } + } + } + + private void initViews(View v) { + tvTotalRevenue = v.findViewById(R.id.tvTotalRevenue); + tvTotalTransactions = v.findViewById(R.id.tvTotalTransactions); + tvAvgTransaction = v.findViewById(R.id.tvAvgTransaction); + tvTotalItems = v.findViewById(R.id.tvTotalItems); + llTopRevenue = v.findViewById(R.id.llTopRevenue); + llTopQuantity = v.findViewById(R.id.llTopQuantity); + llPaymentMethods = v.findViewById(R.id.llPaymentMethods); + llEmployeePerformance = v.findViewById(R.id.llEmployeePerformance); + llDailyRevenue = v.findViewById(R.id.llDailyRevenue); + btnRefresh = v.findViewById(R.id.btnRefreshAnalytics); + hamburger = v.findViewById(R.id.btnHamburgerAnalytics); + } + + private void loadAnalytics() { + // Clear all sections + llTopRevenue.removeAllViews(); + llTopQuantity.removeAllViews(); + llPaymentMethods.removeAllViews(); + llEmployeePerformance.removeAllViews(); + llDailyRevenue.removeAllViews(); + + // Show loading + tvTotalRevenue.setText("Loading..."); + tvTotalTransactions.setText("..."); + tvAvgTransaction.setText("..."); + tvTotalItems.setText("..."); + + RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 1000) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (r.isSuccessful() && r.body() != null) { + computeAndDisplay(r.body().getContent()); + } else { + showError("Failed to load sales data"); + } + } + + public void onFailure(Call> c, Throwable t) { + Log.e("Analytics", t.getMessage()); + showError("Network error"); + } + }); + } + + 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()); + } + } + } + + int totalTx = regularSales.size(); + BigDecimal avgTx = totalTx > 0 + ? totalRevenue.divide(BigDecimal.valueOf(totalTx), 2, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + + tvTotalRevenue.setText("$" + totalRevenue.setScale(2, RoundingMode.HALF_UP)); + tvTotalTransactions.setText(String.valueOf(totalTx)); + tvAvgTransaction.setText("$" + avgTx); + 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(); + + llTopRevenue.removeAllViews(); + for (int i = 0; i < Math.min(5, topRevenue.size()); i++) { + Map.Entry e = topRevenue.get(i); + addBarRow(llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35"); + } + if (topRevenue.isEmpty()) + addEmptyRow(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(); + + llTopQuantity.removeAllViews(); + for (int i = 0; i < Math.min(5, topQuantity.size()); i++) { + Map.Entry e = topQuantity.get(i); + addBarRow(llTopQuantity, e.getKey(), e.getValue() + " units", + (float) e.getValue() / maxQty, "#4ecdc4"); + } + if (topQuantity.isEmpty()) + addEmptyRow(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; + llPaymentMethods.removeAllViews(); + for (Map.Entry e : paymentCount.entrySet()) { + addBarRow(llPaymentMethods, e.getKey(), + e.getValue() + " transactions", + (float) e.getValue() / maxPayment, + paymentColors[ci % paymentColors.length]); + ci++; + } + if (paymentCount.isEmpty()) + addEmptyRow(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(); + + llEmployeePerformance.removeAllViews(); + for (Map.Entry e : empList) { + addBarRow(llEmployeePerformance, e.getKey(), + "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxEmp.floatValue(), + "#1a759f"); + } + if (empList.isEmpty()) + addEmptyRow(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); + } + } + } + + BigDecimal maxDaily = dailyRevenue.values().stream() + .max(BigDecimal::compareTo).orElse(BigDecimal.ONE); + if (maxDaily.compareTo(BigDecimal.ZERO) == 0) + maxDaily = BigDecimal.ONE; + + 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(llDailyRevenue, label, + "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxDaily.floatValue(), + "#ff6b35"); + } + } + + // Adds a horizontal bar row with label, value and a proportional bar + private void addBarRow(LinearLayout parent, String label, String value, float ratio, String color) { + LinearLayout row = new LinearLayout(getContext()); + row.setOrientation(LinearLayout.VERTICAL); + row.setPadding(0, 6, 0, 6); + + // Label + value row + LinearLayout labelRow = new LinearLayout(getContext()); + labelRow.setOrientation(LinearLayout.HORIZONTAL); + + TextView tvLabel = new TextView(getContext()); + tvLabel.setLayoutParams(new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvLabel.setText(label); + tvLabel.setTextColor(Color.parseColor("#444441")); + tvLabel.setTextSize(13f); + + TextView tvValue = new TextView(getContext()); + tvValue.setText(value); + tvValue.setTextColor(Color.parseColor("#444441")); + tvValue.setTextSize(13f); + tvValue.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); + + labelRow.addView(tvLabel); + labelRow.addView(tvValue); + + // Bar background + LinearLayout barBg = new LinearLayout(getContext()); + LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, 12); + bgParams.setMargins(0, 4, 0, 0); + barBg.setLayoutParams(bgParams); + barBg.setBackgroundColor(Color.parseColor("#EEEEEE")); + + // Bar fill + View barFill = new View(getContext()); + int fillWidth = (int) (ratio * 100); + LinearLayout.LayoutParams fillParams = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.MATCH_PARENT, ratio); + barFill.setLayoutParams(fillParams); + barFill.setBackgroundColor(Color.parseColor(color)); + barBg.addView(barFill); + + // Empty space + View spacer = new View(getContext()); + spacer.setLayoutParams(new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - ratio)); + barBg.addView(spacer); + + row.addView(labelRow); + row.addView(barBg); + parent.addView(row); + } + + private void addEmptyRow(LinearLayout parent, String message) { + TextView tv = new TextView(getContext()); + tv.setText(message); + tv.setTextColor(Color.parseColor("#888780")); + tv.setTextSize(13f); + parent.addView(tv); + } + + private void showError(String msg) { + if (getContext() == null) + return; + tvTotalRevenue.setText("Error"); + tvTotalTransactions.setText("—"); + tvAvgTransaction.setText("—"); + tvTotalItems.setText("—"); + Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); + } +} 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 3724850b..f7aed714 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 @@ -16,16 +16,15 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AppointmentAdapter; -import com.example.petstoremobile.adapters.WhiteTextArrayAdapter; import com.example.petstoremobile.databinding.FragmentAppointmentBinding; import com.example.petstoremobile.dtos.AppointmentDTO; 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.AppointmentViewModel; @@ -57,6 +56,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private AppointmentViewModel appointmentViewModel; private StoreViewModel storeViewModel; private AuthViewModel authViewModel; + private BulkDeleteHandler bulkDeleteHandler; private CalendarDay selectedCalendarDay; private boolean isMonthMode = false; @@ -90,6 +90,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. setupCalendar(); setupFilterToggle(); setupMyAppointmentFilter(); + setupBulkDelete(); binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); @@ -110,6 +111,19 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. return binding.getRoot(); } + private void setupBulkDelete() { + bulkDeleteHandler = new BulkDeleteHandler( + this, + binding.layoutBulkDelete, + binding.tvSelectionCount, + binding.btnBulkDelete, + adapter, + "appointment", + appointmentViewModel::bulkDeleteAppointments, + this::loadAppointmentData + ); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -237,30 +251,14 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"}; - WhiteTextArrayAdapter adapter = new WhiteTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - binding.spinnerStatus.setAdapter(adapter); - - binding.spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - loadAppointmentData(); - } - @Override public void onNothingSelected(AdapterView parent) {} - }); + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadAppointmentData); } /** * Configures the store filter spinner. */ private void setupStoreFilter() { - binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - loadAppointmentData(); - } - @Override public void onNothingSelected(AdapterView parent) {} - }); + SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadAppointmentData); } /** @@ -303,6 +301,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. openAppointmentDetails(position); } + @Override + public void onSelectionChanged(int count) { + if (bulkDeleteHandler != null) { + bulkDeleteHandler.onSelectionChanged(count); + } + } + /** * Fetches appointment data from the server with all active filters. */ @@ -366,4 +371,4 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAppointments.setAdapter(adapter); } -} \ No newline at end of file +} 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 fe68a629..bf78e2b8 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 @@ -7,7 +7,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -24,6 +23,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 +44,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 +73,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn setupStoreFilter(); setupSwipeRefresh(); setupFilterToggle(); + setupBulkDelete(); loadInventory(true); loadStoreData(); @@ -87,11 +89,22 @@ 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, + adapter, + "inventory item", + viewModel::bulkDeleteInventory, + () -> loadInventory(true) + ); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -134,13 +147,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn * Configures the store filter spinner. */ private void setupStoreFilter() { - binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - loadInventory(true); - } - @Override public void onNothingSelected(AdapterView parent) {} - }); + SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadInventory(true)); } /** @@ -243,50 +250,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 +285,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..578022c3 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 @@ -15,16 +15,15 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.PetAdapter; -import com.example.petstoremobile.adapters.WhiteTextArrayAdapter; 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 +45,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 +74,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen setupStoreFilter(); setupSwipeRefresh(); setupFilterToggle(); + setupBulkDelete(); binding.fabAddPet.setOnClickListener(v -> openPetDetails()); @@ -90,6 +91,19 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen return binding.getRoot(); } + private void setupBulkDelete() { + bulkDeleteHandler = new BulkDeleteHandler( + this, + binding.layoutBulkDelete, + binding.tvSelectionCount, + binding.btnBulkDelete, + adapter, + "pet", + viewModel::bulkDeletePets, + this::loadPetData + ); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -145,17 +159,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"}; - WhiteTextArrayAdapter adapter = new WhiteTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - binding.spinnerStatus.setAdapter(adapter); - - binding.spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - loadPetData(); - } - @Override public void onNothingSelected(AdapterView parent) {} - }); + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData); } /** @@ -163,30 +167,14 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen */ private void setupSpeciesFilter() { String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"}; - WhiteTextArrayAdapter adapter = new WhiteTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, species); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - binding.spinnerSpecies.setAdapter(adapter); - - binding.spinnerSpecies.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - loadPetData(); - } - @Override public void onNothingSelected(AdapterView parent) {} - }); + SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, species, this::loadPetData); } /** * Configures the store filter spinner. */ private void setupStoreFilter() { - binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - loadPetData(); - } - @Override public void onNothingSelected(AdapterView parent) {} - }); + SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData); } /** @@ -231,6 +219,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/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 0eea959f..b2c28fee 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 @@ -15,7 +15,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.Toast; import com.example.petstoremobile.R; @@ -137,13 +136,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc * Configures the category filter spinner. */ private void setupCategoryFilter() { - binding.spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - loadProductData(); - } - @Override public void onNothingSelected(AdapterView parent) {} - }); + SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData); } /** @@ -173,7 +166,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc Bundle args = new Bundle(); if (position != -1) { ProductDTO product = productList.get(position); - args.putLong("productId", product.getProdId()); + args.putLong("prodId", product.getProdId()); } NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args); } 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 1c0a9c19..578aa7a9 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 @@ -7,7 +7,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -24,6 +23,7 @@ import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.dtos.SupplierDTO; 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.ProductSupplierViewModel; @@ -48,6 +48,7 @@ public class ProductSupplierFragment extends Fragment private ProductSupplierViewModel viewModel; private ProductViewModel productViewModel; private SupplierViewModel supplierViewModel; + private BulkDeleteHandler bulkDeleteHandler; /** * Initializes the fragment and its associated ViewModels. @@ -74,6 +75,7 @@ public class ProductSupplierFragment extends Fragment setupSupplierFilter(); setupSwipeRefresh(); setupFilterToggle(); + setupBulkDelete(); binding.fabAddPS.setOnClickListener(v -> openDetail(-1)); @@ -90,6 +92,19 @@ public class ProductSupplierFragment extends Fragment return binding.getRoot(); } + private void setupBulkDelete() { + bulkDeleteHandler = new BulkDeleteHandler( + this, + binding.layoutBulkDelete, + binding.tvSelectionCount, + binding.btnBulkDelete, + adapter, + "relationship", + viewModel::bulkDeleteProductSuppliers, + this::loadData + ); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -152,26 +167,14 @@ public class ProductSupplierFragment extends Fragment * Configures the product filter spinner. */ private void setupProductFilter() { - binding.spinnerProduct.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - loadData(); - } - @Override public void onNothingSelected(AdapterView parent) {} - }); + SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData); } /** * Configures the supplier filter spinner. */ private void setupSupplierFilter() { - binding.spinnerSupplier.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - loadData(); - } - @Override public void onNothingSelected(AdapterView parent) {} - }); + SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData); } /** @@ -265,4 +268,11 @@ public class ProductSupplierFragment extends Fragment */ @Override public void onProductSupplierClick(int position) { openDetail(position); } + + @Override + public void onSelectionChanged(int count) { + if (bulkDeleteHandler != null) { + bulkDeleteHandler.onSelectionChanged(count); + } + } } 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 452f8d69..9a758cb8 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 @@ -7,7 +7,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -133,13 +132,7 @@ public class PurchaseOrderFragment extends Fragment * Configures the store filter spinner. */ private void setupStoreFilter() { - binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - loadData(); - } - @Override public void onNothingSelected(AdapterView parent) {} - }); + SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadData); } /** 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 99f249db..dc882fff 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 @@ -2,7 +2,9 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import android.text.Editable; @@ -13,38 +15,43 @@ import android.view.ViewGroup; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SaleAdapter; -import com.example.petstoremobile.api.SaleApi; import com.example.petstoremobile.databinding.FragmentSaleBinding; +import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.models.Sale; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.viewmodels.SaleViewModel; + import java.util.ArrayList; import java.util.List; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { private FragmentSaleBinding binding; - private List saleList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private List saleList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); private SaleAdapter adapter; - - @Inject SaleApi api; + private SaleViewModel saleViewModel; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentSaleBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + saleViewModel = new ViewModelProvider(this).get(SaleViewModel.class); setupRecyclerView(); - loadSaleData(); setupSearch(); setupSwipeRefresh(); + loadSales(); - // Make the hamburger button open the drawer from listFragment binding.btnHamburger.setOnClickListener(v -> { Fragment parent = getParentFragment(); if (parent != null) { @@ -55,7 +62,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } }); - return binding.getRoot(); + binding.fabAddSale.setOnClickListener(v -> + NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail)); + + binding.btnOpenRefund.setOnClickListener(v -> + NavHostFragment.findNavController(this).navigate(R.id.nav_refund)); } @Override @@ -64,19 +75,20 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis binding = null; } + private void setupRecyclerView() { + adapter = new SaleAdapter(filteredList, this); + binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewSales.setAdapter(adapter); + } + private void setupSearch() { binding.etSearchSale.addTextChangedListener(new TextWatcher() { - @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void afterTextChanged(Editable s) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { filterSales(s.toString()); } - - @Override - public void afterTextChanged(Editable s) { - } }); } @@ -86,60 +98,46 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis filteredList.addAll(saleList); } else { String lower = query.toLowerCase(); - for (Sale s : saleList) { - if (s.getItemName().toLowerCase().contains(lower) - || s.getEmployeeName().toLowerCase().contains(lower) - || s.getSaleDate().toLowerCase().contains(lower) - || s.getPaymentMethod().toLowerCase().contains(lower) - || String.valueOf(s.getSaleId()).contains(lower)) { + 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); } } } - adapter.notifyDataSetChanged(); + if (adapter != null) adapter.notifyDataSetChanged(); } private void setupSwipeRefresh() { binding.swipeRefreshSale.setOnRefreshListener(() -> { - loadSaleData(); - binding.swipeRefreshSale.setRefreshing(false); + loadSales(); + }); + } + + 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() : ""); + } }); } - // When a sale row is clicked, open the refund screen for that sale @Override public void onSaleClick(int position) { - Sale sale = filteredList.get(position); + SaleDTO sale = filteredList.get(position); Bundle args = new Bundle(); - args.putInt("saleId", sale.getSaleId()); - args.putString("saleDate", sale.getSaleDate()); - args.putString("employeeName", sale.getEmployeeName()); - args.putDouble("total", sale.getTotal()); - args.putString("paymentMethod", sale.getPaymentMethod()); - - NavHostFragment.findNavController(this).navigate(R.id.nav_refund_detail, args); - } - - public void reloadSales() { - loadSaleData(); - } - - // TODO: Replace with actual API call - GET v1/sales - private void loadSaleData() { - saleList.clear(); - saleList.add(new Sale(1, "2026-03-01", "John Smith", "Premium Dog Food", 2, 45.99, 91.98, "Card", false)); - saleList.add(new Sale(2, "2026-03-02", "Jane Doe", "Cat Toy Bundle", 1, 19.99, 19.99, "Cash", false)); - saleList.add(new Sale(3, "2026-03-03", "John Smith", "Pet Shampoo", 3, 12.99, 38.97, "Card", false)); - saleList.add(new Sale(4, "2026-03-04", "Jane Doe", "Dog Bed - Large", 1, 89.99, 89.99, "Cash", true)); - filteredList.clear(); - filteredList.addAll(saleList); - if (adapter != null) - adapter.notifyDataSetChanged(); - } - - private void setupRecyclerView() { - adapter = new SaleAdapter(filteredList, this); - binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); - binding.recyclerViewSales.setAdapter(adapter); + if (sale.getSaleId() != null) { + args.putLong("saleId", sale.getSaleId()); + } + if (sale.getIsRefund() != null) { + args.putBoolean("isRefund", sale.getIsRefund()); + } + NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail, args); } } 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 bda282ce..0923e4b1 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,14 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.fragment.NavHostFragment; -import androidx.recyclerview.widget.LinearLayoutManager; - import android.text.Editable; import android.text.TextWatcher; import android.util.Log; @@ -17,11 +9,21 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +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 com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ServiceAdapter; import com.example.petstoremobile.databinding.FragmentServiceBinding; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.BulkDeleteHandler; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.viewmodels.ServiceViewModel; import java.util.ArrayList; @@ -29,16 +31,28 @@ import java.util.List; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment class for displaying a list of services in a RecyclerView. + */ @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 List serviceList = new ArrayList<>(); + private final List serviceList = new ArrayList<>(); private ServiceAdapter adapter; private ServiceViewModel viewModel; + private BulkDeleteHandler bulkDeleteHandler; + + // Pagination + private int currentPage = 0; + private boolean isLastPage = false; + private boolean isLoading = false; /** - * Initializes the fragment and its associated ServiceViewModel. + * Initializes the fragment and its associated ViewModel. */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -58,12 +72,11 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic setupSearch(); setupSwipeRefresh(); setupFilterToggle(); - loadServiceData(); + setupBulkDelete(); + loadServices(true); - //Add button to opens the add dialog - binding.fabAddService.setOnClickListener(v -> openServiceDetails(-1)); + binding.fabAddService.setOnClickListener(v -> openDetail(null)); - //Make the hamburger button open the drawer from listFragment binding.btnHamburger.setOnClickListener(v -> { Fragment parent = getParentFragment(); if (parent != null) { @@ -77,6 +90,19 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic return binding.getRoot(); } + private void setupBulkDelete() { + bulkDeleteHandler = new BulkDeleteHandler( + this, + binding.layoutBulkDelete, + binding.tvSelectionCount, + binding.btnBulkDelete, + adapter, + "service", + viewModel::bulkDeleteServices, + () -> loadServices(true) + ); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -95,100 +121,121 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic binding.layoutFilter.setVisibility(View.GONE); binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); - // Reset search when closing + // Reset filters when closing binding.etSearchService.setText(""); } }); } /** - * Configures the search bar for filtering. + * Sets up the search bar for filtering. */ private void setupSearch() { binding.etSearchService.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { - loadServiceData(); + loadServices(true); } @Override public void afterTextChanged(Editable s) {} }); } /** - * Sets up the SwipeRefreshLayout to allow manual reloading of service data. + * Initializes the RecyclerView with a layout manager and adapter. + */ + private void setupRecyclerView() { + adapter = new ServiceAdapter(serviceList, this); + binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewServices.setAdapter(adapter); + + binding.recyclerViewServices.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) return; + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewServices.getLayoutManager(); + if (lm == null) return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + loadServices(false); + } + } + }); + } + + /** + * Sets up the SwipeRefreshLayout. */ private void setupSwipeRefresh() { - binding.swipeRefreshService.setOnRefreshListener(this::loadServiceData); + binding.swipeRefreshService.setOnRefreshListener(() -> loadServices(true)); } /** - * Navigates to the service detail screen for editing an existing service or adding a new one. + * Fetches a page of services from the API. */ - private void openServiceDetails(int position) { - //Make a bundle to pass data to the detail fragment - Bundle args = new Bundle(); + private void loadServices(boolean reset) { + if (isLoading) return; - //if editing a service, add the service id to the bundle - if (position != -1) { - ServiceDTO service = serviceList.get(position); - args.putLong("serviceId", service.getServiceId()); + if (reset) { + currentPage = 0; + isLastPage = false; } - NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail, args); - } - - /** - * Handles item click in the service list. - */ - @Override - public void onServiceClick(int position) { - openServiceDetails(position); - } - - /** - * Fetches all service data from the server through the ViewModel and updates the UI. - */ - private void loadServiceData() { - String query = binding.etSearchService != null ? binding.etSearchService.getText().toString().trim() : ""; + String query = binding.etSearchService.getText().toString().trim(); if (query.isEmpty()) query = null; - //Load services from the backend with query and default sort - viewModel.getAllServices(0, 100, query, "serviceName").observe(getViewLifecycleOwner(), resource -> { + viewModel.getAllServices(currentPage, PAGE_SIZE, query, "serviceName").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.swipeRefreshService.setRefreshing(true); break; case SUCCESS: - // Hide loading indicator and display data + isLoading = false; binding.swipeRefreshService.setRefreshing(false); if (resource.data != null) { - serviceList.clear(); + if (reset) serviceList.clear(); serviceList.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.swipeRefreshService.setRefreshing(false); - if (getContext() != null) { - Toast.makeText(getContext(), "Failed to load services: " + resource.message, Toast.LENGTH_SHORT).show(); - } - Log.e("ServiceFragment", "Error loading services: " + resource.message); + Log.e(TAG, "Error: " + resource.message); + Toast.makeText(getContext(), "Failed to load services: " + resource.message, Toast.LENGTH_SHORT).show(); break; } }); } /** - * Initializes the RecyclerView with a layout manager and adapter for services. + * Navigates to the service detail screen. */ - private void setupRecyclerView() { - adapter = new ServiceAdapter(serviceList, this); - binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext())); - binding.recyclerViewServices.setAdapter(adapter); + private void openDetail(ServiceDTO service) { + Bundle args = new Bundle(); + if (service != null) { + args.putLong("serviceId", service.getServiceId()); + } + NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail, args); + } + + @Override + public void onServiceClick(int position) { + if (position >= 0 && position < serviceList.size()) { + openDetail(serviceList.get(position)); + } + } + + @Override + public void onSelectionChanged(int count) { + if (bulkDeleteHandler != null) { + bulkDeleteHandler.onSelectionChanged(count); + } } } 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 new file mode 100644 index 00000000..1c8fcf4e --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java @@ -0,0 +1,147 @@ +package com.example.petstoremobile.fragments.listfragments; + +import android.os.Bundle; +import android.text.*; +import android.util.Log; +import android.view.*; +import android.widget.*; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.EmployeeAdapter; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.fragments.ListFragment; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import java.util.*; +import retrofit2.*; + +public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener { + + private List employeeList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); + private EmployeeAdapter adapter; + private SwipeRefreshLayout swipeRefresh; + private EditText etSearch; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_staff, container, false); + + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); + loadStaff(); + + FloatingActionButton fab = view.findViewById(R.id.fabAddStaff); + fab.setOnClickListener(v -> openDetail(-1)); + + ImageButton hamburger = view.findViewById(R.id.btnHamburgerStaff); + hamburger.setOnClickListener(v -> openDrawer()); + + return view; + } + + private void openDrawer() { + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } + } + } + + private void setupRecyclerView(View view) { + RecyclerView rv = view.findViewById(R.id.recyclerViewStaff); + adapter = new EmployeeAdapter(filteredList, this); + rv.setLayoutManager(new LinearLayoutManager(getContext())); + rv.setAdapter(adapter); + } + + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchStaff); + etSearch.addTextChangedListener(new TextWatcher() { + public void beforeTextChanged(CharSequence s, int a, int b, int c) {} + public void afterTextChanged(Editable s) {} + public void onTextChanged(CharSequence s, int a, int b, int c) { + filter(s.toString()); + } + }); + } + + private void setupSwipeRefresh(View view) { + swipeRefresh = view.findViewById(R.id.swipeRefreshStaff); + swipeRefresh.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() { + if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + RetrofitClient.getEmployeeApi(requireContext()).getAllEmployees(0, 100) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + if (r.isSuccessful() && r.body() != null) { + employeeList.clear(); + employeeList.addAll(r.body().getContent()); + filter(etSearch != null ? etSearch.getText().toString() : ""); + } else { + Toast.makeText(getContext(), "Failed to load staff", + Toast.LENGTH_SHORT).show(); + } + } + public void onFailure(Call> c, Throwable t) { + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + Log.e("StaffFragment", t.getMessage()); + } + }); + } + + private void openDetail(int position) { + Bundle args = new Bundle(); + if (position != -1) { + EmployeeDTO e = filteredList.get(position); + args.putLong("employeeId", e.getEmployeeId()); + args.putString("username", e.getUsername() != null ? e.getUsername() : ""); + args.putString("firstName", e.getFirstName() != null ? e.getFirstName() : ""); + args.putString("lastName", e.getLastName() != null ? e.getLastName() : ""); + args.putString("email", e.getEmail() != null ? e.getEmail() : ""); + args.putString("phone", e.getPhone() != null ? e.getPhone() : ""); + args.putString("role", e.getRole() != null ? e.getRole() : "STAFF"); + args.putBoolean("active", Boolean.TRUE.equals(e.getActive())); + args.putBoolean("isEditing", true); + } + NavHostFragment.findNavController(this).navigate(R.id.nav_staff_detail, args); + } + + @Override + public void onEmployeeClick(int position) { + openDetail(position); + } +} 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 3d2e038d..6d6f28ac 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 @@ -22,6 +22,8 @@ import com.example.petstoremobile.adapters.SupplierAdapter; import com.example.petstoremobile.databinding.FragmentSupplierBinding; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.BulkDeleteHandler; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.viewmodels.SupplierViewModel; import java.util.ArrayList; @@ -36,6 +38,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp private List supplierList = new ArrayList<>(); private SupplierAdapter adapter; private SupplierViewModel viewModel; + private BulkDeleteHandler bulkDeleteHandler; /** * Initializes the fragment and its associated SupplierViewModel. @@ -58,6 +61,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp setupSearch(); setupSwipeRefresh(); setupFilterToggle(); + setupBulkDelete(); loadSupplierData(); //Add button to opens the add dialog @@ -77,6 +81,19 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp return binding.getRoot(); } + private void setupBulkDelete() { + bulkDeleteHandler = new BulkDeleteHandler( + this, + binding.layoutBulkDelete, + binding.tvSelectionCount, + binding.btnBulkDelete, + adapter, + "supplier", + viewModel::bulkDeleteSuppliers, + this::loadSupplierData + ); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -146,6 +163,13 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp openSupplierDetails(position); } + @Override + public void onSelectionChanged(int count) { + if (bulkDeleteHandler != null) { + bulkDeleteHandler.onSelectionChanged(count); + } + } + /** * Fetches all supplier data from the server through the ViewModel and updates the UI. */ 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 8789433d..7098b795 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 @@ -19,7 +19,9 @@ 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 java.math.BigDecimal; import java.util.*; import dagger.hilt.android.AndroidEntryPoint; @@ -37,10 +39,12 @@ public class AdoptionDetailFragment extends Fragment { 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"}; @@ -48,6 +52,7 @@ public class AdoptionDetailFragment extends Fragment { private PetViewModel petViewModel; private CustomerViewModel customerViewModel; private StoreViewModel storeViewModel; + private UserViewModel userViewModel; @Override public void onCreate(Bundle savedInstanceState) { @@ -56,6 +61,7 @@ public class AdoptionDetailFragment extends Fragment { 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); } @Override @@ -113,6 +119,7 @@ public class AdoptionDetailFragment extends Fragment { loadPets(); loadCustomers(); loadStores(); + loadEmployees(); } /** @@ -179,6 +186,27 @@ public class AdoptionDetailFragment extends Fragment { preselectedStoreId, StoreDTO::getStoreId); } + /** + * Loads the list of employees from the API. + */ + private void loadEmployees() { + userViewModel.getUsers("STAFF", 0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + employeeList = resource.data.getContent(); + refreshEmployeeSpinner(); + } + }); + } + + /** + * Populates the employee selection spinner with data. + */ + private void refreshEmployeeSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, employeeList, + UserDTO::getFullName, "-- Select Staff --", + preselectedEmployeeId, UserDTO::getId); + } + /** * Handles arguments to determine if the fragment is in edit or add mode. */ @@ -210,12 +238,16 @@ public class AdoptionDetailFragment extends Fragment { preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; preselectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1; + preselectedEmployeeId = a.getEmployeeId() != null ? a.getEmployeeId() : -1; + binding.etAdoptionDate.setText(a.getAdoptionDate()); + binding.etAdoptionFee.setText(a.getAdoptionFee() != null ? a.getAdoptionFee().toString() : ""); SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getAdoptionStatus()); refreshPetSpinner(); refreshCustomerSpinner(); refreshStoreSpinner(); + refreshEmployeeSpinner(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -240,17 +272,36 @@ public class AdoptionDetailFragment extends Fragment { Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; } + BigDecimal fee = BigDecimal.ZERO; + String feeStr = binding.etAdoptionFee.getText().toString().trim(); + if (!feeStr.isEmpty()) { + try { + fee = new BigDecimal(feeStr); + } catch (NumberFormatException e) { + Toast.makeText(getContext(), "Invalid fee format", Toast.LENGTH_SHORT).show(); + return; + } + } + CustomerDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); PetDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); StoreDTO store = storeList.get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); + + Long employeeId = null; + if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0) { + employeeId = employeeList.get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId(); + } + String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; AdoptionDTO dto = new AdoptionDTO( pet.getPetId(), customer.getCustomerId(), + employeeId, store.getStoreId(), date, - status + status, + fee ); if (isEditing) { 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 1a32df6e..7a729b94 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 @@ -1,14 +1,9 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.text.Editable; -import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; import android.widget.Toast; import androidx.annotation.NonNull; @@ -18,15 +13,16 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; -import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding; import com.example.petstoremobile.dtos.InventoryDTO; -import com.example.petstoremobile.dtos.InventoryRequest; 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; @@ -43,20 +39,15 @@ public class InventoryDetailFragment extends Fragment { 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; - // The product selected from the dropdown - private ProductDTO selectedProduct = null; - - // For debouncing product search - private final Handler searchHandler = new Handler(Looper.getMainLooper()); - private Runnable searchRunnable; - - // Dropdown list - private final List productSuggestions = new ArrayList<>(); - private ArrayAdapter dropdownAdapter; + private List storeList = new ArrayList<>(); + private List productList = new ArrayList<>(); /** * Initializes the view models. @@ -66,6 +57,7 @@ public class InventoryDetailFragment extends Fragment { super.onCreate(savedInstanceState); inventoryViewModel = new ViewModelProvider(this).get(InventoryViewModel.class); productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); + storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); } /** @@ -85,94 +77,64 @@ public class InventoryDetailFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - setupProductSearch(); + loadSpinnersData(); handleArguments(); binding.btnInventoryBack.setOnClickListener(v -> navigateBack()); binding.btnSaveInventory.setOnClickListener(v -> saveInventory()); binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete()); - - // Setup dropdown adapter - dropdownAdapter = new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_dropdown_item_1line, new ArrayList<>()); - binding.etProductSearch.setAdapter(dropdownAdapter); - binding.etProductSearch.setThreshold(1); // start showing after 1 character } @Override public void onDestroyView() { super.onDestroyView(); - if (searchRunnable != null) { - searchHandler.removeCallbacks(searchRunnable); - } binding = null; } /** - * Sets up the product search dropdown. + * Fetches required data for spinners from the backend. */ - private void setupProductSearch() { - binding.etProductSearch.addTextChangedListener(new TextWatcher() { - @Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { - } + private void loadSpinnersData() { + loadStores(); + loadProducts(); + } - @Override public void afterTextChanged(Editable s) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Clear selected product when user is typing again - selectedProduct = null; - binding.tvProductInfo.setVisibility(View.GONE); - - if (searchRunnable != null) - searchHandler.removeCallbacks(searchRunnable); - String query = s.toString().trim(); - if (query.isEmpty()) - return; - - searchRunnable = () -> searchProducts(query); - searchHandler.postDelayed(searchRunnable, 400); - } - }); - - // When user picks an item from the dropdown - binding.etProductSearch.setOnItemClickListener((parent, view, position, id) -> { - if (position < productSuggestions.size()) { - selectedProduct = productSuggestions.get(position); - // Show product details below the search box - binding.tvProductInfo.setText( - "ID: " + selectedProduct.getProdId() - + " • " + selectedProduct.getCategoryName()); - binding.tvProductInfo.setVisibility(View.VISIBLE); + /** + * Loads the list of stores for the spinner. + */ + private void loadStores() { + storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList = resource.data.getContent(); + refreshStoreSpinner(); } }); } + private void refreshStoreSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, storeList, + StoreDTO::getStoreName, "-- Select Store --", + preselectedStoreId, StoreDTO::getStoreId); + } + /** - * Searches for products matching the query from the backend. + * Loads the list of products for the spinner. */ - private void searchProducts(String query) { - if (getView() == null) return; - productViewModel.getAllProducts(query, null, 0, 20, "prodName").observe(getViewLifecycleOwner(), resource -> { + private void loadProducts() { + productViewModel.getAllProducts(null, null, 0, 500, "prodName").observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - productSuggestions.clear(); - productSuggestions.addAll(resource.data.getContent()); - - // Build display strings: "Product Name (ID: X)" - List names = new ArrayList<>(); - for (ProductDTO p : productSuggestions) { - names.add(p.getProdName() + " (ID: " + p.getProdId() + ")"); - } - - dropdownAdapter.clear(); - dropdownAdapter.addAll(names); - dropdownAdapter.notifyDataSetChanged(); - binding.etProductSearch.showDropDown(); + productList = resource.data.getContent(); + refreshProductSpinner(); } }); } + private void refreshProductSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, productList, + ProductDTO::getProdName, "-- Select Product --", + preselectedProductId, ProductDTO::getProdId); + } + /** * Handles fragment arguments to determine if we are in edit or add mode. */ @@ -193,7 +155,6 @@ public class InventoryDetailFragment extends Fragment { isEditing = false; binding.tvInventoryMode.setText("Add Inventory"); binding.tvInventoryId.setVisibility(View.GONE); - binding.tvProductInfo.setVisibility(View.GONE); binding.btnDeleteInventory.setVisibility(View.GONE); binding.btnSaveInventory.setText("Add"); } @@ -207,20 +168,12 @@ public class InventoryDetailFragment extends Fragment { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { InventoryDTO inv = resource.data; - binding.etProductSearch.setText(inv.getProductName()); binding.etQuantity.setText(String.valueOf(inv.getQuantity())); - - if (inv.getProdId() != null) { - binding.tvProductInfo.setText( - "ID: " + inv.getProdId() - + " • " + inv.getCategoryName()); - binding.tvProductInfo.setVisibility(View.VISIBLE); - - selectedProduct = new ProductDTO(); - selectedProduct.setProdId(inv.getProdId()); - selectedProduct.setProdName(inv.getProductName()); - selectedProduct.setCategoryName(inv.getCategoryName()); - } + preselectedStoreId = inv.getStoreId() != null ? inv.getStoreId() : -1; + preselectedProductId = inv.getProdId() != null ? inv.getProdId() : -1; + + refreshStoreSpinner(); + refreshProductSpinner(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -231,9 +184,12 @@ public class InventoryDetailFragment extends Fragment { * Validates input and saves the current inventory item details to the backend. */ private void saveInventory() { - if (selectedProduct == null) { - binding.etProductSearch.setError("Please select a product from the list"); - binding.etProductSearch.requestFocus(); + if (binding.spinnerInventoryStore.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Please select a store", Toast.LENGTH_SHORT).show(); + return; + } + if (binding.spinnerInventoryProduct.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Please select a product", Toast.LENGTH_SHORT).show(); return; } @@ -243,8 +199,10 @@ public class InventoryDetailFragment extends Fragment { } int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim()); + StoreDTO store = storeList.get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); + ProductDTO product = productList.get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); - InventoryRequest request = new InventoryRequest(selectedProduct.getProdId(), quantity); + InventoryDTO request = new InventoryDTO(product.getProdId(), store.getStoreId(), quantity); setButtonsEnabled(false); if (isEditing) { 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 4a30e4cd..eb69bd16 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 @@ -73,19 +73,27 @@ public class PurchaseOrderDetailFragment extends Fragment { PurchaseOrderDTO po = resource.data; binding.tvPODetailId.setText("PO #" + po.getPurchaseOrderId()); binding.tvPODetailSupplier.setText(po.getSupplierName()); + binding.tvPODetailStore.setText(po.getStoreName() != null ? po.getStoreName() : "N/A"); binding.tvPODetailDate.setText(po.getOrderDate()); String status = po.getStatus() != null ? po.getStatus() : ""; binding.tvPODetailStatus.setText(status); - switch (status) { - case "Completed": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#4CAF50")); break; - case "Pending": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#FF9800")); break; - case "Cancelled": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#F44336")); break; + switch (status.toUpperCase()) { + case "RECEIVED": + binding.tvPODetailStatus.setTextColor(Color.parseColor("#4CAF50")); + break; + case "PLACED": + binding.tvPODetailStatus.setTextColor(Color.parseColor("#2196F3")); + break; + case "PENDING": + binding.tvPODetailStatus.setTextColor(Color.parseColor("#FF9800")); + break; + case "CANCELLED": + binding.tvPODetailStatus.setTextColor(Color.parseColor("#F44336")); + break; default: - binding.tvPODetailStatus.setTextColor(Color.parseColor("#9E9E9E")); break; + binding.tvPODetailStatus.setTextColor(Color.parseColor("#9E9E9E")); + break; } } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load order: " + resource.message, Toast.LENGTH_SHORT).show(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java deleted file mode 100644 index ce532696..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.example.petstoremobile.fragments.listfragments.detailfragments; - -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.navigation.fragment.NavHostFragment; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; - -import com.example.petstoremobile.api.SaleApi; -import com.example.petstoremobile.databinding.FragmentRefundDetailBinding; -import com.example.petstoremobile.fragments.listfragments.SaleFragment; -import com.example.petstoremobile.utils.ActivityLogger; -import com.example.petstoremobile.utils.InputValidator; -import com.example.petstoremobile.utils.SpinnerUtils; - -import javax.inject.Inject; - -import dagger.hilt.android.AndroidEntryPoint; - -@AndroidEntryPoint -public class RefundDetailFragment extends Fragment { - - private FragmentRefundDetailBinding binding; - private int saleId; - private SaleFragment saleFragment; - - @Inject SaleApi saleApi; - - public void setSaleFragment(SaleFragment fragment) { - this.saleFragment = fragment; - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - binding = FragmentRefundDetailBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - setupSpinner(); - handleArguments(); - - binding.btnRefundBack.setOnClickListener(v -> goBack()); - binding.btnLoadSale.setOnClickListener(v -> loadSaleDetails()); - binding.btnProcessRefund.setOnClickListener(v -> processRefund()); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - - private void loadSaleDetails() { - String idText = binding.etRefundSaleId.getText().toString().trim(); - if (idText.isEmpty()) { - Toast.makeText(getContext(), "Enter a Sale ID", Toast.LENGTH_SHORT).show(); - return; - } - - try { - int id = Integer.parseInt(idText); - // TODO: Replace with actual API call - GET v1/sales/{id} - // For now show placeholder info - binding.tvSaleInfo.setText("Sale ID: " + id + " loaded. Enter reason and payment method to process refund."); - binding.tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); - } catch (NumberFormatException e) { - Toast.makeText(getContext(), "Invalid Sale ID", Toast.LENGTH_SHORT).show(); - } - } - - private void processRefund() { - if (!InputValidator.isNotEmpty(binding.etRefundSaleId, "Sale ID")) - return; - if (!InputValidator.isNotEmpty(binding.etRefundReason, "Refund Reason")) - return; - - String idText = binding.etRefundSaleId.getText().toString().trim(); - String reason = binding.etRefundReason.getText().toString().trim(); - - try { - int id = Integer.parseInt(idText); - // TODO: Replace with actual API call - POST v1/refunds - ActivityLogger.log(requireContext(), "Processed refund for Sale ID: " + id + " - Reason: " + reason); - Toast.makeText(getContext(), "Refund processed for Sale ID: " + id, Toast.LENGTH_SHORT).show(); - if (saleFragment != null) - saleFragment.reloadSales(); - goBack(); - } catch (NumberFormatException e) { - Toast.makeText(getContext(), "Invalid Sale ID", Toast.LENGTH_SHORT).show(); - } - } - - private void handleArguments() { - if (getArguments() != null && getArguments().containsKey("saleId")) { - saleId = getArguments().getInt("saleId"); - binding.etRefundSaleId.setText(String.valueOf(saleId)); - String info = "Sale Date: " + getArguments().getString("saleDate") - + " | Employee: " + getArguments().getString("employeeName") - + " | Total: $" + String.format("%.2f", getArguments().getDouble("total")) - + " | Payment: " + getArguments().getString("paymentMethod"); - binding.tvSaleInfo.setText(info); - binding.tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); - - // Pre-select payment method - SpinnerUtils.setSelectionByValue(binding.spinnerRefundPayment, getArguments().getString("paymentMethod")); - } - } - - private void goBack() { - NavHostFragment.findNavController(this).popBackStack(); - } - - private void setupSpinner() { - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerRefundPayment, - new String[] { "Cash", "Card", "Debit" }); - } -} 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 new file mode 100644 index 00000000..a53b5402 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java @@ -0,0 +1,510 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +import android.app.AlertDialog; +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.navigation.fragment.NavHostFragment; +import com.example.petstoremobile.R; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.dtos.PageResponse; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import retrofit2.*; + +public class RefundFragment extends Fragment { + + private EditText etSaleId; + private Button btnLoadSale, btnProcessRefund, btnBack; + private TextView tvSaleInfo, tvRefundTotal; + private LinearLayout llOriginalItems, llRefundItems; + private LinearLayout cardOriginalItems, cardRefundItems, cardPayment; + private Spinner spinnerPayment; + + 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 final String[] PAYMENT_METHODS = {"Cash", "Card", "Debit"}; + + // 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) { + View view = inflater.inflate(R.layout.fragment_refund, container, false); + initViews(view); + setupSpinner(); + loadAllSales(); + + // Pre-fill sale ID if passed from SaleFragment + Bundle args = getArguments(); + if (args != null && args.containsKey("saleId")) { + long saleId = args.getLong("saleId"); + etSaleId.setText(String.valueOf(saleId)); + // Auto-load after sales are fetched + } + + btnLoadSale.setOnClickListener(v -> loadSale()); + btnProcessRefund.setOnClickListener(v -> processRefund()); + btnBack.setOnClickListener(v -> navigateBack()); + + return view; + } + + private void initViews(View v) { + etSaleId = v.findViewById(R.id.etRefundSaleId); + btnLoadSale = v.findViewById(R.id.btnLoadSale); + btnProcessRefund= v.findViewById(R.id.btnProcessRefund); + btnBack = v.findViewById(R.id.btnRefundBack); + tvSaleInfo = v.findViewById(R.id.tvSaleInfo); + tvRefundTotal = v.findViewById(R.id.tvRefundTotal); + llOriginalItems = v.findViewById(R.id.llOriginalItems); + llRefundItems = v.findViewById(R.id.llRefundItems); + cardOriginalItems = v.findViewById(R.id.cardOriginalItems); + cardRefundItems = v.findViewById(R.id.cardRefundItems); + cardPayment = v.findViewById(R.id.cardPayment); + spinnerPayment = v.findViewById(R.id.spinnerRefundPayment); + } + + private void setupSpinner() { + spinnerPayment.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, PAYMENT_METHODS)); + } + + private void loadAllSales() { + RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 1000) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (r.isSuccessful() && r.body() != null) { + allSales = r.body().getContent(); + // Auto-load if saleId was pre-filled + Bundle args = getArguments(); + if (args != null && args.containsKey("saleId")) { + loadSale(); + } + } + } + public void onFailure(Call> c, Throwable t) { + Log.e("Refund", "Failed to load sales: " + t.getMessage()); + } + }); + } + + private void loadSale() { + String idStr = etSaleId.getText().toString().trim(); + if (idStr.isEmpty()) { + Toast.makeText(getContext(), "Enter a Sale ID", Toast.LENGTH_SHORT).show(); + return; + } + + long saleId; + try { saleId = Long.parseLong(idStr); } + catch (Exception e) { + Toast.makeText(getContext(), "Invalid Sale ID", Toast.LENGTH_SHORT).show(); + return; + } + + // Find sale in loaded list + SaleDTO found = null; + for (SaleDTO s : allSales) { + if (s.getSaleId() != null && s.getSaleId() == saleId) { + found = s; break; + } + } + + if (found == null) { + Toast.makeText(getContext(), "Sale #" + saleId + " not found", Toast.LENGTH_SHORT).show(); + return; + } + + if (Boolean.TRUE.equals(found.getIsRefund())) { + Toast.makeText(getContext(), "Select an original sale, not a refund record", + Toast.LENGTH_LONG).show(); + return; + } + + currentSale = found; + + // Show sale info + tvSaleInfo.setVisibility(View.VISIBLE); + tvSaleInfo.setText("Sale #" + currentSale.getSaleId() + + " | " + (currentSale.getSaleDate() != null + ? currentSale.getSaleDate().substring(0, 10) : "") + + " | Employee: " + (currentSale.getEmployeeName() != null + ? currentSale.getEmployeeName() : "") + + " | 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())) { + spinnerPayment.setSelection(i); break; + } + } + } + + // 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(); + return; + } + + // Reset refund cart + refundCart.clear(); + + // Show cards + cardOriginalItems.setVisibility(View.VISIBLE); + cardRefundItems.setVisibility(View.VISIBLE); + cardPayment.setVisibility(View.VISIBLE); + 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() { + llOriginalItems.removeAllViews(); + + // Header + addTableHeader(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; } + } + int displayQty = item.quantity - pendingQty; + if (displayQty <= 0) continue; + + LinearLayout row = buildItemRow( + item.productName, + displayQty, + item.unitPrice, + true, // show add button + () -> showQuantityDialog(item) + ); + llOriginalItems.addView(row); + } + } + + private void renderRefundCart() { + llRefundItems.removeAllViews(); + + if (refundCart.isEmpty()) { + TextView empty = new TextView(getContext()); + empty.setText("No items added to refund yet"); + empty.setTextColor(0xFF888780); + empty.setTextSize(13f); + llRefundItems.addView(empty); + return; + } + + addTableHeader(llRefundItems); + + for (RefundItem item : refundCart) { + LinearLayout row = buildItemRow( + item.productName, + item.quantity, + item.unitPrice, + false, // show remove button + () -> { + refundCart.remove(item); + renderOriginalItems(); + renderRefundCart(); + updateRefundTotal(); + } + ); + llRefundItems.addView(row); + } + } + + private void addTableHeader(LinearLayout parent) { + LinearLayout header = new LinearLayout(getContext()); + header.setOrientation(LinearLayout.HORIZONTAL); + header.setPadding(0, 0, 0, 8); + + String[] cols = {"Product", "Qty", "Unit Price", ""}; + float[] weights = {2f, 1f, 1f, 0.8f}; + for (int i = 0; i < cols.length; i++) { + TextView tv = new TextView(getContext()); + tv.setLayoutParams(new LinearLayout.LayoutParams(0, + LinearLayout.LayoutParams.WRAP_CONTENT, weights[i])); + tv.setText(cols[i]); + tv.setTextColor(0xFF888780); + tv.setTextSize(11f); + header.addView(tv); + } + parent.addView(header); + } + + private LinearLayout buildItemRow(String name, int qty, BigDecimal unitPrice, + boolean isAdd, Runnable action) { + LinearLayout row = new LinearLayout(getContext()); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(android.view.Gravity.CENTER_VERTICAL); + row.setPadding(0, 8, 0, 8); + + TextView tvName = new TextView(getContext()); + tvName.setLayoutParams(new LinearLayout.LayoutParams(0, + LinearLayout.LayoutParams.WRAP_CONTENT, 2f)); + tvName.setText(name); + tvName.setTextSize(13f); + tvName.setTextColor(0xFF3d3d3a); + + TextView tvQty = new TextView(getContext()); + tvQty.setLayoutParams(new LinearLayout.LayoutParams(0, + LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvQty.setText(String.valueOf(qty)); + tvQty.setTextSize(13f); + tvQty.setTextColor(0xFF3d3d3a); + + TextView tvPrice = new TextView(getContext()); + tvPrice.setLayoutParams(new LinearLayout.LayoutParams(0, + LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvPrice.setText(unitPrice != null ? "$" + unitPrice : ""); + tvPrice.setTextSize(13f); + tvPrice.setTextColor(0xFF3d3d3a); + + Button btn = new Button(getContext()); + LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams(0, + LinearLayout.LayoutParams.WRAP_CONTENT, 0.8f); + btn.setLayoutParams(btnParams); + btn.setText(isAdd ? "Add" : "Remove"); + btn.setTextSize(11f); + btn.setBackgroundColor(isAdd ? 0xFF1a759f : 0xFFE24B4A); + btn.setTextColor(0xFFFFFFFF); + btn.setPadding(4, 4, 4, 4); + btn.setOnClickListener(v -> action.run()); + + row.addView(tvName); + row.addView(tvQty); + row.addView(tvPrice); + row.addView(btn); + 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 + AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); + builder.setTitle("Refund Quantity"); + builder.setMessage("Product: " + item.productName + + "\nAvailable: " + available); + + EditText input = new EditText(getContext()); + input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); + input.setText(String.valueOf(available)); + input.setSelectAllOnFocus(true); + builder.setView(input); + + builder.setPositiveButton("Add to Refund", (d, w) -> { + String val = input.getText().toString().trim(); + if (val.isEmpty()) return; + int qty; + try { qty = Integer.parseInt(val); } + catch (Exception e) { + Toast.makeText(getContext(), "Invalid quantity", Toast.LENGTH_SHORT).show(); + return; + } + if (qty <= 0) { + 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(); + 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(); + }); + + builder.setNegativeButton("Cancel", null); + builder.show(); + } + + private void updateRefundTotal() { + BigDecimal total = BigDecimal.ZERO; + for (RefundItem item : refundCart) total = total.add(item.getTotal()); + tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP)); + } + + private void processRefund() { + if (currentSale == 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(); + return; + } + + String payment = PAYMENT_METHODS[spinnerPayment.getSelectedItemPosition()]; + + // Confirm dialog + BigDecimal total = BigDecimal.ZERO; + for (RefundItem item : refundCart) total = total.add(item.getTotal()); + final BigDecimal finalTotal = total; + + new AlertDialog.Builder(requireContext()) + .setTitle("Confirm Refund") + .setMessage("Process refund for Sale #" + currentSale.getSaleId() + + "?\nRefund amount: $" + finalTotal.setScale(2, RoundingMode.HALF_UP)) + .setPositiveButton("Yes", (d, w) -> submitRefund(payment)) + .setNegativeButton("No", null) + .show(); + } + + 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()); + + RetrofitClient.getSaleApi(requireContext()).createSale(dto) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { + if (r.isSuccessful() && r.body() != null) { + Toast.makeText(getContext(), + "Refund #" + r.body().getSaleId() + " processed successfully!", + Toast.LENGTH_LONG).show(); + navigateBack(); + } else { + try { + String err = r.errorBody().string(); + Log.e("REFUND", "Error: " + err); + Toast.makeText(getContext(), "Error: " + err, + Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Log.e("REFUND", "Failed to read error"); + } + } + } + public void onFailure(Call c, Throwable t) { + Log.e("REFUND", "Failure: " + t.getMessage()); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void navigateBack() { + NavHostFragment.findNavController(this).popBackStack(); + } +} 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 new file mode 100644 index 00000000..d71b2686 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java @@ -0,0 +1,368 @@ +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; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; +import com.example.petstoremobile.R; +import com.example.petstoremobile.api.*; +import com.example.petstoremobile.dtos.*; +import java.math.BigDecimal; +import java.util.*; +import retrofit2.*; + +public class SaleDetailFragment extends Fragment { + + private TextView tvMode, tvSaleDetailId, tvTotal; + private Spinner spinnerStore, spinnerCustomer, spinnerPayment, spinnerProduct; + private EditText etQuantity; + private Button btnAddItem, btnSave, btnBack, btnRefund; + private LinearLayout llItems; + + 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 final String[] PAYMENT_METHODS = { "Cash", "Credit Card", "Debit Card", "Online" }; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_sale_detail, container, false); + initViews(view); + handleArguments(); + + if (!viewOnly) { + loadData(); + setupAddItem(); + } + + btnBack.setOnClickListener(v -> navigateBack()); + btnSave.setOnClickListener(v -> saveSale()); + btnRefund.setOnClickListener(v -> showRefundDialog()); + + return view; + } + + private void initViews(View v) { + tvMode = v.findViewById(R.id.tvSaleMode); + tvSaleDetailId = v.findViewById(R.id.tvSaleDetailId); + tvTotal = v.findViewById(R.id.tvSaleDetailTotal); + spinnerStore = v.findViewById(R.id.spinnerSaleStore); + spinnerCustomer = v.findViewById(R.id.spinnerSaleCustomer); + spinnerPayment = v.findViewById(R.id.spinnerPaymentMethod); + spinnerProduct = v.findViewById(R.id.spinnerSaleProduct); + etQuantity = v.findViewById(R.id.etSaleQuantity); + btnAddItem = v.findViewById(R.id.btnAddItem); + btnSave = v.findViewById(R.id.btnSaveSale); + btnBack = v.findViewById(R.id.btnSaleBack); + btnRefund = v.findViewById(R.id.btnRefundSale); + llItems = v.findViewById(R.id.llSaleItems); + + spinnerPayment.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, PAYMENT_METHODS)); + } + + private void handleArguments() { + Bundle a = getArguments(); + if (a != null && a.containsKey("saleId")) { + saleId = a.getLong("saleId"); + viewOnly = a.getBoolean("viewOnly", false); + tvMode.setText("Sale #" + saleId); + tvSaleDetailId.setText("ID: " + saleId); + + // Show refund button for existing non-refund sales + if (!a.getBoolean("isRefund", false)) { + btnRefund.setVisibility(View.VISIBLE); + } + + // Hide save and input controls for view only + if (viewOnly) { + btnSave.setVisibility(View.GONE); + spinnerStore.setEnabled(false); + spinnerCustomer.setEnabled(false); + spinnerPayment.setEnabled(false); + spinnerProduct.setEnabled(false); + etQuantity.setEnabled(false); + btnAddItem.setEnabled(false); + } + + // Load sale details + loadSaleDetails(); + } else { + tvMode.setText("New Sale"); + tvSaleDetailId.setVisibility(View.GONE); + btnRefund.setVisibility(View.GONE); + } + } + + private void loadData() { + loadStores(); + loadCustomers(); + loadProducts(); + } + + private void loadStores() { + // Hardcoded since store endpoint is admin only + storeList = new ArrayList<>(); + storeList.add(new StoreDTO(1L, "Downtown Branch")); + List names = new ArrayList<>(); + names.add("-- Select Store --"); + names.add("Downtown Branch"); + spinnerStore.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + } + + private void loadCustomers() { + RetrofitClient.getCustomerApi(requireContext()).getAllCustomers(0, 200) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (r.isSuccessful() && r.body() != null) { + customerList = r.body().getContent(); + List names = new ArrayList<>(); + names.add("-- No Customer --"); + for (CustomerDTO cu : customerList) + names.add(cu.getFirstName() + " " + cu.getLastName()); + spinnerCustomer.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + } + } + + public void onFailure(Call> c, Throwable t) { + Log.e("SaleDetail", "Customer load failed: " + t.getMessage()); + } + }); + } + + private void loadProducts() { + RetrofitClient.getProductApi(requireContext()).getAllProducts(null, null, 0, 200, null) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (r.isSuccessful() && r.body() != null) { + productList = r.body().getContent(); + List names = new ArrayList<>(); + names.add("-- Select Product --"); + for (ProductDTO p : productList) + names.add(p.getProdName()); + spinnerProduct.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + } + } + + public void onFailure(Call> c, Throwable t) { + Log.e("SaleDetail", "Product load failed: " + t.getMessage()); + } + }); + } + + private void loadSaleDetails() { + RetrofitClient.getSaleApi(requireContext()).getSaleById(saleId) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { + if (r.isSuccessful() && r.body() != null) { + SaleDTO sale = r.body(); + tvTotal.setText("Total: $" + sale.getTotalAmount()); + // Display items + if (sale.getItems() != null) { + llItems.removeAllViews(); + for (SaleDTO.SaleItemDTO item : sale.getItems()) { + addItemRow(item.getProductName(), + Math.abs(item.getQuantity()), + item.getUnitPrice()); + } + } + } + } + + public void onFailure(Call c, Throwable t) { + Log.e("SaleDetail", "Load failed: " + t.getMessage()); + } + }); + } + + private void setupAddItem() { + btnAddItem.setOnClickListener(v -> { + if (spinnerProduct.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); + return; + } + String qtyStr = etQuantity.getText().toString().trim(); + if (qtyStr.isEmpty()) { + etQuantity.setError("Enter quantity"); + return; + } + int qty; + try { + qty = Integer.parseInt(qtyStr); + } catch (Exception e) { + etQuantity.setError("Invalid quantity"); + return; + } + + ProductDTO product = productList.get(spinnerProduct.getSelectedItemPosition() - 1); + + // Check if product already in cart + for (SaleDTO.SaleItemDTO existing : cartItems) { + 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(); + etQuantity.setText(""); + }); + } + + private void addItemRow(String name, int qty, BigDecimal price) { + LinearLayout row = new LinearLayout(getContext()); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setPadding(0, 8, 0, 8); + + TextView tvName = new TextView(getContext()); + 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.setText("x" + qty); + + TextView tvPrice = new TextView(getContext()); + tvPrice.setLayoutParams(new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvPrice.setText(price != null ? "$" + price : ""); + + row.addView(tvName); + row.addView(tvQty); + row.addView(tvPrice); + llItems.addView(row); + } + + private void updateTotal() { + BigDecimal total = BigDecimal.ZERO; + int productIdx = 0; + for (SaleDTO.SaleItemDTO item : cartItems) { + if (productIdx < productList.size()) { + for (ProductDTO p : productList) { + if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) { + total = total.add(p.getProdPrice() + .multiply(BigDecimal.valueOf(item.getQuantity()))); + break; + } + } + } + } + tvTotal.setText("Total: $" + total); + } + + private void saveSale() { + if (spinnerStore.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); + return; + } + if (cartItems.isEmpty()) { + Toast.makeText(getContext(), "Add at least one item", Toast.LENGTH_SHORT).show(); + return; + } + + StoreDTO store = storeList.get(0); // only one store + String payment = PAYMENT_METHODS[spinnerPayment.getSelectedItemPosition()]; + + // Optional customer + Long customerId = null; + if (spinnerCustomer.getSelectedItemPosition() > 0) { + customerId = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1) + .getCustomerId(); + } + + SaleDTO dto = new SaleDTO( + store.getStoreId(), + payment, + cartItems, + false, + null, + customerId); + + Log.d("SALE_SAVE", "storeId=" + store.getStoreId() + + " payment=" + payment + + " items=" + cartItems.size() + + " customerId=" + customerId); + + RetrofitClient.getSaleApi(requireContext()).createSale(dto) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { + if (r.isSuccessful()) { + Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + try { + String err = r.errorBody().string(); + Log.e("SALE_SAVE", "Error: " + err); + Toast.makeText(getContext(), "Error " + r.code() + ": " + err, + Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Log.e("SALE_SAVE", "Failed to read error"); + } + } + } + + public void onFailure(Call c, Throwable t) { + Log.e("SALE_SAVE", "Failure: " + t.getMessage()); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void showRefundDialog() { + Bundle args = new Bundle(); + args.putLong("saleId", saleId); + NavHostFragment.findNavController(this).navigate(R.id.nav_refund, args); + } + + private void submitRefund() { + RefundDTO refundDTO = new RefundDTO(saleId, "Refund requested from mobile app"); + RetrofitClient.getRefundApi(requireContext()).createRefund(refundDTO) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { + if (r.isSuccessful()) { + Toast.makeText(getContext(), "Refund request submitted!", + Toast.LENGTH_SHORT).show(); + btnRefund.setVisibility(View.GONE); + } else { + try { + String err = r.errorBody().string(); + Log.e("REFUND", "Error: " + err); + Toast.makeText(getContext(), "Error: " + err, + Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Log.e("REFUND", "Failed to read error"); + } + } + } + + public void onFailure(Call c, Throwable t) { + Log.e("REFUND", "Failure: " + t.getMessage()); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void navigateBack() { + NavHostFragment.findNavController(this).popBackStack(); + } +} 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 new file mode 100644 index 00000000..2750db72 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java @@ -0,0 +1,199 @@ +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; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; +import com.example.petstoremobile.R; +import com.example.petstoremobile.api.EmployeeApi; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.EmployeeDTO; +import retrofit2.*; + +public class StaffDetailFragment extends Fragment { + + private TextView tvMode, tvStaffId; + private EditText etUsername, etPassword, etFirstName, etLastName, etEmail, etPhone; + private Spinner spinnerRole, spinnerStatus; + private Button btnSave, btnDelete, btnBack; + + private long employeeId = -1; + private boolean isEditing = false; + + private final String[] ROLES = {"STAFF", "ADMIN"}; + private final String[] STATUSES = {"Active", "Inactive"}; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_staff_detail, container, false); + initViews(view); + setupSpinners(); + handleArguments(); + + btnBack.setOnClickListener(v -> navigateBack()); + btnSave.setOnClickListener(v -> save()); + btnDelete.setOnClickListener(v -> confirmDelete()); + return view; + } + + private void initViews(View v) { + tvMode = v.findViewById(R.id.tvStaffMode); + tvStaffId = v.findViewById(R.id.tvStaffId); + etUsername = v.findViewById(R.id.etStaffUsername); + etPassword = v.findViewById(R.id.etStaffPassword); + etFirstName = v.findViewById(R.id.etStaffFirstName); + etLastName = v.findViewById(R.id.etStaffLastName); + etEmail = v.findViewById(R.id.etStaffEmail); + etPhone = v.findViewById(R.id.etStaffPhone); + spinnerRole = v.findViewById(R.id.spinnerStaffRole); + spinnerStatus = v.findViewById(R.id.spinnerStaffStatus); + btnSave = v.findViewById(R.id.btnSaveStaff); + btnDelete = v.findViewById(R.id.btnDeleteStaff); + btnBack = v.findViewById(R.id.btnStaffBack); + } + + private void setupSpinners() { + spinnerRole.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, ROLES)); + spinnerStatus.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, STATUSES)); + } + + private void handleArguments() { + Bundle a = getArguments(); + if (a != null && a.getBoolean("isEditing", false)) { + isEditing = true; + employeeId = a.getLong("employeeId", -1); + + tvMode.setText("Edit Staff Account"); + tvStaffId.setText("ID: " + employeeId); + tvStaffId.setVisibility(View.VISIBLE); + etUsername.setText(a.getString("username", "")); + etFirstName.setText(a.getString("firstName", "")); + etLastName.setText(a.getString("lastName", "")); + etEmail.setText(a.getString("email", "")); // ← was showing fullName + etPhone.setText(a.getString("phone", "")); + btnDelete.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)) { + spinnerRole.setSelection(i); + break; + } + } + + // Pre-fill status + boolean active = a.getBoolean("active", true); + spinnerStatus.setSelection(active ? 0 : 1); + + } else { + isEditing = false; + employeeId = -1; + tvMode.setText("Add Staff Account"); + btnDelete.setVisibility(View.GONE); + tvStaffId.setVisibility(View.GONE); + } + } + + + private void save() { + String username = etUsername.getText() != null ? etUsername.getText().toString().trim() : ""; + String password = etPassword.getText() != null ? etPassword.getText().toString().trim() : ""; + String firstName = etFirstName.getText() != null ? etFirstName.getText().toString().trim() : ""; + String lastName = etLastName.getText() != null ? etLastName.getText().toString().trim() : ""; + String email = etEmail.getText() != null ? etEmail.getText().toString().trim() : ""; + String phone = etPhone.getText() != null ? etPhone.getText().toString().trim() : ""; + String role = ROLES[spinnerRole.getSelectedItemPosition()]; + boolean active = spinnerStatus.getSelectedItemPosition() == 0; + + // Validation + if (username.isEmpty()) { etUsername.setError("Required"); return; } + if (!isEditing && password.isEmpty()) { + etPassword.setError("Required for new account"); return; + } + if (!isEditing && password.length() < 6) { + etPassword.setError("At least 6 characters"); return; + } + if (firstName.isEmpty()) { etFirstName.setError("Required"); return; } + if (lastName.isEmpty()) { etLastName.setError("Required"); return; } + if (email.isEmpty()) { etEmail.setError("Required"); return; } + if (phone.isEmpty()) { etPhone.setError("Required"); return; } + + EmployeeDTO dto = new EmployeeDTO( + username, + password.isEmpty() ? null : password, + firstName, + lastName, + email, + phone, + role, + active + ); + + Log.d("STAFF_SAVE", "isEditing=" + isEditing + + " employeeId=" + employeeId + + " username=" + username); + + EmployeeApi api = RetrofitClient.getEmployeeApi(requireContext()); + if (isEditing && employeeId > 0) { + api.updateEmployee(employeeId, dto).enqueue(simpleCallback("Updated successfully")); + } else { + api.createEmployee(dto).enqueue(simpleCallback("Staff account created")); + } + } + + private Callback simpleCallback(String msg) { + return new Callback<>() { + public void onResponse(Call c, Response r) { + Log.d("STAFF_SAVE", "Response: " + r.code()); + if (r.isSuccessful()) { + Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + try { + String err = r.errorBody().string(); + Log.e("STAFF_SAVE", "Error: " + err); + Toast.makeText(getContext(), "Error " + r.code() + ": " + err, + Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Log.e("STAFF_SAVE", "Failed to read error"); + } + } + } + public void onFailure(Call c, Throwable t) { + Log.e("STAFF_SAVE", "Failure: " + t.getMessage()); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }; + } + + private void confirmDelete() { + new AlertDialog.Builder(requireContext()) + .setTitle("Delete Staff Account?") + .setMessage("This will permanently delete this staff account.") + .setPositiveButton("Yes", (d, w) -> + RetrofitClient.getEmployeeApi(requireContext()) + .deleteEmployee(employeeId) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { + navigateBack(); + } + public void onFailure(Call c, Throwable t) { + Toast.makeText(getContext(), "Delete failed", + Toast.LENGTH_SHORT).show(); + } + })) + .setNegativeButton("No", null).show(); + } + + private void navigateBack() { + NavHostFragment.findNavController(this).popBackStack(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java index 0e73b706..f09f85b4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.AdoptionApi; import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; @@ -23,8 +24,8 @@ public class AdoptionRepository extends BaseRepository { /** * Retrieves a paginated list of all adoptions from the API. */ - public LiveData>> getAllAdoptions(int page, int size) { - return executeCall(adoptionApi.getAllAdoptions(page, size)); + public LiveData>> getAllAdoptions(int page, int size, String query, String status, Long storeId, String date, Long employeeId) { + return executeCall(adoptionApi.getAllAdoptions(page, size, query, status, storeId, date, employeeId)); } /** @@ -54,4 +55,11 @@ public class AdoptionRepository extends BaseRepository { public LiveData> deleteAdoption(Long id) { return executeCall(adoptionApi.deleteAdoption(id)); } + + /** + * Sends a request to the API to delete multiple adoption records. + */ + public LiveData> bulkDeleteAdoptions(BulkDeleteRequest request) { + return executeCall(adoptionApi.bulkDeleteAdoptions(request)); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java index 1c85e91a..85083a25 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.AppointmentApi; import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; @@ -54,4 +55,11 @@ public class AppointmentRepository extends BaseRepository { public LiveData> deleteAppointment(Long id) { return executeCall(appointmentApi.deleteAppointment(id)); } + + /** + * Sends a request to the API to delete multiple appointment records. + */ + public LiveData> bulkDeleteAppointments(BulkDeleteRequest request) { + return executeCall(appointmentApi.bulkDeleteAppointments(request)); + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/EmployeeRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/EmployeeRepository.java new file mode 100644 index 00000000..b6ddcc9c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/EmployeeRepository.java @@ -0,0 +1,42 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; + +import com.example.petstoremobile.api.EmployeeApi; +import com.example.petstoremobile.dtos.EmployeeDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class EmployeeRepository extends BaseRepository { + private final EmployeeApi employeeApi; + + @Inject + public EmployeeRepository(EmployeeApi employeeApi) { + super("EmployeeRepository"); + this.employeeApi = employeeApi; + } + + public LiveData>> getAllEmployees(int page, int size) { + return executeCall(employeeApi.getAllEmployees(page, size)); + } + + public LiveData> getEmployeeById(Long id) { + return executeCall(employeeApi.getEmployeeById(id)); + } + + public LiveData> createEmployee(EmployeeDTO dto) { + return executeCall(employeeApi.createEmployee(dto)); + } + + public LiveData> updateEmployee(Long id, EmployeeDTO dto) { + return executeCall(employeeApi.updateEmployee(id, dto)); + } + + public LiveData> deleteEmployee(Long id) { + return executeCall(employeeApi.deleteEmployee(id)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java index 364dc20d..94526d25 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java @@ -5,7 +5,6 @@ import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.InventoryApi; import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.InventoryDTO; -import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; @@ -39,11 +38,11 @@ public class InventoryRepository extends BaseRepository { /** * Sends a request to the API to create a new inventory record. */ - public LiveData> createInventory(InventoryRequest request) { + public LiveData> createInventory(InventoryDTO request) { return executeCall(inventoryApi.createInventory(request)); } - public LiveData> updateInventory(Long id, InventoryRequest request) { + public LiveData> updateInventory(Long id, InventoryDTO request) { return executeCall(inventoryApi.updateInventory(id, request)); } 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/repositories/ProductSupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java index e5c135a5..9b2f8df3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java @@ -3,6 +3,7 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.ProductSupplierApi; +import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.utils.Resource; @@ -54,4 +55,8 @@ public class ProductSupplierRepository extends BaseRepository { public LiveData> deleteProductSupplier(Long productId, Long supplierId) { return executeCall(api.deleteProductSupplier(productId, supplierId)); } + + public LiveData> bulkDeleteProductSuppliers(BulkDeleteRequest request) { + return executeCall(api.bulkDeleteProductSuppliers(request)); + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java index b55f9a33..dd9bd637 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java @@ -12,25 +12,25 @@ import javax.inject.Singleton; @Singleton public class PurchaseOrderRepository extends BaseRepository { - private final PurchaseOrderApi api; + private final PurchaseOrderApi purchaseOrderApi; @Inject - public PurchaseOrderRepository(PurchaseOrderApi api) { - super("PurchaseOrderRepo"); - this.api = api; + public PurchaseOrderRepository(PurchaseOrderApi purchaseOrderApi) { + super("PurchaseOrderRepository"); + this.purchaseOrderApi = purchaseOrderApi; } /** * Retrieves a paginated list of all purchase orders from the API. */ public LiveData>> getAllPurchaseOrders(int page, int size, String query, Long storeId, String sort) { - return executeCall(api.getAllPurchaseOrders(page, size, query, storeId, sort)); + return executeCall(purchaseOrderApi.getAllPurchaseOrders(page, size, query, storeId, sort)); } /** * Retrieves a specific purchase order by its ID from the API. */ public LiveData> getPurchaseOrderById(Long id) { - return executeCall(api.getPurchaseOrderById(id)); + return executeCall(purchaseOrderApi.getPurchaseOrderById(id)); } -} +} \ No newline at end of file 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 new file mode 100644 index 00000000..4068c472 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java @@ -0,0 +1,34 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; + +import com.example.petstoremobile.api.SaleApi; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class SaleRepository extends BaseRepository { + private final SaleApi saleApi; + + @Inject + public SaleRepository(SaleApi saleApi) { + super("SaleRepository"); + this.saleApi = saleApi; + } + + public LiveData>> getAllSales(int page, int size) { + return executeCall(saleApi.getAllSales(page, size)); + } + + public LiveData> getSaleById(Long id) { + return executeCall(saleApi.getSaleById(id)); + } + + public LiveData> createSale(SaleDTO sale) { + return executeCall(saleApi.createSale(sale)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java index 7787c36a..bd5f3ebc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java @@ -3,6 +3,7 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.ServiceApi; +import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.Resource; @@ -54,4 +55,11 @@ public class ServiceRepository extends BaseRepository { public LiveData> deleteService(Long id) { return executeCall(serviceApi.deleteService(id)); } + + /** + * Sends a request to the API to delete multiple services. + */ + public LiveData> bulkDeleteServices(BulkDeleteRequest request) { + return executeCall(serviceApi.bulkDeleteServices(request)); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java index e4eb4c0c..7aef86a2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java @@ -3,6 +3,7 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.SupplierApi; +import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.Resource; @@ -54,4 +55,11 @@ public class SupplierRepository extends BaseRepository { public LiveData> deleteSupplier(Long id) { return executeCall(supplierApi.deleteSupplier(id)); } + + /** + * Sends a request to the API to delete multiple supplier records. + */ + public LiveData> bulkDeleteSuppliers(BulkDeleteRequest request) { + return executeCall(supplierApi.bulkDeleteSuppliers(request)); + } } 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..fe67282b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/BulkDeleteHandler.java @@ -0,0 +1,109 @@ +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. + * Now supports String keys to accommodate both simple and composite keys. + */ +public class BulkDeleteHandler { + + /** + * Interface that adapters must implement to support bulk selection. + */ + public interface SelectableAdapter { + List getSelectedKeys(); + void clearSelection(); + } + + /** + * Functional interface for the API call execution. + */ + public interface BulkDeleteOperation { + LiveData> execute(List keys); + } + + 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 keys = adapter.getSelectedKeys(); + if (keys.isEmpty()) return; + + DialogUtils.showBulkDeleteConfirmDialog(fragment.requireContext(), keys.size(), () -> performDelete(keys)); + } + + /** + * Executes the deletion via the provided operation. + */ + private void performDelete(List keys) { + operation.execute(keys).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(), keys.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..197cb557 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SelectionHelper.java @@ -0,0 +1,68 @@ +package com.example.petstoremobile.utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to manage selection state in Adapters for bulk operations. + * Uses String keys to support both simple Long IDs and composite keys (e.g., "id1-id2"). + */ +public class SelectionHelper { + + private final List selectedKeys = 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(String key) { + if (key == null) return; + + if (selectedKeys.contains(key)) { + selectedKeys.remove(key); + } else { + selectedKeys.add(key); + } + + listener.onSelectionChanged(selectedKeys.size()); + + if (selectedKeys.isEmpty() && selectionMode) { + selectionMode = false; + listener.onSelectionModeToggle(false); + } + } + + public void startSelection(String key) { + if (key == null) return; + selectionMode = true; + selectedKeys.add(key); + listener.onSelectionChanged(selectedKeys.size()); + listener.onSelectionModeToggle(true); + } + + public boolean isSelected(String key) { + return selectedKeys.contains(key); + } + + public boolean isInSelectionMode() { + return selectionMode; + } + + public List getSelectedKeys() { + return new ArrayList<>(selectedKeys); + } + + public void clearSelection() { + selectedKeys.clear(); + selectionMode = false; + listener.onSelectionChanged(0); + listener.onSelectionModeToggle(false); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java index 0041a0b2..b0aae8b8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java @@ -1,6 +1,8 @@ package com.example.petstoremobile.utils; import android.content.Context; +import android.view.View; +import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Spinner; @@ -70,6 +72,30 @@ public class SpinnerUtils { } } + /** + * Sets up a simple string spinner for filtering with a callback. + */ + public static void setupStringFilterSpinner(Context context, Spinner spinner, String[] items, Runnable onSelectionChanged) { + WhiteTextArrayAdapter adapter = new WhiteTextArrayAdapter<>(context, + android.R.layout.simple_spinner_item, items); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + setupFilterSpinner(spinner, onSelectionChanged); + } + + /** + * Attaches an item selected listener to a spinner that triggers a callback. + */ + public static void setupFilterSpinner(Spinner spinner, Runnable onSelectionChanged) { + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + onSelectionChanged.run(); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + /** * Sets the selection of a spinner based on a string value. */ 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 index 039cef30..12eb9779 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java @@ -4,10 +4,13 @@ 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; @@ -22,10 +25,10 @@ public class AdoptionViewModel extends ViewModel { } /** - * Fetches a paginated list of all adoptions. + * Fetches a paginated list of all adoptions with filters. */ - public LiveData>> getAllAdoptions(int page, int size) { - return repository.getAllAdoptions(page, size); + 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); } /** @@ -55,4 +58,11 @@ public class AdoptionViewModel extends ViewModel { 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 index 913d7ab2..69f24c95 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java @@ -4,10 +4,13 @@ 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.repositories.AppointmentRepository; import com.example.petstoremobile.utils.Resource; +import java.util.List; + import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; @@ -55,4 +58,11 @@ public class AppointmentViewModel extends ViewModel { public LiveData> deleteAppointment(Long id) { return repository.deleteAppointment(id); } + + /** + * Deletes multiple appointment records. + */ + public LiveData> bulkDeleteAppointments(List ids) { + return repository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); + } } \ 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 new file mode 100644 index 00000000..5454269e --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/EmployeeViewModel.java @@ -0,0 +1,43 @@ +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 index 6facbb2f..c7ccc070 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java @@ -6,7 +6,6 @@ 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.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.repositories.CategoryRepository; @@ -50,14 +49,14 @@ public class InventoryViewModel extends ViewModel { /** * Creates a new inventory record. */ - public LiveData> createInventory(InventoryRequest request) { + public LiveData> createInventory(InventoryDTO request) { return inventoryRepository.createInventory(request); } /** * Updates an existing inventory record by ID. */ - public LiveData> updateInventory(Long id, InventoryRequest request) { + public LiveData> updateInventory(Long id, InventoryDTO request) { return inventoryRepository.updateInventory(id, request); } @@ -71,7 +70,7 @@ public class InventoryViewModel extends ViewModel { /** * Deletes multiple inventory records in a single request. */ - public LiveData> bulkDeleteInventory(List ids) { + public LiveData> bulkDeleteInventory(List ids) { return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids)); } 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..c75926a7 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/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java index 95613929..f4302225 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.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.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; @@ -48,4 +51,8 @@ public class ProductSupplierViewModel extends ViewModel { 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/SaleViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java new file mode 100644 index 00000000..aca186f6 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleViewModel.java @@ -0,0 +1,35 @@ +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) { + return saleRepository.getAllSales(page, size); + } + + 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 index 142ac85b..ebd5c3b6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.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.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; @@ -55,4 +58,11 @@ public class ServiceViewModel extends ViewModel { 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/SupplierViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java index a89426de..1486a562 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.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.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; @@ -55,4 +58,11 @@ public class SupplierViewModel extends ViewModel { 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/res/layout/fragment_adoption.xml b/android/app/src/main/res/layout/fragment_adoption.xml index 5bc95c38..f222f6e7 100644 --- a/android/app/src/main/res/layout/fragment_adoption.xml +++ b/android/app/src/main/res/layout/fragment_adoption.xml @@ -12,6 +12,7 @@ android:orientation="vertical"> + android:textStyle="bold" + android:layout_marginStart="8dp"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +