Medicare Appointment Booking & Scheduling

Business Scenario

In this lab, you will enhance the Medicare application by implementing the My Appointments feature, allowing patients to view and manage their appointments.

Pre-Lab Preparation

Module:

1) React Basics & Backend Integration (Spring Boot)​

Phase 12F — My Appointments & Cancel Appointment

  • Now we build the patient’s appointment history.
Patient Dashboard
      ↓
  • Your original Medicare plan requires patients to view past/upcoming appointments with statuses such as PENDING, CONFIRMED, and COMPLETED. Pasted markdown
  • The flow becomes:
My Appointments
      ↓
GET /api/appointments/my
      ↓
Appointment Cards
      ↓
Doctor
Date
Time Slot
Fee
Appointment Status
Payment Status
      ↓
PENDING / CONFIRMED
      ↓
Cancel Appointment

Update appointmentService.js

1

  • Open : src/services/appointmentService.js
  • Replace it with:
import api, { getAuthConfig } from "../api/api";

const createAppointment = async (appointmentData) => {
        const response = await api.post("/appointments",appointmentData,
        getAuthConfig());
        return response.data;
    };

const getMyAppointments = async () => {
        const response = await api.get("/appointments/my",getAuthConfig());
        return response.data;
    };

const cancelAppointment = async (appointmentId) => {
        const response = await api.patch(
                `/appointments/${appointmentId}/cancel`,{},getAuthConfig());
        return response.data;
    };
const appointmentService = {
    createAppointment,
    getMyAppointments,
    cancelAppointment
};


export default appointmentService;
  • Notice the cancel request:
api.patch(
    `/appointments/${appointmentId}/cancel`,
    {},
    getAuthConfig()
);
  • We send: PATCH /api/appointments/5/cancel
  • There is no request body, so we send: {} as the second Axios argument.

Create AppointmentCard

2

  • Create:
src/components/appointment/AppointmentCard.jsx
function AppointmentCard({ appointment, onCancel, cancellingId }) {
    const formatTimeSlot = (timeSlot) => {
        switch (timeSlot) {
            case "MORNING_10_AM":
                return "10:00 AM";
            case "AFTERNOON_2_PM":
                return "2:00 PM";
            case "EVENING_5_PM":
                return "5:00 PM";
            default:
                return timeSlot;
        }
    };

    const getAppointmentBadge = (status) => {
        switch (status) {
            case "PENDING":
                return "text-bg-warning";
            case "CONFIRMED":
                return "text-bg-primary";
            case "COMPLETED":
                return "text-bg-success";
            case "CANCELLED":
                return "text-bg-secondary";
            default:
                return "text-bg-light";
        }
    };

    const getPaymentBadge = (status) => {
        switch (status) {
            case "PAID":
                return "text-bg-success";
            case "FAILED":
                return "text-bg-danger";
            case "UNPAID":
                return "text-bg-warning";
            default:
                return "text-bg-light";
        }
    };
    const canCancel =
        appointment.appointmentStatus === "PENDING" ||
        appointment.appointmentStatus === "CONFIRMED";

    return (
        <div className="card shadow-sm h-100">
            <div className="card-body">
                <div className="d-flex justify-content-between 
                align-items-start mb-3">
                    <div>
                        <h5 className="mb-1">
                            {appointment.doctorName}
                        </h5>
                        <p className="text-primary mb-0">
                            {appointment.specialization}
                        </p>
                    </div>

                    <span
                        className={`badge ${getAppointmentBadge(
                            appointment.appointmentStatus
                        )}`}>
                        {appointment.appointmentStatus}
                    </span>
                </div>

                <hr />

                <p className="mb-2">
                    <strong>Appointment ID:</strong> {appointment.id}
                </p>

                <p className="mb-2">
                    <strong>Date:</strong> {appointment.appointmentDate}
                </p>

                <p className="mb-2">
                    <strong>Time:</strong> {formatTimeSlot(appointment.timeSlot)}
                </p>

                <p className="mb-2">
                    <strong>Consultation Fee:</strong> ₹{appointment.consultationFee}
                </p>

                <p className="mb-4">
                    <strong>Payment:</strong>{" "}
                    <span
                        className={`badge ${getPaymentBadge(
                            appointment.paymentStatus
                        )}`}
                    >
                        {appointment.paymentStatus}
                    </span>
                </p>

                {canCancel && (
                    <button
                        className="btn btn-outline-danger w-100"
                        onClick={() => onCancel(appointment.id)}
                        disabled={cancellingId === appointment.id}>
                        {cancellingId === appointment.id
                            ? "Cancelling..."
                            : "Cancel Appointment"}
                    </button>
                )}

                {appointment.appointmentStatus === "COMPLETED" && (
                    <button
                        className="btn btn-outline-primary w-100"
                        disabled
                    >
                        View Prescription — Coming Soon
                    </button>
                )}

                {appointment.appointmentStatus === "CANCELLED" && (
                    <div className="alert alert-secondary mb-0">
                        This appointment has been cancelled.
                    </div>
                )}
            </div>
        </div>
    );
}


export default AppointmentCard;

Why Do We Convert TimeSlot?

  • Backend returns: MORNING_10_AM but patient should see: 10:00 AM
  • Similarly
AFTERNOON_2_PM
→ 2:00 PM
  • And :
EVENING_5_PM
→ 5:00 PM
  • The database/API value stays stable while the UI remains human-friendly.

Status Badges

  • We'll display statuses using Bootstrap:
PENDING → yellow

CONFIRMED → blue

COMPLETED → green

CANCELLED → grey
  • Payment:
UNPAID → yellow
PAID → green
FAILED → red

Create MyAppointments.jsx

3

  • Create:
src/pages/patient/MyAppointments.jsx
import { useEffect, useState } from "react";
import appointmentService from "../../services/appointmentService";
import AppointmentCard from "../../components/appointment/AppointmentCard";

function MyAppointments() {
    const [appointments, setAppointments] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState("");
    const [cancellingId, setCancellingId] = useState(null);

    useEffect(() => {
        loadAppointments();
    }, []);
    const loadAppointments = async () => {
        try {
            setLoading(true);
            setError("");

            const data = await appointmentService.getMyAppointments();
            setAppointments(data);
        } catch (error) {
            setError(
                error.response?.data?.message ||
                "Unable to load appointments"
            );
        } finally {
            setLoading(false);
        }
    };

    const handleCancel = async (appointmentId) => {
        const confirmed = window.confirm(
            "Are you sure you want to cancel this appointment?"
        );

        if (!confirmed) {
            return;
        }

        try {
            setCancellingId(appointmentId);
            setError("");
            const updatedAppointment =
                await appointmentService.cancelAppointment(appointmentId);

            setAppointments((previousAppointments) =>
                previousAppointments.map((appointment) =>
                    appointment.id === appointmentId
                        ? updatedAppointment
                        : appointment
                )
            );
        } catch (error) {
            setError(
                error.response?.data?.message ||
                "Unable to cancel appointment"
            );
        } finally {
            setCancellingId(null);
        }
    };

    if (loading) {
        return (
            <div className="container py-5 text-center">
                <div className="spinner-border text-primary"></div>
                <p className="text-secondary mt-3">
                    Loading appointments...
                </p>
            </div>
        );
    }
    return (
        <div className="container py-5">
            <div className="mb-4">
                <h2>My Appointments</h2>
                <p className="text-secondary">
                    View and manage your doctor appointments.
                </p>
            </div>

            {error && (
                <div className="alert alert-danger">
                    {error}
                </div>
            )}

            {appointments.length === 0 ? (
                <div className="alert alert-info">
                    You do not have any appointments yet.
                </div>
            ) : (
                <div className="row g-4">
                    {appointments.map((appointment) => (
                        <div
                            className="col-md-6 col-lg-4"
                            key={appointment.id}>
                            <AppointmentCard
                                appointment={appointment}
                                onCancel={handleCancel}
                                cancellingId={cancellingId}/>
                        </div>
                    ))}
                </div>
            )}
        </div>
    );
}
export default MyAppointments;

Why Update State Instead of Reloading Everything?

  • After cancellation, backend returns the updated appointment
{
    "id": 5,
    "appointmentStatus": "CANCELLED"
}
  • We could call: loadAppointments(); again.
  • But instead we demonstrate a useful React pattern:
setAppointments( previousAppointments =>
        previousAppointments.map(...)
);
  • Conceptually:
Existing appointments
      ↓
Find appointment ID 5
      ↓
Replace it with updated response
      ↓
Keep all others unchanged
      ↓
React re-renders
  • This is a good use of immutable state updates.

Add Route

4

  • Open src/routes/router.jsx
  • Add :
import MyAppointments from "../pages/patient/MyAppointments";
  • Then add this patient route:
{
    path: "patient/appointments",

    element: (
        <ProtectedRoute allowedRoles={["PATIENT"]}>
            <MyAppointments />
        </ProtectedRoute>
    )
},
  • So patient routes now include:
/patient/dashboard

/patient/doctors

/patient/doctors/:doctorId/book

/patient/appointments

/patient/profile

Update Navbar

5

<li className="nav-item">
    <Link className="nav-link" to="/patient/appointments">
        My Appointments
    </Link>
</li>
  • Inside the PATIENT section of: Navbar.jsx add:
  • Your patient navigation becomes:
Dashboard
Doctors
My Appointments
My Profile
Logout

Update Patient Dashboard

6

  • Your My Appointments card currently has a disabled button.
  • Replace:
<button className="btn btn-primary" disabled>
    Coming Soon
</button>
  • with:
<Link to="/patient/appointments" className="btn btn-primary">
    View Appointments
</Link>
  • Now this works:
Patient Dashboard
       ↓
My Appointments
       ↓
/patient/appointments

Test Appointment List

7

  • Login as PATIENT.
  • Open: http://localhost:5173/patient/appointments
  • React sends:
GET /api/appointments/my
Authorization: Bearer <patient-token>
  • Backend identifies the patient from JWT:
JWT
 ↓
Email
 ↓
User
 ↓
findByPatientId()
  • It does not need: /patient/1 or: patientId=1 from React.

Expected Appointment Card

  • Suppose API returns:
{
    "id": 7,
    "patientId": 1,
    "patientName": "Rahul Shah",
    "doctorId": 3,
    "doctorName": "Dr. Ananya",
    "specialization": "Cardiologist",
    "appointmentDate": "2026-09-27",
    "timeSlot": "MORNING_10_AM",
    "consultationFee": 1200,
    "appointmentStatus": "PENDING",
    "paymentStatus": "UNPAID",
    "createdAt": "2026-09-24T18:30:00"
}
  • React displays:
Dr. Ananya
Cardiologist

Appointment ID: 7
Date: 2026-09-27
Time: 10:00 AM
Consultation Fee: ₹1200

PENDING
UNPAID

[ Cancel Appointment ]

Cancel Appointment

8

  • Click: Cancel Appointment
  • Confirmation appears:
Are you sure you want to cancel this appointment?
  • Choose: OK
  • React sends:
PATCH /api/appointments/7/cancel
Authorization: Bearer <patient-token>
Authenticated user
       ↓
Patient owns appointment?
       ↓
      YES
  • Backend checks:
       ↓
Completed?
       ↓
      NO
       ↓
Already cancelled?
       ↓
      NO
       ↓
    CANCELLED
  • Expected: 200 OK
  • React updates the card immediately.

Completed Appointment

9

  • Suppose doctor later changes appointment status to:
COMPLETED
  • Then patient sees: COMPLETED and:
View Prescription — Coming Soon
  • The button is intentionally disabled for now.
  • We will connect that after the doctor prescription workflow is implemented.

Confirmed Appointment

10

  • When Phase 13 payment is implemented:
PENDING + UNPAID
       ↓
Successful payment
       ↓
CONFIRMED + PAID
  • Our frontend already supports CONFIRMED and: PAID badges.
  • ​So we won't need to redesign AppointmentCard later.

Security Test

11

  • Patient A owns: Appointment 7
  • Patient B should never receive Appointment 7 from: GET /api/appointments/my 

     because the backend uses the authenticated patient.

  • Even if Patient B somehow manually sends: PATCH /api/appointments/7/cancel   backend verifies:
appointment.patient.id
        ==
authenticatedPatient.id
  • If not: 403 FORBIDDEN

What If User Has No Appointments?

12

  • Backend returns: [] React shows:
            </div>
        </div>
    );
}

export default DoctorCard;

Create Booking Page

3

  • Create:
src/pages/patient/BookAppointment.jsx
  • Use :
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router";
import doctorService from "../../services/doctorService";
import appointmentService from "../../services/appointmentService";

function BookAppointment() {
    const { doctorId } = useParams();

    const [doctor, setDoctor] = useState(null);
    const [appointmentDate, setAppointmentDate] = useState("");
    const [timeSlot, setTimeSlot] = useState("");
    const [loadingDoctor, setLoadingDoctor] = useState(true);
    const [booking, setBooking] = useState(false);
    const [error, setError] = useState("");
    const [success, setSuccess] = useState(null);

    useEffect(() => {
        loadDoctor();
    }, [doctorId]);

    const loadDoctor = async () => {
        try {
            setLoadingDoctor(true);
            setError("");

            const data = await doctorService.getDoctorById(doctorId);
            setDoctor(data);
        } catch (error) {
            setError(error.response?.data?.message || 
            "Unable to load doctor");
        } finally {
            setLoadingDoctor(false);
        }
    };

    const getToday = () => {
        const today = new Date();
        const year = today.getFullYear();
        const month = String(today.getMonth() + 1).padStart(2, "0");
        const day = String(today.getDate()).padStart(2, "0");

        return `${year}-${month}-${day}`;
    };

    const handleSubmit = async (event) => {
        event.preventDefault();
        setError("");
        setSuccess(null);

        if (!appointmentDate) {
            setError("Please select an appointment date");
            return;
        }

        if (!timeSlot) {
            setError("Please select a time slot");
            return;
        }

        try {
            setBooking(true);

            const appointmentData = {
                doctorId: Number(doctorId),
                appointmentDate,
                timeSlot
            };

            const response = 
               await appointmentService.createAppointment(appointmentData);
            setSuccess(response);
            setAppointmentDate("");
            setTimeSlot("");
        } catch (error) {
            setError(error.response?.data?.message || 
              "Unable to book appointment");
        } finally {
            setBooking(false);
        }
    };

    if (loadingDoctor) {
        return (
            <div className="container py-5 text-center">
                <div className="spinner-border text-primary"></div>
                <p className="text-secondary mt-3">Loading doctor...</p>
            </div>
        );
    }

    if (!doctor) {
        return (
            <div className="container py-5">
                <div className="alert alert-danger">
                    Doctor information could not be loaded.
                </div>
                <Link to="/patient/doctors" className="btn btn-primary">
                    Back to Doctors
                </Link>
            </div>
        );
    }

    return (
        <div className="container py-5">
            <div className="row justify-content-center">
                <div className="col-lg-8">
                    {/* PAGE HEADER */}
                    <div className="mb-4">
                        <h2>Book Appointment</h2>
                        <p className="text-secondary">
                            Select your preferred appointment date and time.
                        </p>
                    </div>

                    {/* DOCTOR INFORMATION */}
                    <div className="card shadow-sm mb-4">
                        <div className="card-body">
                            <div className="row align-items-center">
                                {doctor.photoUrl && (
                                    <div className="col-md-3 mb-3 mb-md-0">
                                        <img src={doctor.photoUrl} alt={doctor.name} 
                                             className="img-fluid rounded" />
                                    </div>
                                )}

                                <div className={doctor.photoUrl ? "col-md-9" : "col-12"}>
                                    <h4 className="mb-1">{doctor.name}</h4>
                                    <p className="text-primary fw-semibold mb-2">
                                        {doctor.specialization}
                                    </p>
                                    <p className="mb-1">
                                        <strong>Experience:</strong> 
                                      {doctor.experienceYears} years
                                    </p>
                                    <p className="mb-0">
                                        <strong>Consultation Fee:</strong> 
                                      ₹{doctor.consultationFee}
                                    </p>
                                </div>
                            </div>
                        </div>
                    </div>

                    {/* ERROR */}
                    {error && (
                        <div className="alert alert-danger">{error}</div>
                    )}

                    {/* SUCCESS */}
                    {success && (
                        <div className="alert alert-success">
                            <h5>Appointment Booked</h5>
                            <p className="mb-1">
                                Appointment ID: <strong>{success.id}</strong>
                            </p>
                            <p className="mb-1">
                                Doctor: <strong>{success.doctorName}</strong>
                            </p>
                            <p className="mb-1">
                                Date: <strong>{success.appointmentDate}</strong>
                            </p>
                            <p className="mb-1">
                                Slot: <strong>{success.timeSlot}</strong>
                            </p>
                            <p className="mb-1">
                                Status: <strong>{success.appointmentStatus}</strong>
                            </p>
                            <p className="mb-0">
                                Payment: <strong>{success.paymentStatus}</strong>
                            </p>
                        </div>
                    )}

                    {/* BOOKING FORM */}
                    <div className="card shadow-sm">
                        <div className="card-body p-4">
                            <form onSubmit={handleSubmit}>
                                {/* DATE */}
                                <div className="mb-4">
                                    <label className="form-label">
                                      Appointment Date</label>
                                    <input
                                        type="date"
                                        className="form-control"
                                        min={getToday()}
                                        value={appointmentDate}
                                        onChange={(event) => 
                                           setAppointmentDate(event.target.value)}
                                        required
                                    />
                                </div>

                                {/* TIME SLOT */}
                                <div className="mb-4">
                                    <label className="form-label">
                                      Select Time Slot</label>
                                    <div className="row g-3">
                                        {/* 10 AM */}
                                        <div className="col-md-4">
                                            <input
                                                type="radio"
                                                className="btn-check"
                                                name="timeSlot"
                                                id="slotMorning"
                                                value="MORNING_10_AM"
                                                checked={timeSlot === "MORNING_10_AM"}
                                                onChange={(event) => 
                                           setTimeSlot(event.target.value)}
                                            />
                                            <label className="btn btn-outline-primary w-100" 
                                                   htmlFor="slotMorning">
                                                10:00 AM
                                            </label>
                                        </div>

                                        {/* 2 PM */}
                                        <div className="col-md-4">
                                            <input
                                                type="radio"
                                                className="btn-check"
                                                name="timeSlot"
                                                id="slotAfternoon"
                                                value="AFTERNOON_2_PM"
                                                checked={timeSlot === "AFTERNOON_2_PM"}
                                                onChange={(event) => 
                                                   setTimeSlot(event.target.value)}
                                            />
                                            <label className="btn btn-outline-primary 
                                                   w-100" htmlFor="slotAfternoon">
                                                2:00 PM
                                            </label>
                                        </div>

                                        {/* 5 PM */}
                                        <div className="col-md-4">
                                            <input
                                                type="radio"
                                                className="btn-check"
                                                name="timeSlot"
                                                id="slotEvening"
                                                value="EVENING_5_PM"
                                                checked={timeSlot === "EVENING_5_PM"}
                                                onChange={(event) => 
                                                    setTimeSlot(event.target.value)}
                                            />
                                            <label className="btn btn-outline-primary 
                                              w-100" htmlFor="slotEvening">
                                                5:00 PM
                                            </label>
                                        </div>
                                    </div>
                                </div>

                                {/* FEE */}
                                <div className="alert alert-light border">
                                    Consultation Fee: <strong>
                                        ₹{doctor.consultationFee}</strong>
                                </div>

                                <div className="d-flex gap-2">
                                    <Link to="/patient/doctors" 
                                   className="btn btn-outline-secondary">
                                        Back
                                    </Link>
                                    <button
                                        type="submit"
                                        className="btn btn-primary flex-grow-1"
                                        disabled={booking}
                                    >
                                        {booking ? "Booking..." : "Book Appointment"}
                                    </button>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}

export default BookAppointment;

function BookAppointment() {
    const { doctorId } = useParams();

    const [doctor, setDoctor] = useState(null);
    const [appointmentDate, setAppointmentDate] = useState("");
    const [timeSlot, setTimeSlot] = useState("");
    const [loadingDoctor, setLoadingDoctor] = useState(true);
    const [booking, setBooking] = useState(false);
    const [error, setError] = useState("");
    const [success, setSuccess] = useState(null);

    useEffect(() => {
        loadDoctor();
    }, [doctorId]);

    const loadDoctor = async () => {
        try {
            setLoadingDoctor(true);
            setError("");

            const data = await doctorService.getDoctorById(doctorId);
            setDoctor(data);
        } catch (error) {
            setError(error.response?.data?.message || 
            "Unable to load doctor");
        } finally {
            setLoadingDoctor(false);
        }
    };

    const getToday = () => {
        const today = new Date();
        const year = today.getFullYear();
        const month = String(today.getMonth() + 1).padStart(2, "0");
        const day = String(today.getDate()).padStart(2, "0");

        return `${year}-${month}-${day}`;
    };

    const handleSubmit = async (event) => {
        event.preventDefault();
        setError("");
        setSuccess(null);

        if (!appointmentDate) {
            setError("Please select an appointment date");
            return;
        }

        if (!timeSlot) {
            setError("Please select a time slot");
            return;
        }

        try {
            setBooking(true);

            const appointmentData = {
                doctorId: Number(doctorId),
                appointmentDate,
                timeSlot
            };

            const response = 
               await appointmentService.createAppointment(appointmentData);
            setSuccess(response);
            setAppointmentDate("");
            setTimeSlot("");
        } catch (error) {
            setError(error.response?.data?.message || 
              "Unable to book appointment");
        } finally {
            setBooking(false);
        }
    };

    if (loadingDoctor) {
        return (
            <div className="container py-5 text-center">
                <div className="spinner-border text-primary"></div>
                <p className="text-secondary mt-3">Loading doctor...</p>
            </div>
        );
    }

    if (!doctor) {
        return (
            <div className="container py-5">
                <div className="alert alert-danger">
                    Doctor information could not be loaded.
                </div>
                <Link to="/patient/doctors" className="btn btn-primary">
                    Back to Doctors
                </Link>
            </div>
        );
    }

    return (
        <div className="container py-5">
            <div className="row justify-content-center">
                <div className="col-lg-8">
                    {/* PAGE HEADER */}
                    <div className="mb-4">
                        <h2>Book Appointment</h2>
                        <p className="text-secondary">
                            Select your preferred appointment date and time.
                        </p>
                    </div>

                    {/* DOCTOR INFORMATION */}
                    <div className="card shadow-sm mb-4">
                        <div className="card-body">
                            <div className="row align-items-center">
                                {doctor.photoUrl && (
                                    <div className="col-md-3 mb-3 mb-md-0">
                                        <img src={doctor.photoUrl} alt={doctor.name} 
                                             className="img-fluid rounded" />
                                    </div>
                                )}

                                <div className={doctor.photoUrl ? "col-md-9" : "col-12"}>
                                    <h4 className="mb-1">{doctor.name}</h4>
                                    <p className="text-primary fw-semibold mb-2">
                                        {doctor.specialization}
                                    </p>
                                    <p className="mb-1">
                                        <strong>Experience:</strong> 
                                      {doctor.experienceYears} years
                                    </p>
                                    <p className="mb-0">
                                        <strong>Consultation Fee:</strong> 
                                      ₹{doctor.consultationFee}
                                    </p>
                                </div>
                            </div>
                        </div>
                    </div>

                    {/* ERROR */}
                    {error && (
                        <div className="alert alert-danger">{error}</div>
                    )}

                    {/* SUCCESS */}
                    {success && (
                        <div className="alert alert-success">
                            <h5>Appointment Booked</h5>
                            <p className="mb-1">
                                Appointment ID: <strong>{success.id}</strong>
                            </p>
                            <p className="mb-1">
                                Doctor: <strong>{success.doctorName}</strong>
                            </p>
                            <p className="mb-1">
                                Date: <strong>{success.appointmentDate}</strong>
                            </p>
                            <p className="mb-1">
                                Slot: <strong>{success.timeSlot}</strong>
                            </p>
                            <p className="mb-1">
                                Status: <strong>{success.appointmentStatus}</strong>
                            </p>
                            <p className="mb-0">
                                Payment: <strong>{success.paymentStatus}</strong>
                            </p>
                        </div>
                    )}

                    {/* BOOKING FORM */}
                    <div className="card shadow-sm">
                        <div className="card-body p-4">
                            <form onSubmit={handleSubmit}>
                                {/* DATE */}
                                <div className="mb-4">
                                    <label className="form-label">
                                      Appointment Date</label>
                                    <input
                                        type="date"
                                        className="form-control"
                                        min={getToday()}
                                        value={appointmentDate}
                                        onChange={(event) => 
                                           setAppointmentDate(event.target.value)}
                                        required
                                    />
                                </div>

                                {/* TIME SLOT */}
                                <div className="mb-4">
                                    <label className="form-label">
                                      Select Time Slot</label>
                                    <div className="row g-3">
                                        {/* 10 AM */}
                                        <div className="col-md-4">
                                            <input
                                                type="radio"
                                                className="btn-check"
                                                name="timeSlot"
                                                id="slotMorning"
                                                value="MORNING_10_AM"
                                                checked={timeSlot === "MORNING_10_AM"}
                                                onChange={(event) => 
                                           setTimeSlot(event.target.value)}
                                            />
                                            <label className="btn btn-outline-primary w-100" 
                                                   htmlFor="slotMorning">
                                                10:00 AM
                                            </label>
                                        </div>

                                        {/* 2 PM */}
                                        <div className="col-md-4">
                                            <input
                                                type="radio"
                                                className="btn-check"
                                                name="timeSlot"
                                                id="slotAfternoon"
                                                value="AFTERNOON_2_PM"
                                                checked={timeSlot === "AFTERNOON_2_PM"}
                                                onChange={(event) => 
                                                   setTimeSlot(event.target.value)}
                                            />
                                            <label className="btn btn-outline-primary 
                                                   w-100" htmlFor="slotAfternoon">
                                                2:00 PM
                                            </label>
                                        </div>

                                        {/* 5 PM */}
                                        <div className="col-md-4">
                                            <input
                                                type="radio"
                                                className="btn-check"
                                                name="timeSlot"
                                                id="slotEvening"
                                                value="EVENING_5_PM"
                                                checked={timeSlot === "EVENING_5_PM"}
                                                onChange={(event) => 
                                                    setTimeSlot(event.target.value)}
                                            />
                                            <label className="btn btn-outline-primary 
                                              w-100" htmlFor="slotEvening">
                                                5:00 PM
                                            </label>
                                        </div>
                                    </div>
                                </div>

                                {/* FEE */}
                                <div className="alert alert-light border">
                                    Consultation Fee: <strong>
                                        ₹{doctor.consultationFee}</strong>
                                </div>

                                <div className="d-flex gap-2">
                                    <Link to="/patient/doctors" 
                                   className="btn btn-outline-secondary">
                                        Back
                                    </Link>
                                    <button
                                        type="submit"
                                        className="btn btn-primary flex-grow-1"
                                        disabled={booking}
                                    >
                                        {booking ? "Booking..." : "Book Appointment"}
                                    </button>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}

export default BookAppointment;

    const getToday = () => {
        const today = new Date();
        const year = today.getFullYear();
        const month = String(today.getMonth() + 1).padStart(2, "0");
        const day = String(today.getDate()).padStart(2, "0");

        return `${year}-${month}-${day}`;
    };

    const handleSubmit = async (event) => {
        event.preventDefault();
        setError("");
        setSuccess(null);

        if (!appointmentDate) {
            setError("Please select an appointment date");
            return;
        }

        if (!timeSlot) {
            setError("Please select a time slot");
            return;
        }

        try {
            setBooking(true);
            const appointmentData = {
                doctorId: Number(doctorId),appointmentDate,timeSlot
            };

            const response = 
               await appointmentService.createAppointment(appointmentData);
            setSuccess(response);
            setAppointmentDate("");
            setTimeSlot("");
        } catch (error) {
            setError(error.response?.data?.message || 
              "Unable to book appointment");
        } finally {
            setBooking(false);
        }
    };

    if (loadingDoctor) {
        return (
            <div className="container py-5 text-center">
                <div className="spinner-border text-primary"></div>
                <p className="text-secondary mt-3">Loading doctor...</p>
            </div>
        );
    }

    if (!doctor) {
        return (
            <div className="container py-5">
                <div className="alert alert-danger">
                    Doctor information could not be loaded.
                </div>
                <Link to="/patient/doctors" className="btn btn-primary">
                    Back to Doctors
                </Link>
            </div>
        );
    }

    return (
        <div className="container py-5">
            <div className="row justify-content-center">
                <div className="col-lg-8">
                    {/* PAGE HEADER */}
                    <div className="mb-4">
                        <h2>Book Appointment</h2>
                        <p className="text-secondary">
                            Select your preferred appointment date and time.
                        </p>
                    </div>

                    {/* DOCTOR INFORMATION */}
                    <div className="card shadow-sm mb-4">
                        <div className="card-body">
                            <div className="row align-items-center">
                                {doctor.photoUrl && (
                                    <div className="col-md-3 mb-3 mb-md-0">
                                        <img src={doctor.photoUrl} alt={doctor.name} 
                                             className="img-fluid rounded" />
                                    </div>
                                )}

                                <div className={doctor.photoUrl ? "col-md-9" : "col-12"}>
                                    <h4 className="mb-1">{doctor.name}</h4>
                                    <p className="text-primary fw-semibold mb-2">
                                        {doctor.specialization}
                                    </p>
                                    <p className="mb-1">
                                        <strong>Experience:</strong> 
                                      {doctor.experienceYears} years
                                    </p>
                                    <p className="mb-0">
                                        <strong>Consultation Fee:</strong> 
                                      ₹{doctor.consultationFee}
                                    </p>
                                </div>
                            </div>
                        </div>
                    </div>

                    {/* ERROR */}
                    {error && (
                        <div className="alert alert-danger">{error}</div>
                    )}

                    {/* SUCCESS */}
                    {success && (
                        <div className="alert alert-success">
                            <h5>Appointment Booked</h5>
                            <p className="mb-1">
                                Appointment ID: <strong>{success.id}</strong>
                            </p>
                            <p className="mb-1">
                                Doctor: <strong>{success.doctorName}</strong>
                            </p>
                            <p className="mb-1">
                                Date: <strong>{success.appointmentDate}</strong>
                            </p>
                            <p className="mb-1">
                                Slot: <strong>{success.timeSlot}</strong>
                            </p>
                            <p className="mb-1">
                                Status: <strong>{success.appointmentStatus}</strong>
                            </p>
                            <p className="mb-0">
                                Payment: <strong>{success.paymentStatus}</strong>
                            </p>
                        </div>
                    )}

                    {/* BOOKING FORM */}
                    <div className="card shadow-sm">
                        <div className="card-body p-4">
                            <form onSubmit={handleSubmit}>
                                {/* DATE */}
                                <div className="mb-4">
                                    <label className="form-label">
                                      Appointment Date</label>
                                    <input
                                        type="date"
                                        className="form-control"
                                        min={getToday()}
                                        value={appointmentDate}
                                        onChange={(event) => 
                                           setAppointmentDate(event.target.value)}
                                        required
                                    />
                                </div>

                                {/* TIME SLOT */}
                                <div className="mb-4">
                                    <label className="form-label">
                                      Select Time Slot</label>
                                    <div className="row g-3">
                                        {/* 10 AM */}
                                        <div className="col-md-4">
                                            <input
                                                type="radio"
                                                className="btn-check"
                                                name="timeSlot"
                                                id="slotMorning"
                                                value="MORNING_10_AM"
                                                checked={timeSlot === "MORNING_10_AM"}
                                                onChange={(event) => 
                                           setTimeSlot(event.target.value)}
                                            />
                                            <label className="btn btn-outline-primary w-100" 
                                                   htmlFor="slotMorning">
                                                10:00 AM
                                            </label>
                                        </div>

                                        {/* 2 PM */}
                                        <div className="col-md-4">
                                            <input
                                                type="radio"
                                                className="btn-check"
                                                name="timeSlot"
                                                id="slotAfternoon"
                                                value="AFTERNOON_2_PM"
                                                checked={timeSlot === "AFTERNOON_2_PM"}
                                                onChange={(event) => 
                                                   setTimeSlot(event.target.value)}
                                            />
                                            <label className="btn btn-outline-primary 
                                                   w-100" htmlFor="slotAfternoon">
                                                2:00 PM
                                            </label>
                                        </div>

                                        {/* 5 PM */}
                                        <div className="col-md-4">
                                            <input
                                                type="radio"
                                                className="btn-check"
                                                name="timeSlot"
                                                id="slotEvening"
                                                value="EVENING_5_PM"
                                                checked={timeSlot === "EVENING_5_PM"}
                                                onChange={(event) => 
                                                    setTimeSlot(event.target.value)}
                                            />
                                            <label className="btn btn-outline-primary 
                                              w-100" htmlFor="slotEvening">
                                                5:00 PM
                                            </label>
                                        </div>
                                    </div>
                                </div>

                                {/* FEE */}
                                <div className="alert alert-light border">
                                    Consultation Fee: <strong>
                                        ₹{doctor.consultationFee}</strong>
                                </div>

                                <div className="d-flex gap-2">
                                    <Link to="/patient/doctors" 
                                   className="btn btn-outline-secondary">
                                        Back
                                    </Link>
                                    <button
                                        type="submit"
                                        className="btn btn-primary flex-grow-1"
                                        disabled={booking}
                                    >
                                        {booking ? "Booking..." : "Book Appointment"}
                                    </button>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}

export default BookAppointment;

            const response = 
               await appointmentService.createAppointment(appointmentData);
            setSuccess(response);
            setAppointmentDate("");
            setTimeSlot("");
        } catch (error) {
            setError(error.response?.data?.message || 
              "Unable to book appointment");
        } finally {
            setBooking(false);
        }
    };

    if (loadingDoctor) {
        return (
            <div className="container py-5 text-center">
                <div className="spinner-border text-primary"></div>
                <p className="text-secondary mt-3">Loading doctor...</p>
            </div>
        );
    }

    if (!doctor) {
        return (
            <div className="container py-5">
                <div className="alert alert-danger">
                    Doctor information could not be loaded.
                </div>
                <Link to="/patient/doctors" className="btn btn-primary">

                    Back to Doctors
                </Link>
            </div>
        );
    }

    return (
        <div className="container py-5">
            <div className="row justify-content-center">
                <div className="col-lg-8">
                    {/* PAGE HEADER */}
                    <div className="mb-4">
                        <h2>Book Appointment</h2>
                        <p className="text-secondary">
                            Select your preferred appointment date and time.
                        </p>
                    </div>

                    {/* DOCTOR INFORMATION */}
                    <div className="card shadow-sm mb-4">
                        <div className="card-body">
                            <div className="row align-items-center">
                                {doctor.photoUrl && (
                                    <div className="col-md-3 mb-3 mb-md-0">
                                        <img src={doctor.photoUrl} alt={doctor.name} 
                                             className="img-fluid rounded" />
                                    </div>
                                )}

                                <div className={doctor.photoUrl ? 
                                                "col-md-9" : "col-12"}>
                                    <h4 className="mb-1">
                                      {doctor.name}</h4>
                                    <p className="text-primary 
                                         fw-semibold mb-2">
                                        {doctor.specialization}
                                    </p>
                                    <p className="mb-1">
                                        <strong>Experience:</strong> 
                                      {doctor.experienceYears} years
                                    </p>
                                    <p className="mb-0">
                                        <strong>
                                        Consultation Fee:</strong> 
                                      ₹{doctor.consultationFee}
                                    </p>
                                </div>
                            </div>
                        </div>
                    </div>

                    {/* ERROR */}
                    {error && (
                        <div className="alert alert-danger">
                      {error}</div>
                    )}

                    {/* SUCCESS */}
                    {success && (
                        <div className="alert alert-success">
                            <h5>Appointment Booked</h5>
                            <p className="mb-1">
                                Appointment ID: <strong>
                      {success.id}</strong>
                            </p>
                            <p className="mb-1">
                                Doctor: <strong>{success.doctorName}</strong>
                            </p>
                            <p className="mb-1">
                                Date: <strong>{success.appointmentDate}</strong>
                            </p>
                            <p className="mb-1">
                                Slot: <strong>{success.timeSlot}</strong>
                            </p>
                            <p className="mb-1">
                                Status: <strong>{success.appointmentStatus}</strong>
                            </p>
                            <p className="mb-0">
                                Payment: <strong>{success.paymentStatus}</strong>
                            </p>
                        </div>
                    )}

                    {/* BOOKING FORM */}
                    <div className="card shadow-sm">
                        <div className="card-body p-4">

                            <form onSubmit={handleSubmit}>
                                {/* DATE */}
                                <div className="mb-4">
                                    <label className="form-label">
                                      Appointment Date</label>
                                    <input
                                        type="date"
                                        className="form-control"
                                        min={getToday()}
                                        value={appointmentDate}
                                        onChange={(event) => 
                                           setAppointmentDate(event.target.value)}
                                        required
                                    />
                                </div>

                                {/* TIME SLOT */}
                                <div className="mb-4">
                                    <label className="form-label">
                                      Select Time Slot</label>
                                    <div className="row g-3">
                                        {/* 10 AM */}
                                        <div className="col-md-4">
                                            <input
                                                type="radio"
                                                className="btn-check"
                                                name="timeSlot"
                                                id="slotMorning"
                                                value="MORNING_10_AM"

                                                onChange={(event) => 
                                           setTimeSlot(event.target.value)}
                                            />
                                            <label 
                                            className="btn btn-outline-primary w-100" 
                                            htmlFor="slotMorning">
                                                10:00 AM
                                            </label>
                                        </div>

                                        {/* 2 PM */}
                                        <div className="col-md-4">
                                            <input
                                                type="radio"
                                                className="btn-check"
                                                name="timeSlot"
                                                id="slotAfternoon"
                                                value="AFTERNOON_2_PM"
                                                checked={timeSlot === "AFTERNOON_2_PM"}
                                                onChange={(event) => 
                                                   setTimeSlot(event.target.value)}
                                            />
                                            <label className="btn btn-outline-primary 
                                                   w-100" htmlFor="slotAfternoon">
                                                2:00 PM
                                            </label>
                                        </div>

                                        {/* 5 PM */}
                                        <div className="col-md-4">
                                            <input
                                                type="radio"
                                                className="btn-check"
                                                name="timeSlot"
                                                id="slotEvening"
                                                value="EVENING_5_PM"
                                                checked={timeSlot === "EVENING_5_PM"}
                                                onChange={(event) => 
                                                    setTimeSlot(event.target.value)}
                                            />
                                            <label className="btn btn-outline-primary 
                                              w-100" htmlFor="slotEvening">
                                                5:00 PM
                                            </label>
                                        </div>
                                    </div>
                                </div>

                                {/* FEE */}
                                <div className="alert alert-light border">
                                    Consultation Fee: <strong>
                                        ₹{doctor.consultationFee}</strong>
                                </div>

                                <div className="d-flex gap-2">
                                    <Link to="/patient/doctors" 
                                   className="btn btn-outline-secondary">

                                        Back
                                    </Link>
                                    <button
                                        type="submit"
                                        className="btn btn-primary flex-grow-1"
                                        disabled={booking}>
                                        {booking ? "Booking..." : "Book Appointment"}
                                    </button>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}
export default BookAppointment;

Text

  • The complete code file attached below contains the same code as provided above, with proper indentation and formatting for better readability.

Why the Backend Receives Enum Values

  • The user sees:
10:00 AM
2:00 PM
5:00 PM
  • But React sends:
MORNING_10_AM
AFTERNOON_2_PM
EVENING_5_PM
  • Because our Spring enum is:
public enum TimeSlot {
    MORNING_10_AM,
    AFTERNOON_2_PM,
    EVENING_5_PM
}
  • So:
Human-friendly UI
       ↓
Enum value
       ↓
Spring Boot
  • This is better than exposing ugly enum names to the user.

Why We Don't Send Patient ID

  • Our request becomes:
{
    "doctorId": 3,
    "appointmentDate": "2026-09-26",
    "timeSlot": "MORNING_10_AM"
}
  • Notice :
patientId ❌
consultationFee ❌
appointmentStatus ❌
paymentStatus ❌
  • The backend gets the patient from:
JWT
 ↓
email
 ↓
User
  • And consultation fee from: DoctorProfile

Update Router

4

  • Open src/routes/router.jsx
  • Add:
import BookAppointment from "../pages/patient/BookAppointment";
  • Then inside MainLayout → children, add:
{
    path: "patient/doctors/:doctorId/book",

    element: (
        <ProtectedRoute allowedRoles={[
                "PATIENT"
            ]} >
            <BookAppointment />
        </ProtectedRoute>
    )
},
  • So now: /patient/doctors contains doctor cards.
  • Clicking doctor 3 goes to: /patient/doctors/3/book
  • And: useParams() extracts: doctorId = "3"

Test Doctor Navigation

5

  • Login as PATIENT.
Doctor Name
Specialization
Experience
Fee
  • Open: /patient/doctors
  • Click: Book Appointment
  • Suppose doctor ID is 3.
  • You should reach: /patient/doctors/3/book
  • The frontend calls: GET /api/doctors/3 and displays:

Test Appointment Booking

6

Date: September 26
Slot: 10:00 AM
  • Select:
  • Frontend sends:
POST /api/appointments
Authorization: Bearer <patient-token>
Content-Type: application/json
  • Body:
{
    "doctorId": 3,
    "appointmentDate": "2026-09-26",
    "timeSlot": "MORNING_10_AM"
}
patient → from JWT
doctor → ID 3
consultationFee → DoctorProfile
appointmentStatus → PENDING
paymentStatus → UNPAID
  • Backend should create:
  • Expected: 201 CREATED
  • Expected Response
{
    "id": 7,
    "patientId": 1,
    "patientName": "Rahul Shah",
    "doctorId": 3,
    "doctorName": "Dr. Ananya Sharma",
    "specialization": "Cardiologist",
    "appointmentDate": "2026-09-26",
    "timeSlot": "MORNING_10_AM",
    "consultationFee": 1200,
    "appointmentStatus": "PENDING",
    "paymentStatus": "UNPAID",
    "createdAt": "2026-09-24T18:10:00"
}

Duplicate Slot Test

7

  • Suppose this already exists:
Doctor: Dr. Ananya
Date: 2026-09-26
Slot: 10 AM
  • Try booking it again.
Doctor 3
+
2026-09-26
+
MORNING_10_AM
  • Backend should detect:
  • And return: 409 CONFLICT
  • Something like: Selected appointment slot is already booked

Test Another Slot

8

  • Same doctor: September 26 but choose: 2:00 PM
{
    "doctorId": 3,
    "appointmentDate": "2026-09-26",
    "timeSlot": "AFTERNOON_2_PM"
}
  • Request:
  • This should succeed if that slot is free.

Test Another Doctor

9

  • If: Dr. Ananya 10 AM is already booked, another doctor can still have:
Dr. Ananya
10 AM

Cancelled Slot Behavior

11

  • Because slot availability is based on:
Doctor
+
Date
+
Slot
  • Not globally across the hospital
  • Our backend query intentionally ignores: CANCELLED  appointments when checking
    slot occupancy
  • So:
Appointment
Doctor 3
10 AM
CANCELLED
  • Means the same slot becomes available again.

We are done with this lab. The latest source code has been uploaded to GitHub. You can access the latest commit using the link below: 

 

Great job!

You have successfully completed the appointment booking functionality in the Medicare application.

Checkpoint

   Git Push

git push origin branchName

Next-Lab Preparation

Module:

1) React Basics & Backend Integration (Spring Boot)​

SpringBoot Lab - 18

By Content ITV

SpringBoot Lab - 18

  • 19