diff --git a/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java b/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java new file mode 100644 index 00000000..7924c12b --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java @@ -0,0 +1,86 @@ +package com.example.petstoremobile.adapters; + + + +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.models.Appointment; +import java.util.List; + +public class AppointmentAdapter extends RecyclerView.Adapter { + + 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) { + 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; + + public AppointmentViewHolder(@NonNull View v) { + super(v); + tvCustomerName = v.findViewById(R.id.tvCustomerName); + tvPetName = v.findViewById(R.id.tvApptPetName); + tvServiceType = v.findViewById(R.id.tvServiceType); + tvDateTime = v.findViewById(R.id.tvDateTime); + tvAppointmentStatus = v.findViewById(R.id.tvAppointmentStatus); + } + } + + // Create a new row view + @NonNull + @Override + public AppointmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_appointment, parent, false); + return new AppointmentViewHolder(v); + } + + // Populate the row with appointment data + @Override + public void onBindViewHolder(@NonNull AppointmentViewHolder holder, int position) { + Appointment appointment = 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()); + + // Set the status color depending on appointment status + switch (appointment.getStatus()) { + case "Confirmed": + holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50")); + break; + case "Pending": + holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#FF9800")); + break; + default: + holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336")); + break; + } + + // When a row is clicked, open the detail view + holder.itemView.setOnClickListener(v -> appointmentClickListener.onAppointmentClick(position)); + } + + @Override + public int getItemCount() { + return appointmentList.size(); + } +} diff --git a/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java new file mode 100644 index 00000000..a1bce0b2 --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -0,0 +1,153 @@ +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.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 com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.AppointmentAdapter; +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; + +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 AppointmentAdapter adapter; + private SwipeRefreshLayout swipeRefreshLayout; + private EditText etSearch; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_appointment, container, false); + + loadAppointmentData(); // TODO: Replace with actual API call when backend is ready + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); + + FloatingActionButton fabAddAppointment = view.findViewById(R.id.fabAddAppointment); + fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); + + 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) { + filterAppointments(s.toString()); + } + @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)) { + filteredList.add(a); + } + } + } + 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); + }); + } + + 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()); + } + + detailFragment.setArguments(args); + detailFragment.setAppointmentFragment(this); + + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.loadFragment(detailFragment); + } + + public void onAppointmentSaved(int position, Appointment appointment) { + if (position == -1) { + appointmentList.add(appointment); + } else { + appointmentList.set(position, appointment); + } + filterAppointments(etSearch.getText().toString()); + } + + public void onAppointmentDeleted(int position) { + appointmentList.remove(position); + filterAppointments(etSearch.getText().toString()); + } + + @Override + public void onAppointmentClick(int position) { + 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); + } + + private void setupRecyclerView(View view) { + RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAppointments); + adapter = new AppointmentAdapter(filteredList, this); // adapter uses filteredList + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + } +} diff --git a/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java new file mode 100644 index 00000000..3b60a70f --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -0,0 +1,163 @@ +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.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.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; + +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; + + // Set the appointment fragment as parent so we refer back when save or delete is done + public void setAppointmentFragment(AppointmentFragment fragment) { + this.appointmentFragment = fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_appointment_detail, container, false); + + initViews(view); + setupSpinner(); + handleArguments(); + + btnBack.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) { + listFragment.getChildFragmentManager().popBackStack(); + } + }); + btnSaveAppointment.setOnClickListener(v -> saveAppointment()); + btnDeleteAppointment.setOnClickListener(v -> deleteAppointment()); + + 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; + + 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(); + + 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(); + } + // 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(); + } + } + + // Determines if the fragment is in add or edit mode and populates fields accordingly + private void handleArguments() { + if (getArguments() != null && getArguments().containsKey("appointmentId")) { + isEditing = true; + appointmentId = getArguments().getInt("appointmentId"); + position = getArguments().getInt("position"); + 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); + } else { + isEditing = false; + tvMode.setText("Add Appointment"); + 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 setupSpinner() { + ArrayAdapter adapter = new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, + new String[]{"Confirmed", "Pending", "Cancelled"}); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerStatus.setAdapter(adapter); + } +} diff --git a/app/src/main/java/com/example/petstoremobile/models/Appointment.java b/app/src/main/java/com/example/petstoremobile/models/Appointment.java new file mode 100644 index 00000000..38e8da10 --- /dev/null +++ b/app/src/main/java/com/example/petstoremobile/models/Appointment.java @@ -0,0 +1,76 @@ +package com.example.petstoremobile.models; + + +public class Appointment { + private int appointmentId; + private String customerName; + private String petName; + private String serviceType; + private String appointmentDate; + private String appointmentTime; + private String status; + + // Constructor + public Appointment(int appointmentId, String customerName, String petName, String serviceType, String appointmentDate, String appointmentTime, String status) { + this.appointmentId = appointmentId; + this.customerName = customerName; + this.petName = petName; + this.serviceType = serviceType; + this.appointmentDate = appointmentDate; + this.appointmentTime = appointmentTime; + this.status = status; + } + + // Getters and setters + public int getAppointmentId() { + return appointmentId; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getServiceType() { + return serviceType; + } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } + + public String getAppointmentDate() { + return appointmentDate; + } + + public void setAppointmentDate(String appointmentDate) { + this.appointmentDate = appointmentDate; + } + + public String getAppointmentTime() { + return appointmentTime; + } + + public void setAppointmentTime(String appointmentTime) { + this.appointmentTime = appointmentTime; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/app/src/main/res/layout/fragment_appointment.xml b/app/src/main/res/layout/fragment_appointment.xml new file mode 100644 index 00000000..18e28efe --- /dev/null +++ b/app/src/main/res/layout/fragment_appointment.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_appointment_detail.xml b/app/src/main/res/layout/fragment_appointment_detail.xml new file mode 100644 index 00000000..48238dc8 --- /dev/null +++ b/app/src/main/res/layout/fragment_appointment_detail.xml @@ -0,0 +1,101 @@ + + + + + + + + + + +