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
Patient Dashboard ↓
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
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;api.patch(
`/appointments/${appointmentId}/cancel`,
{},
getAuthConfig()
);Create AppointmentCard
2
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?
AFTERNOON_2_PM → 2:00 PM
EVENING_5_PM → 5:00 PM
Status Badges
PENDING → yellow CONFIRMED → blue COMPLETED → green CANCELLED → grey
UNPAID → yellow PAID → green FAILED → red
Create MyAppointments.jsx
3
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?
{
"id": 5,
"appointmentStatus": "CANCELLED"
}setAppointments( previousAppointments => previousAppointments.map(...) );
Existing appointments ↓ Find appointment ID 5 ↓ Replace it with updated response ↓ Keep all others unchanged ↓ React re-renders
Add Route
4
import MyAppointments from "../pages/patient/MyAppointments";{
path: "patient/appointments",
element: (
<ProtectedRoute allowedRoles={["PATIENT"]}> <MyAppointments />
</ProtectedRoute>
)
},/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>Dashboard Doctors My Appointments My Profile Logout
Update Patient Dashboard
6
<button className="btn btn-primary" disabled>
Coming Soon
</button><Link to="/patient/appointments" className="btn btn-primary">
View Appointments
</Link>Patient Dashboard ↓ My Appointments ↓ /patient/appointments
Test Appointment List
7
GET /api/appointments/my Authorization: Bearer <patient-token>
JWT ↓ Email ↓ User ↓ findByPatientId()
Expected Appointment Card
{
"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"
}Dr. Ananya Cardiologist Appointment ID: 7 Date: 2026-09-27 Time: 10:00 AM Consultation Fee: ₹1200 PENDING UNPAID [ Cancel Appointment ]
Cancel Appointment
8
Are you sure you want to cancel this appointment?PATCH /api/appointments/7/cancel Authorization: Bearer <patient-token>
Authenticated user ↓ Patient owns appointment? ↓ YES
↓ Completed? ↓ NO ↓ Already cancelled? ↓ NO ↓ CANCELLED
Completed Appointment
9
COMPLETEDView Prescription — Coming Soon
Confirmed Appointment
10
PENDING + UNPAID ↓ Successful payment
↓ CONFIRMED + PAID
Security Test
11
because the backend uses the authenticated patient.
appointment.patient.id == authenticatedPatient.id
What If User Has No Appointments?
12
You do not have any appointments yet.
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 branchNameNext-Lab Preparation
Module:
1) React Basics & Backend Integration (Spring Boot)