From 7e832a139f7348c1671485d39226219625443099 Mon Sep 17 00:00:00 2001 From: Nikitha Date: Sun, 29 Mar 2026 16:20:38 -0600 Subject: [PATCH 01/35] Appointments validation, time slots for booking appointment , date, store. connected to service Type, pet name, customer name. Loads from database --- .../adapters/AppointmentAdapter.java | 46 +- .../petstoremobile/api/AppointmentApi.java | 33 ++ .../petstoremobile/dtos/AppointmentDTO.java | 120 ++++ .../petstoremobile/dtos/CustomerDTO.java | 43 +- .../example/petstoremobile/dtos/StoreDTO.java | 25 + .../fragments/ListFragment.java | 32 +- .../listfragments/AppointmentFragment.java | 204 +++++-- .../AppointmentDetailFragment.java | 529 +++++++++++++----- 8 files changed, 786 insertions(+), 246 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/StoreDTO.java 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 7924c12b..9960b5b6 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 @@ -1,7 +1,5 @@ package com.example.petstoremobile.adapters; - - import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; @@ -10,26 +8,24 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; -import com.example.petstoremobile.models.Appointment; +import com.example.petstoremobile.dtos.AppointmentDTO; import java.util.List; public class AppointmentAdapter extends RecyclerView.Adapter { - private List appointmentList; + private List appointmentList; private OnAppointmentClickListener appointmentClickListener; - // Interface for appointment click on recycler view public interface OnAppointmentClickListener { void onAppointmentClick(int position); } - // Constructor - public AppointmentAdapter(List appointmentList, OnAppointmentClickListener appointmentClickListener) { + public AppointmentAdapter(List appointmentList, + OnAppointmentClickListener appointmentClickListener) { this.appointmentList = appointmentList; this.appointmentClickListener = appointmentClickListener; } - // Get the controls of each row in recycler view public static class AppointmentViewHolder extends RecyclerView.ViewHolder { TextView tvCustomerName, tvPetName, tvServiceType, tvDateTime, tvAppointmentStatus; @@ -43,7 +39,6 @@ public class AppointmentAdapter extends RecyclerView.Adapter appointmentClickListener.onAppointmentClick(position)); } 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 new file mode 100644 index 00000000..5d7044cf --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java @@ -0,0 +1,33 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.PageResponse; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface AppointmentApi { + + @GET("api/v1/appointments") + Call> getAllAppointments( + @Query("page") int page, + @Query("size") int size); + + @GET("api/v1/appointments/{id}") + Call getAppointmentById(@Path("id") Long id); + + @POST("api/v1/appointments") + Call createAppointment(@Body AppointmentDTO appointment); + + @PUT("api/v1/appointments/{id}") + Call updateAppointment(@Path("id") Long id, @Body AppointmentDTO appointment); + + @DELETE("api/v1/appointments/{id}") + Call deleteAppointment(@Path("id") Long id); +} \ No newline at end of file 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 new file mode 100644 index 00000000..4c7a91b7 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java @@ -0,0 +1,120 @@ +package com.example.petstoremobile.dtos; + +import java.math.BigDecimal; +import java.util.List; + +public class AppointmentDTO { + // Response fields (from server) + private Long appointmentId; + private Long customerId; + private String customerName; + private Long storeId; + private String storeName; + private Long serviceId; + private String serviceName; + private String appointmentDate; + private String appointmentTime; + private String appointmentStatus; + private List petNames; + private List petIds; + private String createdAt; + private String updatedAt; + + // Constructor for CREATE/UPDATE request body + // Matches AppointmentRequest exactly + public AppointmentDTO(Long customerId, Long storeId, Long serviceId, + String appointmentDate, String appointmentTime, + String appointmentStatus, List petIds) { + this.customerId = customerId; + this.storeId = storeId; + this.serviceId = serviceId; + this.appointmentDate = appointmentDate; + this.appointmentTime = appointmentTime; + this.appointmentStatus = appointmentStatus; + this.petIds = petIds; + } + + // Getters + public Long getAppointmentId() { + return appointmentId; + } + + public Long getCustomerId() { + return customerId; + } + + public String getCustomerName() { + return customerName; + } + + public Long getStoreId() { + return storeId; + } + + public String getStoreName() { + return storeName; + } + + public Long getServiceId() { + return serviceId; + } + + public String getServiceName() { + return serviceName; + } + + public String getAppointmentDate() { + return appointmentDate; + } + + public String getAppointmentTime() { + return appointmentTime; + } + + public String getAppointmentStatus() { + return appointmentStatus; + } + + public List getPetNames() { + return petNames; + } + + public List getPetIds() { + return petIds; + } + + public String getCreatedAt() { + return createdAt; + } + + public String getUpdatedAt() { + return updatedAt; + } + + // Convenience getters for adapter/list display + public String getPetName() { + return (petNames != null && !petNames.isEmpty()) ? petNames.get(0) : ""; + } + + public Long getPetID() { + return (petIds != null && !petIds.isEmpty()) ? petIds.get(0) : null; + } + + public Long getPetId() { + return getPetID(); + } + + // Keep old name so adapter doesn't break + public String getServiceType() { + return serviceName; + } + + public Long getServiceID() { + return serviceId; + } + + // Status alias + public String getStatus() { + return appointmentStatus; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java index 1a135a6d..178b0033 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java @@ -1,59 +1,38 @@ package com.example.petstoremobile.dtos; -import com.google.gson.annotations.SerializedName; - public class CustomerDTO { - @SerializedName("customerId") private Long customerId; - private String firstName; private String lastName; private String email; - private String phone; - - public CustomerDTO() {} + private String createdAt; + private String updatedAt; public Long getCustomerId() { return customerId; } - public void setCustomerId(Long customerId) { - this.customerId = customerId; - } - public String getFirstName() { return firstName; } - public void setFirstName(String firstName) { - this.firstName = firstName; - } - public String getLastName() { return lastName; } - public void setLastName(String lastName) { - this.lastName = lastName; - } - public String getEmail() { return email; } - public void setEmail(String email) { - this.email = email; - } - - public String getPhone() { - return phone; - } - - public void setPhone(String phone) { - this.phone = phone; - } - public String getFullName() { - return (firstName != null ? firstName : "") + " " + (lastName != null ? lastName : ""); + return firstName + " " + lastName; + } + + 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/StoreDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/StoreDTO.java new file mode 100644 index 00000000..da66f046 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/StoreDTO.java @@ -0,0 +1,25 @@ +package com.example.petstoremobile.dtos; + +public class StoreDTO { + private Long storeId; + private String storeName; + private String address; + private String phone; + private String email; + private String createdAt; + private String updatedAt; + + // Constructor for hardcoded fallback + public StoreDTO(Long storeId, String storeName) { + this.storeId = storeId; + this.storeName = storeName; + } + + public Long getStoreId() { return storeId; } + public String getStoreName() { return storeName; } + public String getAddress() { return address; } + public String getPhone() { return phone; } + public String getEmail() { return email; } + public String getCreatedAt() { return createdAt; } + public String getUpdatedAt() { return updatedAt; } +} 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 cd07df63..dee6ca80 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 @@ -21,6 +21,9 @@ import com.example.petstoremobile.fragments.listfragments.AdoptionFragment; import com.example.petstoremobile.fragments.listfragments.AppointmentFragment; import com.example.petstoremobile.fragments.listfragments.InventoryFragment; import com.example.petstoremobile.fragments.listfragments.ProductFragment; +import com.example.petstoremobile.fragments.listfragments.ProductSupplierFragment; +import com.example.petstoremobile.fragments.listfragments.PurchaseOrderFragment; +import com.example.petstoremobile.fragments.listfragments.SaleFragment; //The Fragment for the displaying the list of entities to be viewed public class ListFragment extends Fragment { @@ -31,7 +34,7 @@ public class ListFragment extends Fragment { // Adoptions, Appointments, Inventory, Products - private LinearLayout drawerAdoptions, drawerAppointments, drawerInventory, drawerProducts; + private LinearLayout drawerAdoptions, drawerAppointments, drawerInventory, drawerProducts, drawerProductSupplier, drawerPurchaseOrderView, drawerSale; @Override @@ -48,6 +51,10 @@ public class ListFragment extends Fragment { drawerAppointments = view.findViewById(R.id.drawerAppointments); drawerInventory = view.findViewById(R.id.drawerInventory); drawerProducts = view.findViewById(R.id.drawerProducts); + drawerProductSupplier=view.findViewById(R.id.drawerProductSupplier); + drawerSale=view.findViewById(R.id.drawerSale); + drawerPurchaseOrderView=view.findViewById(R.id.drawerPurchaseOrderView); + //needed to disable touches on the innerContainer while the drawer is open touchBlocker = view.findViewById(R.id.touchBlocker); @@ -108,7 +115,7 @@ public class ListFragment extends Fragment { drawerLayout.closeDrawers(); }); - //Appoinment + //Appointment drawerAppointments.setOnClickListener(v -> { loadFragment(new AppointmentFragment()); drawerLayout.closeDrawers(); @@ -126,6 +133,27 @@ public class ListFragment extends Fragment { drawerLayout.closeDrawers(); }); + //ProductSupplier + + drawerProductSupplier.setOnClickListener(v -> { + loadFragment(new ProductSupplierFragment()); + drawerLayout.closeDrawers(); + }); + + //Purchase + + drawerPurchaseOrderView.setOnClickListener(v -> { + loadFragment(new PurchaseOrderFragment()); + drawerLayout.closeDrawers(); + }); + + //Sale + + drawerSale.setOnClickListener(v -> { + loadFragment(new SaleFragment()); + drawerLayout.closeDrawers(); + }); + return view; } 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 09ad489b..f8aa734f 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 @@ -1,35 +1,52 @@ package com.example.petstoremobile.fragments.listfragments; -// Added search/filter bar to filter appointments by customer name or service type. -// Added pull-to-refresh using SwipeRefreshLayout. - import android.os.Bundle; + +import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.text.Editable; import android.text.TextWatcher; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.ImageButton; +import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AppointmentAdapter; +import com.example.petstoremobile.api.AppointmentApi; +import com.example.petstoremobile.api.PetApi; +import com.example.petstoremobile.api.ServiceApi; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.detailfragments.AppointmentDetailFragment; -import com.example.petstoremobile.models.Appointment; import com.google.android.material.floatingactionbutton.FloatingActionButton; + import java.util.ArrayList; import java.util.List; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + public class AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener { - private List appointmentList = new ArrayList<>(); // full data list - private List filteredList = new ArrayList<>(); // filtered display list + private List appointmentList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); + private List petList = new ArrayList<>(); + private List serviceList = new ArrayList<>(); + private AppointmentAdapter adapter; + private AppointmentApi api; private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; private ImageButton hamburger; @@ -39,51 +56,57 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_appointment, container, false); + api = RetrofitClient.getAppointmentApi(requireContext()); hamburger = view.findViewById(R.id.btnHamburger); - loadAppointmentData(); // TODO: Replace with actual API call when backend is ready setupRecyclerView(view); setupSearch(view); setupSwipeRefresh(view); + loadAppointmentData(); + loadPets(); + loadServices(); - FloatingActionButton fabAddAppointment = view.findViewById(R.id.fabAddAppointment); - fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); - //Make the hamburger button open the drawer from listFragment + FloatingActionButton fabAdd = view.findViewById(R.id.fabAddAppointment); + fabAdd.setOnClickListener(v -> openAppointmentDetails(-1)); + hamburger.setOnClickListener(v -> { ListFragment listFragment = (ListFragment) getParentFragment(); - //if list fragment is found then use its helper function to open the drawer - if (listFragment != null) { + if (listFragment != null) listFragment.openDrawer(); - } }); return view; } - // Sets up the search bar to filter appointments by customer name or service type private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchAppointment); etSearch.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) { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { filterAppointments(s.toString()); } - @Override public void afterTextChanged(Editable s) {} + + @Override + public void afterTextChanged(Editable s) { + } }); } - // Filters the appointment list based on the search query private void filterAppointments(String query) { filteredList.clear(); if (query.isEmpty()) { filteredList.addAll(appointmentList); } else { String lower = query.toLowerCase(); - for (Appointment a : appointmentList) { - if (a.getCustomerName().toLowerCase().contains(lower) - || a.getServiceType().toLowerCase().contains(lower) - || a.getPetName().toLowerCase().contains(lower)) { + for (AppointmentDTO a : appointmentList) { + if ((a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lower)) + || (a.getServiceType() != null && a.getServiceType().toLowerCase().contains(lower)) + || (a.getPetName() != null && a.getPetName().toLowerCase().contains(lower))) { filteredList.add(a); } } @@ -91,43 +114,33 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. adapter.notifyDataSetChanged(); } - // Sets up pull-to-refresh: reloads data when user swipes down private void setupSwipeRefresh(View view) { swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAppointment); - swipeRefreshLayout.setOnRefreshListener(() -> { - loadAppointmentData(); // TODO: Replace with actual API call when backend is ready - filterAppointments(etSearch.getText().toString()); - swipeRefreshLayout.setRefreshing(false); - }); + swipeRefreshLayout.setOnRefreshListener(this::loadAppointmentData); } private void openAppointmentDetails(int position) { AppointmentDetailFragment detailFragment = new AppointmentDetailFragment(); Bundle args = new Bundle(); - args.putInt("position", position); if (position != -1) { - Appointment appointment = filteredList.get(position); - // Find the real position in the full list for save/delete callbacks - int realPosition = appointmentList.indexOf(appointment); - args.putInt("position", realPosition); - args.putInt("appointmentId", appointment.getAppointmentId()); - args.putString("customerName", appointment.getCustomerName()); - args.putString("petName", appointment.getPetName()); - args.putString("serviceType", appointment.getServiceType()); - args.putString("appointmentDate", appointment.getAppointmentDate()); - args.putString("appointmentTime", appointment.getAppointmentTime()); - args.putString("status", appointment.getStatus()); + AppointmentDTO a = filteredList.get(position); + args.putLong("appointmentId", a.getAppointmentId()); + args.putString("appointmentDate", a.getAppointmentDate()); + args.putString("appointmentTime", a.getAppointmentTime()); + args.putString("appointmentStatus", a.getAppointmentStatus()); + // IDs for pre-selecting spinners + if (a.getPetID() != null) args.putLong("petId", a.getPetID()); + if (a.getServiceId() != null) args.putLong("serviceId", a.getServiceId()); + if (a.getCustomerId() != null) args.putLong("customerId", a.getCustomerId()); + if (a.getStoreId() != null) args.putLong("storeId", a.getStoreId()); } detailFragment.setArguments(args); - detailFragment.setAppointmentFragment(this); - - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.loadFragment(detailFragment); + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.loadFragment(detailFragment); } - - public void onAppointmentSaved(int position, Appointment appointment) { + public void onAppointmentSaved(int position, AppointmentDTO appointment) { if (position == -1) { appointmentList.add(appointment); } else { @@ -146,21 +159,100 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. openAppointmentDetails(position); } - // Helper function to load hardcoded sample data - // Replace with API call private void loadAppointmentData() { - appointmentList.clear(); - appointmentList.add(new Appointment(1, "John Smith", "Buddy", "Grooming", "2026-03-10", "10:00 AM", "Confirmed")); - appointmentList.add(new Appointment(2, "Jane Doe", "Luna", "Vet Checkup", "2026-03-11", "02:00 PM", "Pending")); - appointmentList.add(new Appointment(3, "Bob Lee", "Max", "Training", "2026-03-12", "11:00 AM", "Confirmed")); - appointmentList.add(new Appointment(4, "Alice Brown", "Milo", "Grooming", "2026-03-13", "03:00 PM", "Cancelled")); - filteredList.clear(); - filteredList.addAll(appointmentList); + if (swipeRefreshLayout != null) + swipeRefreshLayout.setRefreshing(true); + api.getAllAppointments(0, 100).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, + Response> response) { + if (swipeRefreshLayout != null) + swipeRefreshLayout.setRefreshing(false); + if (response.isSuccessful() && response.body() != null) { + appointmentList.clear(); + appointmentList.addAll(response.body().getContent()); + filterAppointments(etSearch != null ? etSearch.getText().toString() : ""); + } else { + Log.e("AppointmentFragment", "Error: " + response.message()); + Toast.makeText(getContext(), "Failed to load appointments", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + if (swipeRefreshLayout != null) + swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + Log.e("AppointmentFragment", t.getMessage()); + } + }); + } + + + + // Load Pets + private void loadPets() { + PetApi petApi = RetrofitClient.getPetApi(requireContext()); + petApi.getAllPets(0,100).enqueue(new Callback>() { + + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() !=null) { + petList.clear(); + petList.addAll(response.body().getContent()); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + + Log.e("AppointmentFragment", "Pet load error:" + t.getMessage()); + + } + }); + } + + // Load Services + + private void loadServices() { + ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext()); + + serviceApi.getAllServices(0,100).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + serviceList.clear(); + serviceList.addAll(response.body().getContent()); + + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + Log.e("AppointmentFragmnet", "Service load error: " + t.getMessage()); + + } + }); + } + + private String getPetName(Long id) { + for (PetDTO p : petList) { + if (p.getPetId().equals(id)) return p.getPetName(); + + } + return ""; + } + + private String getServiceName(Long id) { + for (ServiceDTO s : serviceList) { + if (s.getServiceId().equals(id))return s.getServiceName(); + } + return ""; } private void setupRecyclerView(View view) { RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAppointments); - adapter = new AppointmentAdapter(filteredList, this); // adapter uses filteredList + adapter = new AppointmentAdapter(filteredList, this); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(adapter); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index bfd2d6b5..573e26e4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -1,178 +1,443 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; - -// Uses InputValidator for detailed field validation and ActivityLogger to log all changes. - -import android.content.res.Configuration; +import android.app.DatePickerDialog; import android.os.Bundle; - +import android.util.Log; +import android.view.*; +import android.widget.*; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; import com.example.petstoremobile.R; +import com.example.petstoremobile.api.*; +import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.AppointmentFragment; -import com.example.petstoremobile.models.Appointment; -import com.example.petstoremobile.utils.ActivityLogger; -import com.example.petstoremobile.utils.InputValidator; +import java.util.*; +import retrofit2.*; public class AppointmentDetailFragment extends Fragment { private TextView tvMode, tvAppointmentId; - private EditText etCustomerName, etPetName, etServiceType, etAppointmentDate, etAppointmentTime; - private Spinner spinnerStatus; - private Button btnSaveAppointment, btnDeleteAppointment, btnBack; - private int appointmentId; - private int position; - private boolean isEditing = false; - private AppointmentFragment appointmentFragment; + private EditText etAppointmentDate; + private Spinner spinnerPet, spinnerService, spinnerStatus, spinnerHour, spinnerMinute; + private Spinner spinnerCustomer, spinnerStore; + private Button btnSave, btnDelete, btnBack; - // Set the appointment fragment as parent so we refer back when save or delete is done - public void setAppointmentFragment(AppointmentFragment fragment) { - this.appointmentFragment = fragment; - } + private long appointmentId = -1; + private boolean isEditing = false; + private long preselectedPetId = -1; + private long preselectedServiceId = -1; + private long preselectedCustomerId = -1; + private long preselectedStoreId = -1; + + private List petList = new ArrayList<>(); + private List serviceList = new ArrayList<>(); + private List customerList = new ArrayList<>(); + private List storeList = new ArrayList<>(); + private List allAppointments = new ArrayList<>(); + + private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; + private final Integer[] MINUTES = {0,15,30,45}; + private final String[] STATUSES = {"Booked","Completed","Cancelled"}; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_appointment_detail, container, false); - initViews(view); - setupSpinner(); + setupSpinners(); + setupDatePicker(); + loadData(); handleArguments(); - btnBack.setOnClickListener(v -> { - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) { - listFragment.getChildFragmentManager().popBackStack(); - } - }); - btnSaveAppointment.setOnClickListener(v -> saveAppointment()); - btnDeleteAppointment.setOnClickListener(v -> deleteAppointment()); - + btnBack.setOnClickListener(v -> navigateBack()); + btnSave.setOnClickListener(v -> saveAppointment()); + btnDelete.setOnClickListener(v -> confirmDelete()); return view; } - // Validates all fields using InputValidator, then saves the appointment - private void saveAppointment() { - // Validate all inputs using InputValidator utility - if (!InputValidator.isNotEmpty(etCustomerName, "Customer Name")) return; - if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return; - if (!InputValidator.isNotEmpty(etServiceType, "Service Type")) return; - if (!InputValidator.isValidDate(etAppointmentDate)) return; - if (!InputValidator.isValidTime(etAppointmentTime)) return; + private void initViews(View v) { + tvMode = v.findViewById(R.id.tvApptMode); + tvAppointmentId = v.findViewById(R.id.tvAppointmentId); + etAppointmentDate= v.findViewById(R.id.etAppointmentDate); + spinnerPet = v.findViewById(R.id.spinnerPet); + spinnerService = v.findViewById(R.id.spinnerService); + spinnerStatus = v.findViewById(R.id.spinnerAppointmentStatus); + spinnerHour = v.findViewById(R.id.spinnerHour); + spinnerMinute = v.findViewById(R.id.spinnerMinute); + spinnerCustomer = v.findViewById(R.id.spinnerCustomer); + spinnerStore = v.findViewById(R.id.spinnerStore); + btnSave = v.findViewById(R.id.btnSaveAppointment); + btnDelete = v.findViewById(R.id.btnDeleteAppointment); + btnBack = v.findViewById(R.id.btnApptBack); + } - String customerName = etCustomerName.getText().toString().trim(); - String petName = etPetName.getText().toString().trim(); - String serviceType = etServiceType.getText().toString().trim(); - String date = etAppointmentDate.getText().toString().trim(); - String time = etAppointmentTime.getText().toString().trim(); - String status = spinnerStatus.getSelectedItem().toString(); + private void setupSpinners() { + spinnerStatus.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, STATUSES)); - try { - if (isEditing) { - // TODO: Replace with actual API PUT call when backend is ready - Appointment updated = new Appointment(appointmentId, customerName, petName, serviceType, date, time, status); - if (appointmentFragment != null) appointmentFragment.onAppointmentSaved(position, updated); - ActivityLogger.logChange(requireContext(), "Appointment", "UPDATED", appointmentId); - Toast.makeText(getContext(), "Appointment updated.", Toast.LENGTH_SHORT).show(); - } else { - // TODO: Replace with actual API POST call when backend is ready - Appointment newAppt = new Appointment(0, customerName, petName, serviceType, date, time, status); - if (appointmentFragment != null) appointmentFragment.onAppointmentSaved(-1, newAppt); - ActivityLogger.log(requireContext(), "Added new Appointment for customer: " + customerName); - Toast.makeText(getContext(), "Appointment added.", Toast.LENGTH_SHORT).show(); + String[] hours = new String[HOURS.length]; + for (int i = 0; i < HOURS.length; i++) + hours[i] = String.format("%02d:00", HOURS[i]); + spinnerHour.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, hours)); + spinnerMinute.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, new String[]{"00","15","30","45"})); + } + + private void setupDatePicker() { + etAppointmentDate.setOnClickListener(v -> { + Calendar c = Calendar.getInstance(); + DatePickerDialog d = new DatePickerDialog(requireContext(), + (dp,y,m,d1) -> etAppointmentDate.setText( + String.format("%04d-%02d-%02d", y, m+1, d1)), + c.get(Calendar.YEAR), c.get(Calendar.MONTH), + c.get(Calendar.DAY_OF_MONTH)); + d.getDatePicker().setMinDate(System.currentTimeMillis() - 1000); + d.show(); + }); + } + + private void loadData() { + loadPets(); + loadServices(); + loadCustomers(); + loadStores(); + loadAllAppointments(); + } + + private void loadPets() { + RetrofitClient.getPetApi(requireContext()).getAllPets(0, 200) + .enqueue(new Callback>() { + public void onResponse(Call> c, Response> r) { + if (r.isSuccessful() && r.body() != null) { + petList = r.body().getContent(); + populatePetSpinner(); + } + } + public void onFailure(Call> c, Throwable t) { + Log.e("APPT", "Pet load failed: " + t.getMessage()); + } + }); + } + + private void populatePetSpinner() { + List names = new ArrayList<>(); + names.add("-- Select Pet --"); + for (PetDTO p : petList) names.add(p.getPetName()); + spinnerPet.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + if (preselectedPetId != -1) { + for (int i = 0; i < petList.size(); i++) { + if (petList.get(i).getPetId().equals(preselectedPetId)) { + spinnerPet.setSelection(i + 1); break; + } } - // Go back to list - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); - } catch (Exception e) { - ActivityLogger.logException(requireContext(), "AppointmentDetailFragment.saveAppointment", e); - Toast.makeText(getContext(), "Error saving appointment.", Toast.LENGTH_SHORT).show(); } } - // Deletes the appointment and logs the action - private void deleteAppointment() { - try { - // TODO: Replace with actual API DELETE call when backend is ready - if (appointmentFragment != null) appointmentFragment.onAppointmentDeleted(position); - ActivityLogger.logChange(requireContext(), "Appointment", "DELETED", appointmentId); - Toast.makeText(getContext(), "Appointment deleted.", Toast.LENGTH_SHORT).show(); - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); - } catch (Exception e) { - ActivityLogger.logException(requireContext(), "AppointmentDetailFragment.deleteAppointment", e); - Toast.makeText(getContext(), "Error deleting appointment.", Toast.LENGTH_SHORT).show(); + private void loadServices() { + RetrofitClient.getServiceApi(requireContext()).getAllServices(0, 200) + .enqueue(new Callback>() { + public void onResponse(Call> c, Response> r) { + if (r.isSuccessful() && r.body() != null) { + serviceList = r.body().getContent(); + populateServiceSpinner(); + } + } + public void onFailure(Call> c, Throwable t) { + Log.e("APPT", "Service load failed: " + t.getMessage()); + } + }); + } + + private void populateServiceSpinner() { + List names = new ArrayList<>(); + names.add("-- Select Service --"); + for (ServiceDTO s : serviceList) names.add(s.getServiceName()); + spinnerService.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + if (preselectedServiceId != -1) { + for (int i = 0; i < serviceList.size(); i++) { + if (serviceList.get(i).getServiceId().equals(preselectedServiceId)) { + spinnerService.setSelection(i + 1); break; + } + } } } - // Determines if the fragment is in add or edit mode and populates fields accordingly + 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(); + populateCustomerSpinner(); + } + } + public void onFailure(Call> c, Throwable t) { + Log.e("APPT", "Customer load failed: " + t.getMessage()); + } + }); + } + + private void populateCustomerSpinner() { + List names = new ArrayList<>(); + names.add("-- Select Customer --"); + for (CustomerDTO c : customerList) + names.add(c.getFirstName() + " " + c.getLastName()); + spinnerCustomer.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + if (preselectedCustomerId != -1) { + for (int i = 0; i < customerList.size(); i++) { + if (customerList.get(i).getCustomerId().equals(preselectedCustomerId)) { + spinnerCustomer.setSelection(i + 1); break; + } + } + } + } + + private void loadStores() { + RetrofitClient.getStoreApi(requireContext()).getAllStores(0, 50) + .enqueue(new Callback>() { + public void onResponse(Call> c, Response> r) { + if (r.isSuccessful() && r.body() != null) { + storeList = r.body().getContent(); + populateStoreSpinner(); + } + } + public void onFailure(Call> c, Throwable t) { + Log.e("APPT", "Store load failed: " + t.getMessage()); + } + }); + } + + private void populateStoreSpinner() { + List names = new ArrayList<>(); + names.add("-- Select Store --"); + for (StoreDTO s : storeList) names.add(s.getStoreName()); + spinnerStore.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + if (preselectedStoreId != -1) { + for (int i = 0; i < storeList.size(); i++) { + if (storeList.get(i).getStoreId().equals(preselectedStoreId)) { + spinnerStore.setSelection(i + 1); break; + } + } + } + } + + private void loadAllAppointments() { + RetrofitClient.getAppointmentApi(requireContext()).getAllAppointments(0, 500) + .enqueue(new Callback>() { + public void onResponse(Call> c, Response> r) { + if (r.isSuccessful() && r.body() != null) + allAppointments = r.body().getContent(); + } + public void onFailure(Call> c, Throwable t) {} + }); + } + private void handleArguments() { - if (getArguments() != null && getArguments().containsKey("appointmentId")) { + Bundle a = getArguments(); + if (a != null && a.containsKey("appointmentId")) { isEditing = true; - appointmentId = getArguments().getInt("appointmentId"); - position = getArguments().getInt("position"); + appointmentId = a.getLong("appointmentId"); + preselectedPetId = a.getLong("petId", -1); + preselectedServiceId= a.getLong("serviceId", -1); + preselectedCustomerId = a.getLong("customerId", -1); + preselectedStoreId = a.getLong("storeId", -1); + tvMode.setText("Edit Appointment"); tvAppointmentId.setText("ID: " + appointmentId); - etCustomerName.setText(getArguments().getString("customerName")); - etPetName.setText(getArguments().getString("petName")); - etServiceType.setText(getArguments().getString("serviceType")); - etAppointmentDate.setText(getArguments().getString("appointmentDate")); - etAppointmentTime.setText(getArguments().getString("appointmentTime")); - String status = getArguments().getString("status"); - if ("Confirmed".equals(status)) spinnerStatus.setSelection(0); - else if ("Pending".equals(status)) spinnerStatus.setSelection(1); - else spinnerStatus.setSelection(2); - btnDeleteAppointment.setVisibility(View.VISIBLE); + tvAppointmentId.setVisibility(View.VISIBLE); + etAppointmentDate.setText(a.getString("appointmentDate")); + btnDelete.setVisibility(View.VISIBLE); + + // Pre-fill time spinners + String time = a.getString("appointmentTime", "09:00"); + if (time.length() > 5) time = time.substring(0, 5); + String[] parts = time.split(":"); + if (parts.length == 2) { + int hour = Integer.parseInt(parts[0]); + int min = Integer.parseInt(parts[1]); + for (int i = 0; i < HOURS.length; i++) + if (HOURS[i] == hour) { spinnerHour.setSelection(i); break; } + for (int i = 0; i < MINUTES.length; i++) + if (MINUTES[i] == min) { spinnerMinute.setSelection(i); break; } + } + + // Pre-fill status + String status = a.getString("appointmentStatus", "Booked"); + for (int i = 0; i < STATUSES.length; i++) + if (STATUSES[i].equals(status)) { spinnerStatus.setSelection(i); break; } + } else { - isEditing = false; tvMode.setText("Add Appointment"); + btnDelete.setVisibility(View.GONE); tvAppointmentId.setVisibility(View.GONE); - btnDeleteAppointment.setVisibility(View.GONE); - btnSaveAppointment.setText("Add"); } } - private void initViews(View view) { - tvMode = view.findViewById(R.id.tvApptMode); - tvAppointmentId = view.findViewById(R.id.tvAppointmentId); - etCustomerName = view.findViewById(R.id.etCustomerName); - etPetName = view.findViewById(R.id.etApptPetName); - etServiceType = view.findViewById(R.id.etServiceType); - etAppointmentDate = view.findViewById(R.id.etAppointmentDate); - etAppointmentTime = view.findViewById(R.id.etAppointmentTime); - spinnerStatus = view.findViewById(R.id.spinnerAppointmentStatus); - btnSaveAppointment = view.findViewById(R.id.btnSaveAppointment); - btnDeleteAppointment = view.findViewById(R.id.btnDeleteAppointment); - btnBack = view.findViewById(R.id.btnApptBack); + private void saveAppointment() { + if (spinnerCustomer.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; + } + if (spinnerStore.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return; + } + if (spinnerPet.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return; + } + if (spinnerService.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a service", Toast.LENGTH_SHORT).show(); return; + } + String date = etAppointmentDate.getText().toString().trim(); + if (date.isEmpty()) { + Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; + } + + CustomerDTO customer = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1); + StoreDTO store = storeList.get(spinnerStore.getSelectedItemPosition() - 1); + PetDTO pet = petList.get(spinnerPet.getSelectedItemPosition() - 1); + ServiceDTO service = serviceList.get(spinnerService.getSelectedItemPosition() - 1); + + String time = String.format("%02d:%02d", + HOURS[spinnerHour.getSelectedItemPosition()], + MINUTES[spinnerMinute.getSelectedItemPosition()]); + String status = STATUSES[spinnerStatus.getSelectedItemPosition()]; + + + // Validate future date+time if status is Booked + if ("Booked".equalsIgnoreCase(status)) { + try { + String[] dateParts = date.split("-"); + String[] timeParts = time.split(":"); + Calendar selected = Calendar.getInstance(); + selected.set( + Integer.parseInt(dateParts[0]), + Integer.parseInt(dateParts[1]) - 1, + Integer.parseInt(dateParts[2]), + Integer.parseInt(timeParts[0]), + Integer.parseInt(timeParts[1]), + 0 + ); + if (selected.before(Calendar.getInstance())) { + showErrorDialog("Invalid Time", + "Booked appointments must be in the future. " + + "Please select a future date and time."); + return; + } + } catch (Exception e) { + Log.e("APPT_SAVE", "Date parse error: " + e.getMessage()); + } + } + + // Build DTO with all required IDs + AppointmentDTO dto = new AppointmentDTO( + customer.getCustomerId(), + store.getStoreId(), + service.getServiceId(), + date, + time, + status, + Collections.singletonList(pet.getPetId()) + ); + + Log.d("APPT_SAVE", "customerId=" + customer.getCustomerId() + + " storeId=" + store.getStoreId() + + " serviceId=" + service.getServiceId() + + " petId=" + pet.getPetId() + + " date=" + date + " time=" + time); + + AppointmentApi api = RetrofitClient.getAppointmentApi(requireContext()); + if (isEditing) { + api.updateAppointment(appointmentId, dto).enqueue(simpleCallback("Updated")); + } else { + api.createAppointment(dto).enqueue(simpleCallback("Saved")); + } } - private void setupSpinner() { - ArrayAdapter adapter = new ArrayAdapter(requireContext(), - android.R.layout.simple_spinner_item, - new String[]{"Confirmed", "Pending", "Cancelled"}) { + private Callback simpleCallback(String msg) { + return new Callback<>() { + public void onResponse(Call c, Response r) { + Log.d("APPT_SAVE", "Response: " + r.code()); + if (r.isSuccessful()) { + Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + try { + String errorBody = r.errorBody().string(); + Log.e("APPT_SAVE", "Error: " + errorBody); - //Override the getView method for the spinner to make the text color darker for more readability - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - View view = super.getView(position, convertView, parent); - ((TextView) view).setTextColor(ContextCompat.getColor(requireContext(), R.color.text_dark)); - return view; + // Show proper dialog based on error type + if (errorBody.toLowerCase().contains("future")) { + showErrorDialog("Invalid Date/Time", + "Booked appointments must be scheduled in the future. " + + "Please select a future date and time."); + //------------------------------------------ + } else if (errorBody.toLowerCase().contains("not available") || + errorBody.toLowerCase().contains("time is not available")) { + showNoAvailabilityDialog(); + } else if (r.code() == 404) { + showErrorDialog("Not Found", + "The selected pet, customer or service was not found."); + } else if (r.code() == 403) { + showErrorDialog("Access Denied", + "You don't have permission to perform this action."); + } else if (r.code() == 400) { + showErrorDialog("Invalid Request", errorBody); + } else { + showErrorDialog("Error", "Something went wrong. Please try again."); + } + //----------------------------- + } catch (Exception e) { + Log.e("APPT_SAVE", "Failed to read error body"); + showErrorDialog("Error", "Something went wrong. Please try again."); + } + } + } + + public void onFailure(Call c, Throwable t) { + Log.e("APPT_SAVE", "Failure: " + t.getMessage()); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); } }; - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinnerStatus.setAdapter(adapter); } -} + + private void showNoAvailabilityDialog() { + new AlertDialog.Builder(requireContext()) + .setTitle("No Availability") + .setMessage("This time slot is already booked for the selected service and store. Please choose a different time or date.") + .setPositiveButton("Change Time", (d, w) -> d.dismiss()) + .setNegativeButton("Cancel Booking", (d, w) -> navigateBack()) + .setCancelable(false) + .show(); + } + + private void showErrorDialog(String title, String message) { + new AlertDialog.Builder(requireContext()) + .setTitle(title) + .setMessage(message) + .setPositiveButton("OK", null) + .show(); + } + private void confirmDelete() { + new AlertDialog.Builder(requireContext()) + .setTitle("Delete Appointment?") + .setPositiveButton("Yes", (d, w) -> + RetrofitClient.getAppointmentApi(requireContext()) + .deleteAppointment(appointmentId) + .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() { + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.getChildFragmentManager().popBackStack(); + } +} \ No newline at end of file From 55f40572de7492e785f496d181987794115750bc Mon Sep 17 00:00:00 2001 From: Nikitha Date: Sun, 29 Mar 2026 16:24:14 -0600 Subject: [PATCH 02/35] Adoption files adoption --- .../adapters/AdoptionAdapter.java | 76 ++-- .../petstoremobile/api/AdoptionApi.java | 34 ++ .../petstoremobile/dtos/AdoptionDTO.java | 68 ++++ .../listfragments/AdoptionFragment.java | 172 ++++----- .../AdoptionDetailFragment.java | 337 +++++++++++------- .../src/main/res/layout/fragment_adoption.xml | 5 +- .../res/layout/fragment_adoption_detail.xml | 75 ++-- .../app/src/main/res/layout/item_adoption.xml | 116 +++--- 8 files changed, 514 insertions(+), 369 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java 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 98e88256..c6bd678c 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,79 @@ package com.example.petstoremobile.adapters; - import android.graphics.Color; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; +import android.view.*; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; -import com.example.petstoremobile.models.Adoption; +import com.example.petstoremobile.dtos.AdoptionDTO; import java.util.List; public class AdoptionAdapter extends RecyclerView.Adapter { - private List adoptionList; - private OnAdoptionClickListener adoptionClickListener; + private List adoptionList; + private OnAdoptionClickListener listener; - // Interface for adoption click on recycler view public interface OnAdoptionClickListener { void onAdoptionClick(int position); } - // Constructor - public AdoptionAdapter(List adoptionList, OnAdoptionClickListener adoptionClickListener) { + public AdoptionAdapter(List adoptionList, OnAdoptionClickListener listener) { this.adoptionList = adoptionList; - this.adoptionClickListener = adoptionClickListener; + this.listener = listener; } - // Get the controls of each row in recycler view public static class AdoptionViewHolder extends RecyclerView.ViewHolder { - TextView tvAdopterName, tvPetName, tvAdoptionDate, tvAdoptionStatus; + TextView tvCustomerName, tvPetName, tvDate, tvFee, tvStatus; public AdoptionViewHolder(@NonNull View v) { super(v); - tvAdopterName = v.findViewById(R.id.tvAdopterName); - tvPetName = v.findViewById(R.id.tvAdoptionPetName); - tvAdoptionDate = v.findViewById(R.id.tvAdoptionDate); - tvAdoptionStatus = v.findViewById(R.id.tvAdoptionStatus); + 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); } } - // Create a new row view @NonNull @Override public AdoptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_adoption, parent, false); + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_adoption, parent, false); return new AdoptionViewHolder(v); } - // Populate the row with adoption data @Override public void onBindViewHolder(@NonNull AdoptionViewHolder holder, int position) { - Adoption adoption = adoptionList.get(position); + AdoptionDTO a = adoptionList.get(position); - holder.tvAdopterName.setText(adoption.getAdopterName()); - holder.tvPetName.setText("Pet: " + adoption.getPetName()); - holder.tvAdoptionDate.setText("Date: " + adoption.getAdoptionDate()); - holder.tvAdoptionStatus.setText(adoption.getStatus()); + 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() : ""); - // Set the status color depending on adoption status - if (adoption.getStatus().equals("Approved")) { - holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#4CAF50")); - } else if (adoption.getStatus().equals("Pending")) { - holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#FF9800")); - } else { - holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#F44336")); + String status = a.getAdoptionStatus() != null ? a.getAdoptionStatus() : ""; + holder.tvStatus.setText(status); + + switch (status) { + case "Approved": + holder.tvStatus.setBackgroundColor(Color.parseColor("#4CAF50")); + break; + case "Pending": + holder.tvStatus.setBackgroundColor(Color.parseColor("#FF9800")); + break; + case "Rejected": + holder.tvStatus.setBackgroundColor(Color.parseColor("#F44336")); + break; + default: + holder.tvStatus.setBackgroundColor(Color.parseColor("#9E9E9E")); + break; } - // When a row is clicked, open the detail view - holder.itemView.setOnClickListener(v -> adoptionClickListener.onAdoptionClick(position)); + holder.itemView.setOnClickListener(v -> listener.onAdoptionClick(position)); } @Override - public int getItemCount() { - return adoptionList.size(); - } -} + public int getItemCount() { return adoptionList.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 new file mode 100644 index 00000000..2f704a41 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java @@ -0,0 +1,34 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.dtos.PageResponse; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface AdoptionApi { + + @GET("api/v1/adoptions") + Call> getAllAdoptions( + @Query("page") int page, + @Query("size") int size); + + @GET("api/v1/adoptions/{id}") + Call getAdoptionById(@Path("id") Long id); + + @POST("api/v1/adoptions") + Call createAdoption(@Body AdoptionDTO adoption); + + @PUT("api/v1/adoptions/{id}") + Call updateAdoption(@Path("id") Long id, @Body AdoptionDTO adoption); + + @DELETE("api/v1/adoptions/{id}") + Call deleteAdoption(@Path("id") Long id); +} + 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 new file mode 100644 index 00000000..03758473 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java @@ -0,0 +1,68 @@ +package com.example.petstoremobile.dtos; + +import java.math.BigDecimal; + +public class AdoptionDTO { + private Long adoptionId; + private Long petId; + private String petName; + private Long customerId; + private String customerName; + private String adoptionDate; + private String adoptionStatus; + private BigDecimal adoptionFee; + private String createdAt; + private String updatedAt; + + // Constructor for create/update requests + public AdoptionDTO(Long petId, Long customerId, String adoptionDate, String adoptionStatus) { + this.petId = petId; + this.customerId = customerId; + this.adoptionDate = adoptionDate; + this.adoptionStatus = adoptionStatus; + } + + public Long getAdoptionId() { + return adoptionId; + } + + public Long getPetId() { + return petId; + } + + public String getPetName() { + return petName; + } + + public Long getCustomerId() { + return customerId; + } + + public String getCustomerName() { + return customerName; + } + + public String getAdoptionDate() { + return adoptionDate; + } + + public String getAdoptionStatus() { + return adoptionStatus; + } + + public String getStatus() { + return adoptionStatus; + } + + public BigDecimal getAdoptionFee() { + return adoptionFee; + } + + 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/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index f72d6836..34e7d602 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,36 +1,33 @@ package com.example.petstoremobile.fragments.listfragments; -// Added search/filter bar to filter adoptions by adopter name or pet name. -// Added pull-to-refresh using SwipeRefreshLayout. - import android.os.Bundle; +import android.text.*; +import android.util.Log; +import android.view.*; +import android.widget.*; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ImageButton; - import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AdoptionAdapter; +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.fragments.listfragments.detailfragments.AdoptionDetailFragment; -import com.example.petstoremobile.models.Adoption; import com.google.android.material.floatingactionbutton.FloatingActionButton; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import retrofit2.*; public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener { - private List adoptionList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private List adoptionList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); private AdoptionAdapter adapter; - private SwipeRefreshLayout swipeRefreshLayout; + private AdoptionApi api; + private SwipeRefreshLayout swipeRefresh; private EditText etSearch; private ImageButton hamburger; @@ -39,51 +36,58 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_adoption, container, false); - hamburger = view.findViewById(R.id.btnHamburger); + api = RetrofitClient.getAdoptionApi(requireContext()); + hamburger = view.findViewById(R.id.btnHamburgerAdoption); - loadAdoptionData(); - // Replace with actual API call when backend is ready setupRecyclerView(view); setupSearch(view); setupSwipeRefresh(view); + loadAdoptions(); - FloatingActionButton fabAddAdoption = view.findViewById(R.id.fabAddAdoption); - fabAddAdoption.setOnClickListener(v -> openAdoptionDetails(-1)); + FloatingActionButton fab = view.findViewById(R.id.fabAddAdoption); + fab.setOnClickListener(v -> openDetail(-1)); - //Make the hamburger button open the drawer from listFragment hamburger.setOnClickListener(v -> { - ListFragment listFragment = (ListFragment) getParentFragment(); - //if list fragment is found then use its helper function to open the drawer - if (listFragment != null) { - listFragment.openDrawer(); - } + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.openDrawer(); }); return view; } - // Filters adoption list by adopter name or pet name + private void setupRecyclerView(View view) { + RecyclerView rv = view.findViewById(R.id.recyclerViewAdoptions); + adapter = new AdoptionAdapter(filteredList, this); + rv.setLayoutManager(new LinearLayoutManager(getContext())); + rv.setAdapter(adapter); + } + private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchAdoption); etSearch.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) { - filterAdoptions(s.toString()); + 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()); } - @Override public void afterTextChanged(Editable s) {} }); } - private void filterAdoptions(String query) { + private void setupSwipeRefresh(View view) { + swipeRefresh = view.findViewById(R.id.swipeRefreshAdoption); + swipeRefresh.setOnRefreshListener(this::loadAdoptions); + } + + private void filter(String query) { filteredList.clear(); if (query.isEmpty()) { filteredList.addAll(adoptionList); } else { String lower = query.toLowerCase(); - for (Adoption a : adoptionList) { - if (a.getAdopterName().toLowerCase().contains(lower) - || a.getPetName().toLowerCase().contains(lower) - || a.getStatus().toLowerCase().contains(lower)) { + 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); } } @@ -91,73 +95,47 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop adapter.notifyDataSetChanged(); } - private void setupSwipeRefresh(View view) { - swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAdoption); - swipeRefreshLayout.setOnRefreshListener(() -> { - loadAdoptionData(); // TODO: Replace with actual API call - filterAdoptions(etSearch.getText().toString()); - swipeRefreshLayout.setRefreshing(false); + private void loadAdoptions() { + if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + api.getAllAdoptions(0, 100).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()); + } }); } - private void openAdoptionDetails(int position) { - AdoptionDetailFragment detailFragment = new AdoptionDetailFragment(); + private void openDetail(int position) { + AdoptionDetailFragment detail = new AdoptionDetailFragment(); Bundle args = new Bundle(); - args.putInt("position", position); if (position != -1) { - Adoption adoption = filteredList.get(position); - int realPosition = adoptionList.indexOf(adoption); - args.putInt("position", realPosition); - args.putInt("adoptionId", adoption.getAdoptionId()); - args.putString("adopterName", adoption.getAdopterName()); - args.putString("adopterEmail", adoption.getAdopterEmail()); - args.putString("adopterPhone", adoption.getAdopterPhone()); - args.putString("petName", adoption.getPetName()); - args.putString("adoptionDate", adoption.getAdoptionDate()); - args.putString("status", adoption.getStatus()); + 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()); } - detailFragment.setArguments(args); - detailFragment.setAdoptionFragment(this); - - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.loadFragment(detailFragment); - } - - public void onAdoptionSaved(int position, Adoption adoption) { - if (position == -1) { - adoptionList.add(adoption); - } else { - adoptionList.set(position, adoption); - } - filterAdoptions(etSearch.getText().toString()); - } - - public void onAdoptionDeleted(int position) { - adoptionList.remove(position); - filterAdoptions(etSearch.getText().toString()); + detail.setArguments(args); + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.loadFragment(detail); } @Override - public void onAdoptionClick(int position) { - openAdoptionDetails(position); - } - - private void loadAdoptionData() { - adoptionList.clear(); - adoptionList.add(new Adoption(1, "Sarah Connor", "sarah@email.com", "555-1234", "Luna", "2026-03-01", "Approved")); - adoptionList.add(new Adoption(2, "Tom Hardy", "tom@email.com", "555-5678", "Bella", "2026-03-05", "Pending")); - adoptionList.add(new Adoption(3, "Emily Clark", "emily@email.com", "555-9012", "Charlie", "2026-03-07", "Pending")); - adoptionList.add(new Adoption(4, "Mike Ross", "mike@email.com", "555-3456", "Milo", "2026-02-20", "Rejected")); - filteredList.clear(); - filteredList.addAll(adoptionList); - } - - private void setupRecyclerView(View view) { - RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAdoptions); - adapter = new AdoptionAdapter(filteredList, this); - recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(adapter); - } -} + public void onAdoptionClick(int position) { openDetail(position); } +} \ No newline at end of file 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 32c06996..c32edaf0 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 @@ -1,172 +1,257 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; - -// Uses InputValidator for detailed field validation and ActivityLogger to log all changes. - +import android.app.DatePickerDialog; import android.os.Bundle; - +import android.util.Log; +import android.view.*; +import android.widget.*; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; import com.example.petstoremobile.R; +import com.example.petstoremobile.api.*; +import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.AdoptionFragment; -import com.example.petstoremobile.models.Adoption; -import com.example.petstoremobile.utils.ActivityLogger; -import com.example.petstoremobile.utils.InputValidator; +import java.util.*; +import retrofit2.*; public class AdoptionDetailFragment extends Fragment { private TextView tvMode, tvAdoptionId; - private EditText etAdopterName, etAdopterEmail, etAdopterPhone, etPetName, etAdoptionDate; - private Spinner spinnerAdoptionStatus; - private Button btnSaveAdoption, btnDeleteAdoption, btnBack; - private int adoptionId; - private int position; - private boolean isEditing = false; - private AdoptionFragment adoptionFragment; + private EditText etAdoptionDate; + private Spinner spinnerPet, spinnerCustomer, spinnerStatus; + private Button btnSave, btnDelete, btnBack; - // Set the adoption fragment as parent so we refer back when save or delete is done - public void setAdoptionFragment(AdoptionFragment fragment) { - this.adoptionFragment = fragment; - } + private long adoptionId = -1; + private boolean isEditing = false; + private long preselectedPetId = -1; + private long preselectedCustomerId = -1; + + private List petList = new ArrayList<>(); + private List customerList = new ArrayList<>(); + + private final String[] STATUSES = {"Pending", "Approved", "Rejected"}; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_adoption_detail, container, false); - initViews(view); - setupSpinner(); + setupSpinners(); + setupDatePicker(); + loadData(); handleArguments(); - btnBack.setOnClickListener(v -> { - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); - }); - btnSaveAdoption.setOnClickListener(v -> saveAdoption()); - btnDeleteAdoption.setOnClickListener(v -> deleteAdoption()); - + btnBack.setOnClickListener(v -> navigateBack()); + btnSave.setOnClickListener(v -> saveAdoption()); + btnDelete.setOnClickListener(v -> confirmDelete()); return view; } - // Validates all fields using InputValidator, then saves the adoption record - private void saveAdoption() { - if (!InputValidator.isNotEmpty(etAdopterName, "Adopter Name")) return; - if (!InputValidator.isValidEmail(etAdopterEmail)) return; - if (!InputValidator.isValidPhone(etAdopterPhone)) return; - if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return; - if (!InputValidator.isValidDate(etAdoptionDate)) return; + private void initViews(View v) { + tvMode = v.findViewById(R.id.tvAdoptionMode); + tvAdoptionId = v.findViewById(R.id.tvAdoptionId); + etAdoptionDate = v.findViewById(R.id.etAdoptionDate); + spinnerPet = v.findViewById(R.id.spinnerAdoptionPet); + spinnerCustomer= v.findViewById(R.id.spinnerAdoptionCustomer); + spinnerStatus = v.findViewById(R.id.spinnerAdoptionStatus); + btnSave = v.findViewById(R.id.btnSaveAdoption); + btnDelete = v.findViewById(R.id.btnDeleteAdoption); + btnBack = v.findViewById(R.id.btnAdoptionBack); + } - String adopterName = etAdopterName.getText().toString().trim(); - String adopterEmail = etAdopterEmail.getText().toString().trim(); - String adopterPhone = etAdopterPhone.getText().toString().trim(); - String petName = etPetName.getText().toString().trim(); - String adoptionDate = etAdoptionDate.getText().toString().trim(); - String status = spinnerAdoptionStatus.getSelectedItem().toString(); + private void setupSpinners() { + spinnerStatus.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, STATUSES)); + } - try { - if (isEditing) { - // TODO: Replace with actual API PUT call when backend is ready - Adoption updated = new Adoption(adoptionId, adopterName, adopterEmail, adopterPhone, petName, adoptionDate, status); - if (adoptionFragment != null) adoptionFragment.onAdoptionSaved(position, updated); - ActivityLogger.logChange(requireContext(), "Adoption", "UPDATED", adoptionId); - Toast.makeText(getContext(), "Adoption record updated.", Toast.LENGTH_SHORT).show(); - } else { - // TODO: Replace with actual API POST call when backend is ready - Adoption newAdoption = new Adoption(0, adopterName, adopterEmail, adopterPhone, petName, adoptionDate, status); - if (adoptionFragment != null) adoptionFragment.onAdoptionSaved(-1, newAdoption); - ActivityLogger.log(requireContext(), "Added new Adoption record for: " + adopterName + " adopting " + petName); - Toast.makeText(getContext(), "Adoption record added.", Toast.LENGTH_SHORT).show(); + private void setupDatePicker() { + etAdoptionDate.setOnClickListener(v -> { + Calendar c = Calendar.getInstance(); + new DatePickerDialog(requireContext(), + (dp, y, m, d) -> etAdoptionDate.setText( + String.format("%04d-%02d-%02d", y, m + 1, d)), + c.get(Calendar.YEAR), + c.get(Calendar.MONTH), + c.get(Calendar.DAY_OF_MONTH)).show(); + }); + } + + private void loadData() { + loadPets(); + loadCustomers(); + } + + private void loadPets() { + RetrofitClient.getPetApi(requireContext()).getAllPets(0, 200) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (r.isSuccessful() && r.body() != null) { + petList = r.body().getContent(); + populatePetSpinner(); + } + } + public void onFailure(Call> c, Throwable t) { + Log.e("ADOPTION", "Pet load failed: " + t.getMessage()); + } + }); + } + + private void populatePetSpinner() { + List names = new ArrayList<>(); + names.add("-- Select Pet --"); + for (PetDTO p : petList) names.add(p.getPetName()); + spinnerPet.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + if (preselectedPetId != -1) { + for (int i = 0; i < petList.size(); i++) { + if (petList.get(i).getPetId().equals(preselectedPetId)) { + spinnerPet.setSelection(i + 1); break; + } } - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); - } catch (Exception e) { - ActivityLogger.logException(requireContext(), "AdoptionDetailFragment.saveAdoption", e); - Toast.makeText(getContext(), "Error saving adoption record.", Toast.LENGTH_SHORT).show(); } } - // Deletes the adoption record and logs the action - private void deleteAdoption() { - try { - // TODO: Replace with actual API DELETE call when backend is ready - if (adoptionFragment != null) adoptionFragment.onAdoptionDeleted(position); - ActivityLogger.logChange(requireContext(), "Adoption", "DELETED", adoptionId); - Toast.makeText(getContext(), "Adoption record deleted.", Toast.LENGTH_SHORT).show(); - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); - } catch (Exception e) { - ActivityLogger.logException(requireContext(), "AdoptionDetailFragment.deleteAdoption", e); - Toast.makeText(getContext(), "Error deleting adoption record.", Toast.LENGTH_SHORT).show(); + 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(); + populateCustomerSpinner(); + } + } + public void onFailure(Call> c, Throwable t) { + Log.e("ADOPTION", "Customer load failed: " + t.getMessage()); + } + }); + } + + private void populateCustomerSpinner() { + List names = new ArrayList<>(); + names.add("-- Select Customer --"); + for (CustomerDTO c : customerList) + names.add(c.getFirstName() + " " + c.getLastName()); + spinnerCustomer.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + if (preselectedCustomerId != -1) { + for (int i = 0; i < customerList.size(); i++) { + if (customerList.get(i).getCustomerId().equals(preselectedCustomerId)) { + spinnerCustomer.setSelection(i + 1); break; + } + } } } private void handleArguments() { - if (getArguments() != null && getArguments().containsKey("adoptionId")) { + Bundle a = getArguments(); + if (a != null && a.containsKey("adoptionId")) { isEditing = true; - adoptionId = getArguments().getInt("adoptionId"); - position = getArguments().getInt("position"); + adoptionId = a.getLong("adoptionId"); + preselectedPetId = a.getLong("petId", -1); + preselectedCustomerId = a.getLong("customerId", -1); + tvMode.setText("Edit Adoption"); tvAdoptionId.setText("ID: " + adoptionId); - etAdopterName.setText(getArguments().getString("adopterName")); - etAdopterEmail.setText(getArguments().getString("adopterEmail")); - etAdopterPhone.setText(getArguments().getString("adopterPhone")); - etPetName.setText(getArguments().getString("petName")); - etAdoptionDate.setText(getArguments().getString("adoptionDate")); - String status = getArguments().getString("status"); - if ("Approved".equals(status)) spinnerAdoptionStatus.setSelection(0); - else if ("Pending".equals(status)) spinnerAdoptionStatus.setSelection(1); - else spinnerAdoptionStatus.setSelection(2); - btnDeleteAdoption.setVisibility(View.VISIBLE); + tvAdoptionId.setVisibility(View.VISIBLE); + etAdoptionDate.setText(a.getString("adoptionDate")); + btnDelete.setVisibility(View.VISIBLE); + + // Pre-fill status + String status = a.getString("adoptionStatus", "Pending"); + for (int i = 0; i < STATUSES.length; i++) { + if (STATUSES[i].equals(status)) { + spinnerStatus.setSelection(i); break; + } + } } else { - isEditing = false; tvMode.setText("Add Adoption"); + btnDelete.setVisibility(View.GONE); tvAdoptionId.setVisibility(View.GONE); - btnDeleteAdoption.setVisibility(View.GONE); - btnSaveAdoption.setText("Add"); } } - private void initViews(View view) { - tvMode = view.findViewById(R.id.tvAdoptionMode); - tvAdoptionId = view.findViewById(R.id.tvAdoptionId); - etAdopterName = view.findViewById(R.id.etAdopterName); - etAdopterEmail = view.findViewById(R.id.etAdopterEmail); - etAdopterPhone = view.findViewById(R.id.etAdopterPhone); - etPetName = view.findViewById(R.id.etAdoptionPetName); - etAdoptionDate = view.findViewById(R.id.etAdoptionDate); - spinnerAdoptionStatus = view.findViewById(R.id.spinnerAdoptionStatus); - btnSaveAdoption = view.findViewById(R.id.btnSaveAdoption); - btnDeleteAdoption = view.findViewById(R.id.btnDeleteAdoption); - btnBack = view.findViewById(R.id.btnAdoptionBack); + private void saveAdoption() { + if (spinnerCustomer.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; + } + if (spinnerPet.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return; + } + String date = etAdoptionDate.getText().toString().trim(); + if (date.isEmpty()) { + Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; + } + + CustomerDTO customer = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1); + PetDTO pet = petList.get(spinnerPet.getSelectedItemPosition() - 1); + String status = STATUSES[spinnerStatus.getSelectedItemPosition()]; + + AdoptionDTO dto = new AdoptionDTO( + pet.getPetId(), + customer.getCustomerId(), + date, + status + ); + + Log.d("ADOPTION_SAVE", "petId=" + pet.getPetId() + + " customerId=" + customer.getCustomerId() + + " date=" + date + " status=" + status); + + AdoptionApi api = RetrofitClient.getAdoptionApi(requireContext()); + if (isEditing) { + api.updateAdoption(adoptionId, dto).enqueue(simpleCallback("Updated")); + } else { + api.createAdoption(dto).enqueue(simpleCallback("Saved")); + } } - private void setupSpinner() { - ArrayAdapter adapter = new ArrayAdapter(requireContext(), - android.R.layout.simple_spinner_item, - new String[]{"Approved", "Pending", "Rejected"}) { - - //Override the getView method for the spinner to make the text color darker for more readability - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - View view = super.getView(position, convertView, parent); - ((TextView) view).setTextColor(ContextCompat.getColor(requireContext(), R.color.text_dark)); - return view; + private Callback simpleCallback(String msg) { + return new Callback<>() { + public void onResponse(Call c, Response r) { + Log.d("ADOPTION_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("ADOPTION_SAVE", "Error: " + err); + Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + Log.e("ADOPTION_SAVE", "Failed to read error"); + } + } + } + public void onFailure(Call c, Throwable t) { + Log.e("ADOPTION_SAVE", "Failure: " + t.getMessage()); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); } }; - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinnerAdoptionStatus.setAdapter(adapter); } -} + + private void confirmDelete() { + new AlertDialog.Builder(requireContext()) + .setTitle("Delete Adoption?") + .setPositiveButton("Yes", (d, w) -> + RetrofitClient.getAdoptionApi(requireContext()) + .deleteAdoption(adoptionId) + .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() { + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.getChildFragmentManager().popBackStack(); + } +} \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_adoption.xml b/android/app/src/main/res/layout/fragment_adoption.xml index 705b6fa2..492b9236 100644 --- a/android/app/src/main/res/layout/fragment_adoption.xml +++ b/android/app/src/main/res/layout/fragment_adoption.xml @@ -12,7 +12,6 @@ android:orientation="vertical"> + android:textColor="@color/white"/> @@ -65,74 +65,37 @@ android:layout_gravity="end" android:layout_marginBottom="8dp"/> + - + android:layout_marginBottom="16dp"/> + - - - - - - - - - + android:layout_marginBottom="16dp"/> + + android:hint="Tap to select date" + android:inputType="none" + android:focusable="false" + android:clickable="true" + android:drawableEnd="@android:drawable/ic_menu_my_calendar" + android:layout_marginBottom="16dp"/> + + android:layout_height="wrap_content" + android:layout_marginBottom="8dp"/> diff --git a/android/app/src/main/res/layout/item_adoption.xml b/android/app/src/main/res/layout/item_adoption.xml index 9cf3cd58..40b03b51 100644 --- a/android/app/src/main/res/layout/item_adoption.xml +++ b/android/app/src/main/res/layout/item_adoption.xml @@ -1,64 +1,78 @@ - - + android:layout_margin="8dp" + app:cardCornerRadius="8dp" + app:cardElevation="2dp"> + android:orientation="vertical" + android:padding="16dp"> - + android:orientation="horizontal"> - + + + + + + + + + + + + + + + - - - - - - - + \ No newline at end of file From 87a4404c20bb81afde7625322b904079b7f7f617 Mon Sep 17 00:00:00 2001 From: Nikitha Date: Sun, 29 Mar 2026 16:26:21 -0600 Subject: [PATCH 03/35] Inventory Inventory- details of product loads with id and described with filter, and categories selection --- .../adapters/InventoryAdapter.java | 125 ++++-- .../petstoremobile/api/InventoryApi.java | 46 ++ .../dtos/BulkDeleteRequest.java | 22 + .../petstoremobile/dtos/InventoryDTO.java | 77 ++++ .../petstoremobile/dtos/InventoryRequest.java | 31 ++ .../listfragments/InventoryFragment.java | 406 ++++++++++++++---- .../InventoryDetailFragment.java | 400 ++++++++++++----- .../main/res/layout/fragment_inventory.xml | 58 ++- .../res/layout/fragment_inventory_detail.xml | 84 ++-- .../src/main/res/layout/item_inventory.xml | 122 +++--- 10 files changed, 1028 insertions(+), 343 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java 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 7ae36dc5..63290ae7 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 @@ -1,79 +1,148 @@ package com.example.petstoremobile.adapters; - 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.models.Inventory; +import com.example.petstoremobile.dtos.InventoryDTO; + +import java.util.ArrayList; import java.util.List; public class InventoryAdapter extends RecyclerView.Adapter { - private List inventoryList; - private OnInventoryClickListener inventoryClickListener; + private final List inventoryList; + private final OnInventoryClickListener clickListener; + private final List selectedIds = new ArrayList<>(); + private boolean selectionMode = false; - // Interface for inventory click on recycler view public interface OnInventoryClickListener { void onInventoryClick(int position); + + void onSelectionChanged(int selectedCount); } - // Constructor - public InventoryAdapter(List inventoryList, OnInventoryClickListener inventoryClickListener) { + public InventoryAdapter(List inventoryList, OnInventoryClickListener clickListener) { this.inventoryList = inventoryList; - this.inventoryClickListener = inventoryClickListener; + this.clickListener = clickListener; } - // Get the controls of each row in recycler view public static class InventoryViewHolder extends RecyclerView.ViewHolder { - TextView tvItemName, tvCategory, tvQuantity, tvUnitPrice, tvSupplier; + // Matches desktop table columns: Inventory ID, Product ID, Product Name, + // Quantity + TextView tvInventoryId, tvProdId, tvProductName, tvQuantity; + CheckBox checkBox; public InventoryViewHolder(@NonNull View v) { super(v); - tvItemName = v.findViewById(R.id.tvItemName); - tvCategory = v.findViewById(R.id.tvCategory); + tvInventoryId = v.findViewById(R.id.tvInventoryId); + tvProdId = v.findViewById(R.id.tvProdId); + tvProductName = v.findViewById(R.id.tvProductName); tvQuantity = v.findViewById(R.id.tvQuantity); - tvUnitPrice = v.findViewById(R.id.tvUnitPrice); - tvSupplier = v.findViewById(R.id.tvInvSupplier); + checkBox = v.findViewById(R.id.cbSelectInventory); } } - // Create a new row view @NonNull @Override public InventoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_inventory, parent, false); + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_inventory, parent, false); return new InventoryViewHolder(v); } - // Populate the row with inventory data @Override public void onBindViewHolder(@NonNull InventoryViewHolder holder, int position) { - Inventory inventory = inventoryList.get(position); + InventoryDTO inv = inventoryList.get(position); - holder.tvItemName.setText(inventory.getItemName()); - holder.tvCategory.setText(inventory.getCategory()); - holder.tvQuantity.setText("Qty: " + inventory.getQuantity()); - holder.tvUnitPrice.setText("$" + String.format("%.2f", inventory.getUnitPrice())); - holder.tvSupplier.setText("Supplier: " + inventory.getSupplier()); + // Column: Inventory ID + holder.tvInventoryId.setText(String.valueOf(inv.getInventoryId() != null ? inv.getInventoryId() : "—")); - // Highlight low stock items in red - if (inventory.getQuantity() <= 5) { + // Column: Product ID + holder.tvProdId.setText(String.valueOf(inv.getProdId() != null ? inv.getProdId() : "—")); + + // Column: Product Name + holder.tvProductName.setText(inv.getProductName() != null ? inv.getProductName() : "—"); + + // Column: Quantity + int qty = inv.getQuantity() != null ? inv.getQuantity() : 0; + holder.tvQuantity.setText(String.valueOf(qty)); + + // Low stock = red, normal = green (like desktop reorder concept) + if (qty <= 5) { holder.tvQuantity.setTextColor(Color.parseColor("#F44336")); } else { holder.tvQuantity.setTextColor(Color.parseColor("#4CAF50")); } - // When a row is clicked, open the detail view - holder.itemView.setOnClickListener(v -> inventoryClickListener.onInventoryClick(position)); + // Bulk delete selection mode + if (selectionMode) { + holder.checkBox.setVisibility(View.VISIBLE); + holder.checkBox.setChecked(inv.getInventoryId() != null + && selectedIds.contains(inv.getInventoryId())); + } else { + holder.checkBox.setVisibility(View.GONE); + holder.checkBox.setChecked(false); + } + + holder.itemView.setOnClickListener(v -> { + if (selectionMode) { + toggleSelection(inv.getInventoryId(), holder.checkBox); + } else { + clickListener.onInventoryClick(holder.getAdapterPosition()); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!selectionMode) { + selectionMode = true; + toggleSelection(inv.getInventoryId(), holder.checkBox); + notifyDataSetChanged(); + } + 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/api/InventoryApi.java b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java new file mode 100644 index 00000000..f54616ee --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java @@ -0,0 +1,46 @@ +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.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface InventoryApi { + + // GET /api/v1/inventory?q=...&page=...&size=... + @GET("api/v1/inventory") + Call> getAllInventory( + @Query("q") String query, + @Query("page") int page, + @Query("size") int size, + @Query("sort") String sort); + + // GET /api/v1/inventory/{id} + @GET("api/v1/inventory/{id}") + Call getInventoryById(@Path("id") Long id); + + // POST /api/v1/inventory + @POST("api/v1/inventory") + Call createInventory(@Body InventoryRequest request); + + // PUT /api/v1/inventory/{id} + @PUT("api/v1/inventory/{id}") + Call updateInventory(@Path("id") Long id, @Body InventoryRequest 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") + Call bulkDeleteInventory(@Body BulkDeleteRequest request); +} 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 new file mode 100644 index 00000000..49f92f06 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java @@ -0,0 +1,22 @@ +package com.example.petstoremobile.dtos; + +import java.util.List; + +public class BulkDeleteRequest { + private List ids; + + public BulkDeleteRequest() { + } + + public BulkDeleteRequest(List ids) { + this.ids = ids; + } + + public List getIds() { + return ids; + } + + public void setIds(List ids) { + this.ids = ids; + } +} 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 new file mode 100644 index 00000000..fe2ec542 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java @@ -0,0 +1,77 @@ +package com.example.petstoremobile.dtos; + +public class InventoryDTO { + // Response fields (from backend InventoryResponse) + private Long inventoryId; + private Long prodId; + private String productName; + private String categoryName; + private Integer quantity; + private String createdAt; + private String updatedAt; + + public InventoryDTO() { + } + + // Constructor for create/update requests (matches InventoryRequest) + public InventoryDTO(Long prodId, Integer quantity) { + this.prodId = prodId; + this.quantity = quantity; + } + + public Long getInventoryId() { + return inventoryId; + } + + public void setInventoryId(Long inventoryId) { + this.inventoryId = inventoryId; + } + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public String getCategoryName() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName = categoryName; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public String getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; + } + + 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/InventoryRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java new file mode 100644 index 00000000..f84dfb5f --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java @@ -0,0 +1,31 @@ +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/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 38546e83..6eb27fb4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -1,162 +1,378 @@ package com.example.petstoremobile.fragments.listfragments; -// Added search/filter bar to filter inventory by item name or category. -// Added pull-to-refresh using SwipeRefreshLayout. - import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ImageButton; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.InventoryAdapter; +import com.example.petstoremobile.api.CategoryApi; +import com.example.petstoremobile.api.InventoryApi; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.detailfragments.InventoryDetailFragment; -import com.example.petstoremobile.models.Inventory; import com.google.android.material.floatingactionbutton.FloatingActionButton; + import java.util.ArrayList; import java.util.List; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener { - private List inventoryList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private static final String TAG = "InventoryFragment"; + private static final int PAGE_SIZE = 20; + + private final List inventoryList = new ArrayList<>(); + private final List categoryList = new ArrayList<>(); private InventoryAdapter adapter; + private InventoryApi inventoryApi; + private CategoryApi categoryApi; + private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; + private Spinner spinnerCategory; private ImageButton hamburger; + private Button btnBulkDelete; + private TextView tvSelectionCount; + + // Debounce search + private final Handler searchHandler = new Handler(Looper.getMainLooper()); + private Runnable searchRunnable; + private String currentQuery = ""; + + // Selected category filter — null means "All" + private String selectedCategory = null; + + // Pagination + private int currentPage = 0; + private boolean isLastPage = false; + private boolean isLoading = false; + + // Prevent spinner from firing on initial load + private boolean spinnerReady = false; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_inventory, container, false); - hamburger = view.findViewById(R.id.btnHamburger); + inventoryApi = RetrofitClient.getInventoryApi(requireContext()); + categoryApi = RetrofitClient.getCategoryApi(requireContext()); + + hamburger = view.findViewById(R.id.btnHamburger); + btnBulkDelete = view.findViewById(R.id.btnBulkDelete); + tvSelectionCount = view.findViewById(R.id.tvSelectionCount); + spinnerCategory = view.findViewById(R.id.spinnerCategory); - loadInventoryData(); // TODO: Replace with actual API call when backend is ready setupRecyclerView(view); setupSearch(view); setupSwipeRefresh(view); + loadCategories(); // loads categories then triggers loadInventory + loadInventory(true); - FloatingActionButton fabAddInventory = view.findViewById(R.id.fabAddInventory); - fabAddInventory.setOnClickListener(v -> openInventoryDetails(-1)); + view.findViewById(R.id.fabAddInventory) + .setOnClickListener(v -> openDetail(null)); - //Make the hamburger button open the drawer from listFragment hamburger.setOnClickListener(v -> { - ListFragment listFragment = (ListFragment) getParentFragment(); - //if list fragment is found then use its helper function to open the drawer - if (listFragment != null) { - listFragment.openDrawer(); - } + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) + lf.openDrawer(); }); + btnBulkDelete.setOnClickListener(v -> confirmBulkDelete()); + return view; } - // Filters inventory list by item name or category - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchInventory); - etSearch.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) { - filterInventory(s.toString()); + // Categories + private void loadCategories() { + categoryApi.getAllCategories(0, 100).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, + Response> response) { + if (response.isSuccessful() && response.body() != null) { + categoryList.clear(); + categoryList.addAll(response.body().getContent()); + setupCategorySpinner(); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + Log.e(TAG, "Failed to load categories", t); + // Still setup spinner with just "All" + setupCategorySpinner(); } - @Override public void afterTextChanged(Editable s) {} }); } - private void filterInventory(String query) { - filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(inventoryList); - } else { - String lower = query.toLowerCase(); - for (Inventory i : inventoryList) { - if (i.getItemName().toLowerCase().contains(lower) - || i.getCategory().toLowerCase().contains(lower) - || i.getSupplier().toLowerCase().contains(lower)) { - filteredList.add(i); + private void setupCategorySpinner() { + // First item is always "All Categories" + List categoryNames = new ArrayList<>(); + categoryNames.add("All Categories"); + for (CategoryDTO c : categoryList) { + categoryNames.add(c.getCategoryName()); + } + + ArrayAdapter spinnerAdapter = new ArrayAdapter<>( + requireContext(), + android.R.layout.simple_spinner_item, + categoryNames); + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerCategory.setAdapter(spinnerAdapter); + + spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (!spinnerReady) { + // Skip the first automatic trigger on setup + spinnerReady = true; + return; + } + if (position == 0) { + selectedCategory = null; // "All Categories" + } else { + selectedCategory = categoryList.get(position - 1).getCategoryName(); + } + loadInventory(true); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + } + + // Search + + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchInventory); + etSearch.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { + } + + @Override + public void afterTextChanged(Editable s) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (searchRunnable != null) + searchHandler.removeCallbacks(searchRunnable); + searchRunnable = () -> { + currentQuery = s.toString().trim(); + loadInventory(true); + }; + searchHandler.postDelayed(searchRunnable, 400); + } + }); + } + + // RecyclerView + infinite scroll + private void setupRecyclerView(View view) { + RecyclerView rv = view.findViewById(R.id.recyclerViewInventory); + adapter = new InventoryAdapter(inventoryList, this); + rv.setLayoutManager(new LinearLayoutManager(getContext())); + rv.setAdapter(adapter); + + rv.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (dy <= 0) + return; + LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager(); + if (lm == null) + return; + int visible = lm.getChildCount(); + int total = lm.getItemCount(); + int firstVis = lm.findFirstVisibleItemPosition(); + if (!isLoading && !isLastPage && (visible + firstVis) >= total - 3) { + loadInventory(false); } } - } - adapter.notifyDataSetChanged(); + }); } private void setupSwipeRefresh(View view) { swipeRefreshLayout = view.findViewById(R.id.swipeRefreshInventory); - swipeRefreshLayout.setOnRefreshListener(() -> { - loadInventoryData(); // TODO: Replace with actual API call - filterInventory(etSearch.getText().toString()); - swipeRefreshLayout.setRefreshing(false); - }); + swipeRefreshLayout.setOnRefreshListener(() -> loadInventory(true)); } - private void openInventoryDetails(int position) { - InventoryDetailFragment detailFragment = new InventoryDetailFragment(); + // Load inventory + private void loadInventory(boolean reset) { + if (isLoading) + return; + isLoading = true; + + if (reset) { + currentPage = 0; + isLastPage = false; + } + + // Build query: combine search text + selected category + String q = buildQuery(); + + inventoryApi.getAllInventory(q, currentPage, PAGE_SIZE, "inventoryId,asc") + .enqueue(new Callback>() { + @Override + public void onResponse(Call> call, + Response> response) { + isLoading = false; + if (swipeRefreshLayout != null) + swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null) { + PageResponse page = response.body(); + if (reset) + inventoryList.clear(); + inventoryList.addAll(page.getContent()); + adapter.notifyDataSetChanged(); + isLastPage = page.isLast(); + if (!isLastPage) + currentPage++; + } else { + Log.e(TAG, "Error " + response.code()); + Toast.makeText(getContext(), "Failed to load inventory", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + isLoading = false; + if (swipeRefreshLayout != null) + swipeRefreshLayout.setRefreshing(false); + Log.e(TAG, "Network error", t); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + + // Combines search text and category into one query string for ?q= + private String buildQuery() { + String q = null; + if (!currentQuery.isEmpty() && selectedCategory != null) { + // Both active — prioritize search text, category acts as context + q = currentQuery; + } else if (!currentQuery.isEmpty()) { + q = currentQuery; + } else if (selectedCategory != null) { + q = selectedCategory; + } + return q; + } + + // Bulk delete + 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(); + } + + private void bulkDelete(List ids) { + inventoryApi.bulkDeleteInventory(new BulkDeleteRequest(ids)) + .enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + adapter.clearSelection(); + hideBulkDeleteBar(); + loadInventory(true); + Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void hideBulkDeleteBar() { + if (btnBulkDelete != null) + btnBulkDelete.setVisibility(View.GONE); + if (tvSelectionCount != null) + tvSelectionCount.setVisibility(View.GONE); + } + + // Navigation + private void openDetail(InventoryDTO inv) { + InventoryDetailFragment detail = new InventoryDetailFragment(); Bundle args = new Bundle(); - args.putInt("position", position); - if (position != -1) { - Inventory inventory = filteredList.get(position); - int realPosition = inventoryList.indexOf(inventory); - args.putInt("position", realPosition); - args.putInt("inventoryId", inventory.getInventoryId()); - args.putString("itemName", inventory.getItemName()); - args.putString("category", inventory.getCategory()); - args.putInt("quantity", inventory.getQuantity()); - args.putDouble("unitPrice", inventory.getUnitPrice()); - args.putString("supplier", inventory.getSupplier()); + if (inv != null) { + args.putLong("inventoryId", inv.getInventoryId()); + args.putLong("prodId", inv.getProdId() != null ? inv.getProdId() : -1); + args.putString("productName", inv.getProductName()); + args.putString("categoryName", inv.getCategoryName()); + args.putInt("quantity", inv.getQuantity() != null ? inv.getQuantity() : 0); } - detailFragment.setArguments(args); - detailFragment.setInventoryFragment(this); + detail.setArguments(args); + detail.setInventoryFragment(this); - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.loadFragment(detailFragment); + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) + lf.loadFragment(detail); } - public void onInventorySaved(int position, Inventory inventory) { - if (position == -1) { - inventoryList.add(inventory); - } else { - inventoryList.set(position, inventory); - } - filterInventory(etSearch.getText().toString()); + public void onInventoryChanged() { + loadInventory(true); } - public void onInventoryDeleted(int position) { - inventoryList.remove(position); - filterInventory(etSearch.getText().toString()); - } + // Adapter callbacks @Override public void onInventoryClick(int position) { - openInventoryDetails(position); + if (position >= 0 && position < inventoryList.size()) { + openDetail(inventoryList.get(position)); + } } - private void loadInventoryData() { - inventoryList.clear(); - inventoryList.add(new Inventory(1, "Dog Food - Large", "Food", 50, 25.99, "PetSupplies Co.")); - inventoryList.add(new Inventory(2, "Cat Litter", "Hygiene", 30, 12.99, "CleanPaws Ltd.")); - inventoryList.add(new Inventory(3, "Dog Leash", "Accessories", 4, 15.99, "PetGear Inc.")); - inventoryList.add(new Inventory(4, "Bird Cage - Medium", "Housing", 8, 79.99, "BirdWorld")); - inventoryList.add(new Inventory(5, "Flea Treatment", "Medicine", 2, 34.99, "VetCare Supply")); - filteredList.clear(); - filteredList.addAll(inventoryList); + @Override + public void onSelectionChanged(int selectedCount) { + if (selectedCount > 0) { + btnBulkDelete.setVisibility(View.VISIBLE); + tvSelectionCount.setVisibility(View.VISIBLE); + tvSelectionCount.setText(selectedCount + " selected"); + } else { + hideBulkDeleteBar(); + } } - - private void setupRecyclerView(View view) { - RecyclerView recyclerView = view.findViewById(R.id.recyclerViewInventory); - adapter = new InventoryAdapter(filteredList, this); - recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(adapter); - } -} +} \ No newline at end of file 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 175feb91..9846bb36 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,34 +1,65 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; -// Uses InputValidator for detailed field validation and ActivityLogger to log all changes. - import android.os.Bundle; -import androidx.fragment.app.Fragment; +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.AutoCompleteTextView; import android.widget.Button; -import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + import com.example.petstoremobile.R; +import com.example.petstoremobile.api.InventoryApi; +import com.example.petstoremobile.api.ProductApi; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.InventoryRequest; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.InventoryFragment; -import com.example.petstoremobile.models.Inventory; -import com.example.petstoremobile.utils.ActivityLogger; -import com.example.petstoremobile.utils.InputValidator; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; public class InventoryDetailFragment extends Fragment { - private TextView tvMode, tvInventoryId; - private EditText etItemName, etCategory, etQuantity, etUnitPrice, etSupplier; - private Button btnSaveInventory, btnDeleteInventory, btnBack; - private int inventoryId; - private int position; - private boolean isEditing = false; + private TextView tvMode, tvInventoryId, tvProductInfo; + private AutoCompleteTextView etProductSearch; + private android.widget.EditText etQuantity; + private Button btnSave, btnDelete, btnBack; + + private InventoryApi inventoryApi; + private ProductApi productApi; private InventoryFragment inventoryFragment; - // Set the inventory fragment as parent so we refer back when save or delete is done + private boolean isEditing = false; + private long inventoryId = -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; + public void setInventoryFragment(InventoryFragment fragment) { this.inventoryFragment = fragment; } @@ -38,102 +69,271 @@ public class InventoryDetailFragment extends Fragment { Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_inventory_detail, container, false); + inventoryApi = RetrofitClient.getInventoryApi(requireContext()); + productApi = RetrofitClient.getProductApi(requireContext()); + initViews(view); + setupProductSearch(); handleArguments(); - btnBack.setOnClickListener(v -> { - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); - }); - btnSaveInventory.setOnClickListener(v -> saveInventory()); - btnDeleteInventory.setOnClickListener(v -> deleteInventory()); + btnBack.setOnClickListener(v -> navigateBack()); + btnSave.setOnClickListener(v -> saveInventory()); + btnDelete.setOnClickListener(v -> confirmDelete()); return view; } - // Validates all fields using InputValidator, then saves the inventory item - private void saveInventory() { - if (!InputValidator.isNotEmpty(etItemName, "Item Name")) return; - if (!InputValidator.isNotEmpty(etCategory, "Category")) return; - if (!InputValidator.isPositiveInteger(etQuantity, "Quantity")) return; - if (!InputValidator.isPositiveDecimal(etUnitPrice, "Unit Price")) return; - if (!InputValidator.isNotEmpty(etSupplier, "Supplier")) return; - - String itemName = etItemName.getText().toString().trim(); - String category = etCategory.getText().toString().trim(); - int quantity = Integer.parseInt(etQuantity.getText().toString().trim()); - double unitPrice = Double.parseDouble(etUnitPrice.getText().toString().trim()); - String supplier = etSupplier.getText().toString().trim(); - - try { - if (isEditing) { - // TODO: Replace with actual API PUT call when backend is ready - Inventory updated = new Inventory(inventoryId, itemName, category, quantity, unitPrice, supplier); - if (inventoryFragment != null) inventoryFragment.onInventorySaved(position, updated); - ActivityLogger.logChange(requireContext(), "Inventory", "UPDATED", inventoryId); - Toast.makeText(getContext(), "Inventory item updated.", Toast.LENGTH_SHORT).show(); - } else { - // TODO: Replace with actual API POST call when backend is ready - Inventory newItem = new Inventory(0, itemName, category, quantity, unitPrice, supplier); - if (inventoryFragment != null) inventoryFragment.onInventorySaved(-1, newItem); - ActivityLogger.log(requireContext(), "Added new Inventory item: " + itemName); - Toast.makeText(getContext(), "Inventory item added.", Toast.LENGTH_SHORT).show(); - } - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); - } catch (Exception e) { - ActivityLogger.logException(requireContext(), "InventoryDetailFragment.saveInventory", e); - Toast.makeText(getContext(), "Error saving inventory item.", Toast.LENGTH_SHORT).show(); - } - } - - // Deletes the inventory item and logs the action - private void deleteInventory() { - try { - // TODO: Replace with actual API DELETE call when backend is ready - if (inventoryFragment != null) inventoryFragment.onInventoryDeleted(position); - ActivityLogger.logChange(requireContext(), "Inventory", "DELETED", inventoryId); - Toast.makeText(getContext(), "Inventory item deleted.", Toast.LENGTH_SHORT).show(); - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); - } catch (Exception e) { - ActivityLogger.logException(requireContext(), "InventoryDetailFragment.deleteInventory", e); - Toast.makeText(getContext(), "Error deleting inventory item.", Toast.LENGTH_SHORT).show(); - } - } - - private void handleArguments() { - if (getArguments() != null && getArguments().containsKey("inventoryId")) { - isEditing = true; - inventoryId = getArguments().getInt("inventoryId"); - position = getArguments().getInt("position"); - tvMode.setText("Edit Inventory Item"); - tvInventoryId.setText("ID: " + inventoryId); - etItemName.setText(getArguments().getString("itemName")); - etCategory.setText(getArguments().getString("category")); - etQuantity.setText(String.valueOf(getArguments().getInt("quantity"))); - etUnitPrice.setText(String.valueOf(getArguments().getDouble("unitPrice"))); - etSupplier.setText(getArguments().getString("supplier")); - btnDeleteInventory.setVisibility(View.VISIBLE); - } else { - isEditing = false; - tvMode.setText("Add Inventory Item"); - tvInventoryId.setVisibility(View.GONE); - btnDeleteInventory.setVisibility(View.GONE); - btnSaveInventory.setText("Add"); - } - } - private void initViews(View view) { tvMode = view.findViewById(R.id.tvInventoryMode); tvInventoryId = view.findViewById(R.id.tvInventoryId); - etItemName = view.findViewById(R.id.etItemName); - etCategory = view.findViewById(R.id.etInventoryCategory); + tvProductInfo = view.findViewById(R.id.tvProductInfo); + etProductSearch = view.findViewById(R.id.etProductSearch); etQuantity = view.findViewById(R.id.etQuantity); - etUnitPrice = view.findViewById(R.id.etUnitPrice); - etSupplier = view.findViewById(R.id.etInventorySupplier); - btnSaveInventory = view.findViewById(R.id.btnSaveInventory); - btnDeleteInventory = view.findViewById(R.id.btnDeleteInventory); + btnSave = view.findViewById(R.id.btnSaveInventory); + btnDelete = view.findViewById(R.id.btnDeleteInventory); btnBack = view.findViewById(R.id.btnInventoryBack); + + // Setup dropdown adapter + dropdownAdapter = new ArrayAdapter<>(requireContext(), + android.R.layout.simple_dropdown_item_1line, new ArrayList<>()); + etProductSearch.setAdapter(dropdownAdapter); + etProductSearch.setThreshold(1); // start showing after 1 character } -} + + // Product search dropdown + private void setupProductSearch() { + etProductSearch.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { + } + + @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; + 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 + etProductSearch.setOnItemClickListener((parent, view, position, id) -> { + if (position < productSuggestions.size()) { + selectedProduct = productSuggestions.get(position); + // Show product details below the search box + tvProductInfo.setText( + "ID: " + selectedProduct.getProdId() + + " • " + selectedProduct.getCategoryName()); + tvProductInfo.setVisibility(View.VISIBLE); + } + }); + } + + private void searchProducts(String query) { + productApi.getAllProducts(query, 0, 20).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, + Response> response) { + if (response.isSuccessful() && response.body() != null) { + productSuggestions.clear(); + productSuggestions.addAll(response.body().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(); + etProductSearch.showDropDown(); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + Toast.makeText(getContext(), "Failed to load products", Toast.LENGTH_SHORT).show(); + } + }); + } + + // Arguments (edit mode) + + private void handleArguments() { + Bundle args = getArguments(); + if (args != null && args.containsKey("inventoryId")) { + isEditing = true; + inventoryId = args.getLong("inventoryId"); + + tvMode.setText("Edit Inventory"); + tvInventoryId.setText("Inventory ID: " + inventoryId); + tvInventoryId.setVisibility(View.VISIBLE); + + // Pre-fill search box with existing product name + String productName = args.getString("productName", ""); + long prodId = args.getLong("prodId", -1); + etProductSearch.setText(productName); + + // Show existing product info + if (prodId != -1) { + tvProductInfo.setText( + "ID: " + prodId + + " • " + args.getString("categoryName", "")); + tvProductInfo.setVisibility(View.VISIBLE); + + // Build a minimal ProductDTO so selectedProduct is not null on save + selectedProduct = new ProductDTO(productName, null, null, null); + selectedProduct.setProdId(prodId); + } + + etQuantity.setText(String.valueOf(args.getInt("quantity", 0))); + btnDelete.setVisibility(View.VISIBLE); + btnSave.setText("Save"); + } else { + isEditing = false; + tvMode.setText("Add Inventory"); + tvInventoryId.setVisibility(View.GONE); + tvProductInfo.setVisibility(View.GONE); + btnDelete.setVisibility(View.GONE); + btnSave.setText("Add"); + } + } + + // Save + private void saveInventory() { + if (selectedProduct == null) { + etProductSearch.setError("Please select a product from the list"); + etProductSearch.requestFocus(); + return; + } + + String quantityStr = etQuantity.getText().toString().trim(); + if (quantityStr.isEmpty()) { + etQuantity.setError("Quantity is required"); + etQuantity.requestFocus(); + return; + } + + int quantity; + try { + quantity = Integer.parseInt(quantityStr); + } catch (NumberFormatException e) { + etQuantity.setError("Invalid quantity"); + return; + } + + if (quantity < 0) { + etQuantity.setError("Quantity must be 0 or more"); + etQuantity.requestFocus(); + return; + } + + InventoryRequest request = new InventoryRequest(selectedProduct.getProdId(), quantity); + setButtonsEnabled(false); + + if (isEditing) { + inventoryApi.updateInventory(inventoryId, request).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + setButtonsEnabled(true); + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show(); + notifyParentAndGoBack(); + } else { + Toast.makeText(getContext(), "Update failed: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + setButtonsEnabled(true); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } else { + inventoryApi.createInventory(request).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + setButtonsEnabled(true); + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show(); + notifyParentAndGoBack(); + } else { + Toast.makeText(getContext(), "Create failed: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + setButtonsEnabled(true); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + } + + // Delete + private void confirmDelete() { + new AlertDialog.Builder(requireContext()) + .setTitle("Delete inventory item?") + .setMessage("This cannot be undone.") + .setPositiveButton("Delete", (d, w) -> deleteInventory()) + .setNegativeButton("Cancel", null) + .show(); + } + + private void deleteInventory() { + setButtonsEnabled(false); + inventoryApi.deleteInventory(inventoryId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + setButtonsEnabled(true); + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); + notifyParentAndGoBack(); + } else { + Toast.makeText(getContext(), "Delete failed: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + setButtonsEnabled(true); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + + // Helpers + + private void notifyParentAndGoBack() { + if (inventoryFragment != null) + inventoryFragment.onInventoryChanged(); + navigateBack(); + } + + private void navigateBack() { + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) + lf.getChildFragmentManager().popBackStack(); + } + + private void setButtonsEnabled(boolean enabled) { + btnSave.setEnabled(enabled); + btnDelete.setEnabled(enabled); + btnBack.setEnabled(enabled); + } +} \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_inventory.xml b/android/app/src/main/res/layout/fragment_inventory.xml index 7773c8da..d149214d 100644 --- a/android/app/src/main/res/layout/fragment_inventory.xml +++ b/android/app/src/main/res/layout/fragment_inventory.xml @@ -11,6 +11,7 @@ android:layout_height="match_parent" android:orientation="vertical"> + + + + + + + + + + + - + @@ -151,16 +151,16 @@ - + - - - - - - - - + + + + + + + + From a3851871c7d3026251ed83a55a38a3e44e78d531 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 30 Mar 2026 09:17:22 -0600 Subject: [PATCH 34/35] Stabilize refunds --- .../api/dto/common/PageResponse.java | 66 ++++++- .../petshopdesktop/api/endpoints/SaleApi.java | 33 ++++ .../RefundDialogController.java | 162 ++++++++++++------ .../dialogviews/refund-dialog-view.fxml | 62 +++---- 4 files changed, 236 insertions(+), 87 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java index bbcc467c..5a27e2e9 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java @@ -9,15 +9,14 @@ import java.util.List; public class PageResponse { private List content; - @JsonProperty("number") private int pageNumber; - @JsonProperty("size") private int pageSize; private long totalElements; private int totalPages; private boolean last; + private PageMetadata page; public PageResponse() { } @@ -63,10 +62,71 @@ public class PageResponse { } public boolean isLast() { - return last; + if (last) { + return true; + } + if (page != null) { + return page.number >= Math.max(0, page.totalPages - 1); + } + return content == null || content.isEmpty(); } public void setLast(boolean last) { this.last = last; } + + public PageMetadata getPage() { + return page; + } + + public void setPage(PageMetadata page) { + this.page = page; + if (page != null) { + this.pageNumber = page.number; + this.pageSize = page.size; + this.totalElements = page.totalElements; + this.totalPages = page.totalPages; + this.last = page.number >= Math.max(0, page.totalPages - 1); + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class PageMetadata { + private int size; + private int number; + private long totalElements; + private int totalPages; + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + public long getTotalElements() { + return totalElements; + } + + public void setTotalElements(long totalElements) { + this.totalElements = totalElements; + } + + public int getTotalPages() { + return totalPages; + } + + public void setTotalPages(int totalPages) { + this.totalPages = totalPages; + } + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java index d3355012..a087fd9f 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java @@ -38,6 +38,39 @@ public class SaleApi { return pageResponse.getContent(); } + public List listAllSales(String query) throws Exception { + int page = 0; + int size = 250; + List allSales = new java.util.ArrayList<>(); + + while (true) { + String path = "/api/v1/sales?page=" + page + "&size=" + size; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from sales endpoint"); + } + + if (pageResponse.getContent() != null) { + allSales.addAll(pageResponse.getContent()); + } + + if (pageResponse.isLast()) { + break; + } + page++; + } + + return allSales; + } + public SaleResponse getSale(Long id) throws Exception { return apiClient.get("/api/v1/sales/" + id, SaleResponse.class); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java index 860a9921..bc262e30 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java @@ -4,8 +4,12 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.concurrent.Task; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.ReadOnlyStringWrapper; import javafx.scene.control.*; import javafx.scene.control.cell.PropertyValueFactory; +import javafx.application.Platform; import javafx.stage.Stage; import org.example.petshopdesktop.api.dto.sale.SaleItemRequest; import org.example.petshopdesktop.api.dto.sale.SaleItemResponse; @@ -90,6 +94,7 @@ public class RefundDialogController { private final ObservableList originalItems = FXCollections.observableArrayList(); private final ObservableList refundItems = FXCollections.observableArrayList(); private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); + private boolean refundInProgress; @FXML public void initialize() { @@ -100,17 +105,19 @@ public class RefundDialogController { } private void setupTables() { - colOriginalProduct.setCellValueFactory(new PropertyValueFactory<>("productName")); - colOriginalQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); - colOriginalUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); - colOriginalTotal.setCellValueFactory(new PropertyValueFactory<>("lineTotal")); + tvOriginalItems.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + colOriginalProduct.setCellValueFactory(cell -> new ReadOnlyStringWrapper(cell.getValue().getProductName())); + colOriginalQuantity.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getQuantity())); + colOriginalUnitPrice.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getUnitPrice())); + colOriginalTotal.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getLineTotal())); tvOriginalItems.setItems(originalItems); tvOriginalItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); - colRefundProduct.setCellValueFactory(new PropertyValueFactory<>("productName")); - colRefundQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); - colRefundUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); - colRefundTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + tvRefundItems.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + colRefundProduct.setCellValueFactory(cell -> new ReadOnlyStringWrapper(cell.getValue().getProductName())); + colRefundQuantity.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getQuantity())); + colRefundUnitPrice.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getUnitPrice())); + colRefundTotal.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getTotal())); tvRefundItems.setItems(refundItems); tvRefundItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); } @@ -125,12 +132,13 @@ public class RefundDialogController { return; } txtSaleId.setText(String.valueOf(saleId)); - loadSale(); + Platform.runLater(this::loadSale); } private void loadSale() { String saleIdText = txtSaleId.getText().trim(); if (saleIdText.isEmpty()) { + clearLoadedSale(); showError("Load Sale", "Enter a transaction ID."); return; } @@ -139,22 +147,36 @@ public class RefundDialogController { try { saleId = Long.parseLong(saleIdText); } catch (NumberFormatException e) { + clearLoadedSale(); showError("Load Sale", "Invalid transaction ID."); return; } - try { - List allSales = SaleApi.getInstance().listSales(0, 1000, null); - currentSale = SaleApi.getInstance().getSale(saleId); - if (Boolean.TRUE.equals(currentSale.getIsRefund())) { - clearLoadedSale(); - showError("Load Sale", "Select an original sale, not a refund record."); - return; - } - List previousRefunds = allSales.stream() - .filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId())) - .collect(Collectors.toList()); + setLoadingState(true, "Loading sale..."); + clearLoadedSale(); + Task task = new Task<>() { + @Override + protected LoadedSaleData call() throws Exception { + List allSales = SaleApi.getInstance().listAllSales(null); + SaleResponse sale = SaleApi.getInstance().getSale(saleId); + if (Boolean.TRUE.equals(sale.getIsRefund())) { + throw new IllegalStateException("Select an original sale, not a refund record."); + } + List previousRefunds = allSales.stream() + .filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId())) + .collect(Collectors.toList()); + List refundableItems = buildRefundableItems(sale, previousRefunds); + if (refundableItems.isEmpty()) { + throw new IllegalStateException("This sale has no remaining refundable items."); + } + return new LoadedSaleData(sale, refundableItems); + } + }; + + task.setOnSucceeded(event -> { + LoadedSaleData loaded = task.getValue(); + currentSale = loaded.sale(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); String saleInfo = String.format("Sale Date: %s | Employee: %s | Original Total: %s | Payment: %s", currentSale.getSaleDate().format(formatter), @@ -162,26 +184,25 @@ public class RefundDialogController { currency.format(currentSale.getTotalAmount()), currentSale.getPaymentMethod()); lblSaleInfo.setText(saleInfo); - - List refundableItems = buildRefundableItems(currentSale, previousRefunds); - if (refundableItems.isEmpty()) { - showError("Load Sale", "This sale has no remaining refundable items."); - return; - } - baseOriginalItems.clear(); - baseOriginalItems.addAll(copySaleItems(refundableItems)); - originalItems.setAll(copySaleItems(refundableItems)); + baseOriginalItems.addAll(copySaleItems(loaded.refundableItems())); + originalItems.setAll(copySaleItems(loaded.refundableItems())); cbPaymentMethod.getSelectionModel().select(currentSale.getPaymentMethod()); - refundItems.clear(); updateOriginalItemAvailability(); updateRefundTotal(); + setLoadingState(false, saleInfo); + }); - } catch (Exception e) { - ActivityLogger.getInstance().logException("RefundDialogController.btnLoadSaleClicked", e, "Loading sale"); + task.setOnFailed(event -> { + clearLoadedSale(); + Throwable e = task.getException(); + ActivityLogger.getInstance().logException("RefundDialogController.btnLoadSaleClicked", (Exception) e, "Loading sale"); + setLoadingState(false, ""); showError("Load Sale", e.getMessage() != null ? e.getMessage() : "Could not load sale."); - } + }); + + new Thread(task).start(); } @FXML @@ -248,6 +269,9 @@ public class RefundDialogController { @FXML void btnProcessRefundClicked(ActionEvent event) { + if (refundInProgress) { + return; + } if (currentSale == null) { showError("Process Refund", "Load a sale first."); return; @@ -280,36 +304,53 @@ public class RefundDialogController { return; } - try { - SaleRequest request = new SaleRequest(); - request.setStoreId(storeId); - request.setPaymentMethod(payment); - request.setIsRefund(true); - request.setOriginalSaleId(currentSale.getSaleId()); + SaleRequest request = new SaleRequest(); + request.setStoreId(storeId); + request.setPaymentMethod(payment); + request.setIsRefund(true); + request.setOriginalSaleId(currentSale.getSaleId()); - List items = new ArrayList<>(); - for (RefundItem item : refundItems) { - SaleItemRequest saleItem = new SaleItemRequest(); - saleItem.setProdId((long) item.getProdId()); - saleItem.setQuantity(-item.getQuantity()); - items.add(saleItem); + List items = new ArrayList<>(); + for (RefundItem item : refundItems) { + SaleItemRequest saleItem = new SaleItemRequest(); + saleItem.setProdId((long) item.getProdId()); + saleItem.setQuantity(-item.getQuantity()); + items.add(saleItem); + } + request.setItems(items); + + refundInProgress = true; + setLoadingState(true, lblSaleInfo.getText()); + + Task task = new Task<>() { + @Override + protected SaleResponse call() throws Exception { + return SaleApi.getInstance().createSale(request); } - request.setItems(items); - - SaleResponse refundResponse = SaleApi.getInstance().createSale(request); + }; + task.setOnSucceeded(evt -> { + refundInProgress = false; + setLoadingState(false, lblSaleInfo.getText()); + SaleResponse refundResponse = task.getValue(); Alert success = new Alert(Alert.AlertType.INFORMATION); success.setTitle("Refund Processed"); success.setHeaderText(null); success.setContentText("Refund ID " + refundResponse.getSaleId() + " was created successfully."); success.showAndWait(); - closeDialog(); + }); - } catch (Exception e) { - ActivityLogger.getInstance().logException("RefundDialogController.btnProcessRefundClicked", e, "Processing refund"); - showError("Process Refund", e.getMessage() != null ? e.getMessage() : "Could not process refund."); - } + task.setOnFailed(evt -> { + refundInProgress = false; + setLoadingState(false, lblSaleInfo.getText()); + Throwable e = task.getException(); + Exception ex = e instanceof Exception ? (Exception) e : new RuntimeException(e); + ActivityLogger.getInstance().logException("RefundDialogController.btnProcessRefundClicked", ex, "Processing refund"); + showError("Process Refund", e != null && e.getMessage() != null ? e.getMessage() : "Could not process refund."); + }); + + new Thread(task).start(); } @FXML @@ -326,6 +367,18 @@ public class RefundDialogController { updateRefundTotal(); } + private void setLoadingState(boolean loading, String message) { + btnLoadSale.setDisable(loading); + btnAddToRefund.setDisable(loading); + btnRemoveFromRefund.setDisable(loading); + btnProcessRefund.setDisable(loading); + txtSaleId.setDisable(loading); + cbPaymentMethod.setDisable(loading); + if (loading) { + lblSaleInfo.setText(message); + } + } + private void addOrMergeRefundItem(SaleItemResponse selected, int quantity) { for (int i = 0; i < refundItems.size(); i++) { RefundItem existing = refundItems.get(i); @@ -486,4 +539,7 @@ public class RefundDialogController { return quantity * unitPrice; } } + + private record LoadedSaleData(SaleResponse sale, List refundableItems) { + } } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml index b349a1d9..aad2f4c7 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml @@ -13,7 +13,7 @@ - + @@ -84,14 +84,14 @@ - - - - - - - - + From 4ef913dfd025a3a776bfac723f0ebe24dd0bef1a Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 30 Mar 2026 09:40:22 -0600 Subject: [PATCH 35/35] Fix refund display --- .../controllers/SaleController.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index 7e80c531..22c96dbd 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -258,18 +258,23 @@ public class SaleController { if (sale.getItems() != null && !sale.getItems().isEmpty()) { for (SaleItemResponse item : sale.getItems()) { + boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund(); double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0; - double lineTotal = unitPrice * item.getQuantity(); + int quantity = item.getQuantity() != null ? item.getQuantity() : 0; + if (isRefund && quantity > 0) { + quantity = -quantity; + } + double lineTotal = unitPrice * quantity; lineItems.add(new SaleLineItem( sale.getSaleId().intValue(), saleDate, sale.getEmployeeName(), item.getProductName(), - item.getQuantity(), + quantity, unitPrice, lineTotal, sale.getPaymentMethod(), - sale.getIsRefund() != null && sale.getIsRefund() + isRefund )); } } @@ -501,9 +506,13 @@ public class SaleController { private SaleDetail mapToSaleDetail(SaleResponse sale) { ObservableList items = FXCollections.observableArrayList(); if (sale.getItems() != null) { + boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund(); for (SaleItemResponse item : sale.getItems()) { double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0; int quantity = item.getQuantity() != null ? item.getQuantity() : 0; + if (isRefund && quantity > 0) { + quantity = -quantity; + } items.add(new SaleDetail.SaleDetailItem( item.getProdId() != null ? item.getProdId().intValue() : 0, item.getProductName(),