Content ITV PRO
This is Itvedant Content department
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
</div>
</div>
);
}
export default DoctorCard;Create Booking Page
3
src/pages/patient/BookAppointment.jsx
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
Why the Backend Receives Enum Values
10:00 AM 2:00 PM 5:00 PM
MORNING_10_AM AFTERNOON_2_PM EVENING_5_PM
public enum TimeSlot {
MORNING_10_AM,
AFTERNOON_2_PM,
EVENING_5_PM
}Human-friendly UI ↓ Enum value ↓ Spring Boot
Why We Don't Send Patient ID
{
"doctorId": 3,
"appointmentDate": "2026-09-26",
"timeSlot": "MORNING_10_AM"
}patientId ❌ consultationFee ❌ appointmentStatus ❌ paymentStatus ❌
JWT ↓ email ↓ User
Update Router
4
import BookAppointment from "../pages/patient/BookAppointment";{
path: "patient/doctors/:doctorId/book",
element: (
<ProtectedRoute allowedRoles={[
"PATIENT"
]} >
<BookAppointment />
</ProtectedRoute>
)
},Test Doctor Navigation
5
Doctor Name Specialization Experience Fee
Test Appointment Booking
6
Date: September 26 Slot: 10:00 AM
POST /api/appointments Authorization: Bearer <patient-token> Content-Type: application/json
{
"doctorId": 3,
"appointmentDate": "2026-09-26",
"timeSlot": "MORNING_10_AM"
}patient → from JWT doctor → ID 3 consultationFee → DoctorProfile appointmentStatus → PENDING paymentStatus → UNPAID
{
"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
Doctor: Dr. Ananya
Date: 2026-09-26
Slot: 10 AMDoctor 3 + 2026-09-26 + MORNING_10_AM
Test Another Slot
8
{
"doctorId": 3,
"appointmentDate": "2026-09-26",
"timeSlot": "AFTERNOON_2_PM"
}Test Another Doctor
9
Dr. Ananya 10 AM
Cancelled Slot Behavior
11
Doctor + Date + Slot
Appointment Doctor 3 10 AM CANCELLED
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)
By Content ITV