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