Appointments

validation, time slots for booking appointment , date, store. connected to service Type, pet name, customer name. Loads from database
This commit is contained in:
Nikitha
2026-03-29 16:20:38 -06:00
parent 79f8514f3e
commit 7e832a139f
8 changed files with 786 additions and 246 deletions

View File

@@ -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<AppointmentAdapter.AppointmentViewHolder> {
private List<Appointment> appointmentList;
private List<AppointmentDTO> appointmentList;
private OnAppointmentClickListener appointmentClickListener;
// Interface for appointment click on recycler view
public interface OnAppointmentClickListener {
void onAppointmentClick(int position);
}
// Constructor
public AppointmentAdapter(List<Appointment> appointmentList, OnAppointmentClickListener appointmentClickListener) {
public AppointmentAdapter(List<AppointmentDTO> 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<AppointmentAdapter.
}
}
// Create a new row view
@NonNull
@Override
public AppointmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
@@ -51,31 +46,34 @@ public class AppointmentAdapter extends RecyclerView.Adapter<AppointmentAdapter.
return new AppointmentViewHolder(v);
}
// Populate the row with appointment data
@Override
public void onBindViewHolder(@NonNull AppointmentViewHolder holder, int position) {
Appointment appointment = appointmentList.get(position);
AppointmentDTO a = appointmentList.get(position);
holder.tvCustomerName.setText(appointment.getCustomerName());
holder.tvPetName.setText("Pet: " + appointment.getPetName());
holder.tvServiceType.setText(appointment.getServiceType());
holder.tvDateTime.setText(appointment.getAppointmentDate() + " at " + appointment.getAppointmentTime());
holder.tvAppointmentStatus.setText(appointment.getStatus());
holder.tvCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : "");
holder.tvPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : ""));
holder.tvServiceType.setText(a.getServiceType() != null ? a.getServiceType() : "");
holder.tvDateTime.setText((a.getAppointmentDate() != null ? a.getAppointmentDate() : "") +
" at " + (a.getAppointmentTime() != null ? a.getAppointmentTime() : ""));
// Set the status color depending on appointment status
switch (appointment.getStatus()) {
case "Confirmed":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
String status = a.getStatus() != null ? a.getStatus() : "";
holder.tvAppointmentStatus.setText(status);
switch (status) {
case "Booked":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#2196F3")); // blue
break;
case "Pending":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#FF9800"));
case "Completed":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50")); // green
break;
case "Cancelled":
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336")); // red
break;
default:
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336"));
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#9E9E9E")); // gray
break;
}
// When a row is clicked, open the detail view
holder.itemView.setOnClickListener(v -> appointmentClickListener.onAppointmentClick(position));
}

View File

@@ -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<PageResponse<AppointmentDTO>> getAllAppointments(
@Query("page") int page,
@Query("size") int size);
@GET("api/v1/appointments/{id}")
Call<AppointmentDTO> getAppointmentById(@Path("id") Long id);
@POST("api/v1/appointments")
Call<AppointmentDTO> createAppointment(@Body AppointmentDTO appointment);
@PUT("api/v1/appointments/{id}")
Call<AppointmentDTO> updateAppointment(@Path("id") Long id, @Body AppointmentDTO appointment);
@DELETE("api/v1/appointments/{id}")
Call<Void> deleteAppointment(@Path("id") Long id);
}

View File

@@ -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<String> petNames;
private List<Long> 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<Long> 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<String> getPetNames() {
return petNames;
}
public List<Long> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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;
}

View File

@@ -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<Appointment> appointmentList = new ArrayList<>(); // full data list
private List<Appointment> filteredList = new ArrayList<>(); // filtered display list
private List<AppointmentDTO> appointmentList = new ArrayList<>();
private List<AppointmentDTO> filteredList = new ArrayList<>();
private List<PetDTO> petList = new ArrayList<>();
private List<ServiceDTO> 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<PageResponse<AppointmentDTO>>() {
@Override
public void onResponse(Call<PageResponse<AppointmentDTO>> call,
Response<PageResponse<AppointmentDTO>> 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<PageResponse<AppointmentDTO>> 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<PageResponse<PetDTO>>() {
@Override
public void onResponse(Call<PageResponse<PetDTO>> call, Response<PageResponse<PetDTO>> response) {
if (response.isSuccessful() && response.body() !=null) {
petList.clear();
petList.addAll(response.body().getContent());
}
}
@Override
public void onFailure(Call<PageResponse<PetDTO>> 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<PageResponse<ServiceDTO>>() {
@Override
public void onResponse(Call<PageResponse<ServiceDTO>> call, Response<PageResponse<ServiceDTO>> response) {
if (response.isSuccessful() && response.body() != null) {
serviceList.clear();
serviceList.addAll(response.body().getContent());
}
}
@Override
public void onFailure(Call<PageResponse<ServiceDTO>> 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);
}

View File

@@ -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<PetDTO> petList = new ArrayList<>();
private List<ServiceDTO> serviceList = new ArrayList<>();
private List<CustomerDTO> customerList = new ArrayList<>();
private List<StoreDTO> storeList = new ArrayList<>();
private List<AppointmentDTO> 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<PageResponse<PetDTO>>() {
public void onResponse(Call<PageResponse<PetDTO>> c, Response<PageResponse<PetDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
petList = r.body().getContent();
populatePetSpinner();
}
}
public void onFailure(Call<PageResponse<PetDTO>> c, Throwable t) {
Log.e("APPT", "Pet load failed: " + t.getMessage());
}
});
}
private void populatePetSpinner() {
List<String> 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<PageResponse<ServiceDTO>>() {
public void onResponse(Call<PageResponse<ServiceDTO>> c, Response<PageResponse<ServiceDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
serviceList = r.body().getContent();
populateServiceSpinner();
}
}
public void onFailure(Call<PageResponse<ServiceDTO>> c, Throwable t) {
Log.e("APPT", "Service load failed: " + t.getMessage());
}
});
}
private void populateServiceSpinner() {
List<String> 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<PageResponse<CustomerDTO>>() {
public void onResponse(Call<PageResponse<CustomerDTO>> c, Response<PageResponse<CustomerDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
customerList = r.body().getContent();
populateCustomerSpinner();
}
}
public void onFailure(Call<PageResponse<CustomerDTO>> c, Throwable t) {
Log.e("APPT", "Customer load failed: " + t.getMessage());
}
});
}
private void populateCustomerSpinner() {
List<String> 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<PageResponse<StoreDTO>>() {
public void onResponse(Call<PageResponse<StoreDTO>> c, Response<PageResponse<StoreDTO>> r) {
if (r.isSuccessful() && r.body() != null) {
storeList = r.body().getContent();
populateStoreSpinner();
}
}
public void onFailure(Call<PageResponse<StoreDTO>> c, Throwable t) {
Log.e("APPT", "Store load failed: " + t.getMessage());
}
});
}
private void populateStoreSpinner() {
List<String> 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<PageResponse<AppointmentDTO>>() {
public void onResponse(Call<PageResponse<AppointmentDTO>> c, Response<PageResponse<AppointmentDTO>> r) {
if (r.isSuccessful() && r.body() != null)
allAppointments = r.body().getContent();
}
public void onFailure(Call<PageResponse<AppointmentDTO>> 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<String> adapter = new ArrayAdapter<String>(requireContext(),
android.R.layout.simple_spinner_item,
new String[]{"Confirmed", "Pending", "Cancelled"}) {
private Callback<AppointmentDTO> simpleCallback(String msg) {
return new Callback<>() {
public void onResponse(Call<AppointmentDTO> c, Response<AppointmentDTO> 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<AppointmentDTO> 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<Void>() {
public void onResponse(Call<Void> c, Response<Void> r) { navigateBack(); }
public void onFailure(Call<Void> 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();
}
}