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:
You do not have any appointments yet.
  • No error.
  • An empty list is a normal successful response

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)​