diff --git a/client/index.html b/client/index.html index afbe8ee..864a5c7 100644 --- a/client/index.html +++ b/client/index.html @@ -4,7 +4,7 @@ - Vite + React + QuickClinic
diff --git a/client/src/pages/patient/PatientAppointments.jsx b/client/src/pages/patient/PatientAppointments.jsx index 1d2b488..40d37e0 100644 --- a/client/src/pages/patient/PatientAppointments.jsx +++ b/client/src/pages/patient/PatientAppointments.jsx @@ -1,5 +1,17 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; +import { + Calendar, + Search, + Filter, + Clock, + CheckCircle, + FileText, + MapPin, + User, + Eye, + AlertCircle, +} from 'lucide-react'; import { getPatientAppointments } from '../../service/appointmentApiService'; import { getPatientAppointmentPrescription } from '../../service/prescriptionApiSevice'; import AppointmentStats from '../../components/Patient/PatientAppointments/AppointmentStats'; @@ -7,7 +19,21 @@ import AppointmentTabs from '../../components/Patient/PatientAppointments/Appoin import AppointmentHeader from '../../components/Patient/PatientAppointments/AppointmentHeader'; import ErrorState from '../../components/Patient/PatientAppointments/ErrorState'; import Loading from '../../components/ui/Loading'; + +/** + * PatientAppointments Component + * Main component for displaying and managing patient appointments + * + * Features: + * - View upcoming, today's, and past appointments + * - Search and filter appointments + * - Check prescription status + * - Navigate to appointment details + * + * @returns {JSX.Element} Patient appointments page + */ const PatientAppointments = () => { + // State management const [appointments, setAppointments] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -18,41 +44,61 @@ const PatientAppointments = () => { const navigate = useNavigate(); - // Fetch appointments on component mount - useEffect(() => { - fetchAppointments(); - }, []); - - // Fetch appointments function - const fetchAppointments = async () => { + /** + * Fetch appointments from API + * Loads all appointments and checks prescription status + */ + const fetchAppointments = useCallback(async () => { try { setLoading(true); + setError(null); + const response = await getPatientAppointments(); - if (response && response.appointments) { + if (response?.appointments && Array.isArray(response.appointments)) { setAppointments(response.appointments); - // Check prescription status for each appointment await checkPrescriptionStatus(response.appointments); + } else { + throw new Error('Invalid response format'); } } catch (err) { - setError('Failed to fetch appointments. Please try again.'); - console.error('Error fetching appointments:', err); + const errorMessage = err?.message || 'Failed to fetch appointments. Please try again.'; + setError(errorMessage); + console.error('[PatientAppointments] Error fetching appointments:', err); } finally { setLoading(false); } - }; + }, []); - // Check prescription status for appointments + /** + * Check prescription availability for each appointment + * @param {Array} appointmentList - List of appointments to check + */ const checkPrescriptionStatus = async (appointmentList) => { + if (!Array.isArray(appointmentList) || appointmentList.length === 0) { + return; + } + const prescriptionStatusMap = {}; const statusChecks = appointmentList.map(async (appointment) => { + if (!appointment?._id) { + console.warn('[PatientAppointments] Invalid appointment object:', appointment); + return; + } + try { await getPatientAppointmentPrescription(appointment._id); prescriptionStatusMap[appointment._id] = true; } catch (err) { prescriptionStatusMap[appointment._id] = false; - console.log(err); + // Only log if it's not a 404 (prescription not found is expected) + if (err?.response?.status !== 404) { + console.warn( + `[PatientAppointments] Error checking prescription for appointment ${appointment._id}:`, + err + ); + } } }); @@ -60,24 +106,39 @@ const PatientAppointments = () => { setPrescriptionStatus(prescriptionStatusMap); }; - // Filter and search appointments + // Fetch appointments on component mount + useEffect(() => { + fetchAppointments(); + }, [fetchAppointments]); + + /** + * Filter and search appointments based on search term and status + * Memoized for performance optimization + */ const filteredAppointments = useMemo(() => { - let filtered = appointments; + let filtered = [...appointments]; - // Search filter + // Apply search filter if (searchTerm.trim()) { - const term = searchTerm.toLowerCase(); - filtered = filtered.filter( - (appointment) => - appointment.doctorId?.firstName?.toLowerCase().includes(term) || - appointment.doctorId?.lastName?.toLowerCase().includes(term) || - appointment.doctorId?.specialization?.toLowerCase().includes(term) || - appointment.clinicId?.name?.toLowerCase().includes(term) || - appointment.reason?.toLowerCase().includes(term) - ); + const term = searchTerm.toLowerCase().trim(); + filtered = filtered.filter((appointment) => { + const doctorFirstName = appointment.doctorId?.firstName?.toLowerCase() || ''; + const doctorLastName = appointment.doctorId?.lastName?.toLowerCase() || ''; + const specialization = appointment.doctorId?.specialization?.toLowerCase() || ''; + const clinicName = appointment.clinicId?.name?.toLowerCase() || ''; + const reason = appointment.reason?.toLowerCase() || ''; + + return ( + doctorFirstName.includes(term) || + doctorLastName.includes(term) || + specialization.includes(term) || + clinicName.includes(term) || + reason.includes(term) + ); + }); } - // Status filter + // Apply status filter if (filterStatus !== 'all') { filtered = filtered.filter((appointment) => appointment.status === filterStatus); } @@ -85,39 +146,419 @@ const PatientAppointments = () => { return filtered; }, [appointments, searchTerm, filterStatus]); + /** + * Calculate appointment statistics + * Memoized for performance optimization + */ + const stats = useMemo(() => { + const total = appointments.length; + const upcoming = appointments.filter((a) => a.status === 'scheduled').length; + const completed = appointments.filter((a) => a.status === 'completed').length; + const withPrescription = Object.values(prescriptionStatus).filter(Boolean).length; + + return { total, upcoming, completed, withPrescription }; + }, [appointments, prescriptionStatus]); + + /** + * Get appointments filtered by active tab + * Filters by date and status based on tab selection + */ + const tabAppointments = useMemo(() => { + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + + return filteredAppointments.filter((apt) => { + const aptDate = new Date(apt.date); + + if (activeTab === 'upcoming') { + return aptDate >= today && apt.status === 'scheduled'; + } else if (activeTab === 'today') { + return aptDate.toDateString() === today.toDateString(); + } else if (activeTab === 'past') { + return aptDate < today || apt.status === 'completed'; + } + return true; + }); + }, [filteredAppointments, activeTab]); + + /** + * Handle appointment card click + * @param {string} appointmentId - ID of the appointment to view + */ + const handleAppointmentClick = useCallback( + (appointmentId) => { + if (!appointmentId) { + console.error('[PatientAppointments] Invalid appointment ID'); + return; + } + navigate(`/patient/appointment/${appointmentId}`); + }, + [navigate] + ); + + /** + * Handle search input change + * @param {Event} e - Input change event + */ + const handleSearchChange = useCallback((e) => { + setSearchTerm(e.target.value); + }, []); + + /** + * Handle filter status change + * @param {Event} e - Select change event + */ + const handleFilterChange = useCallback((e) => { + setFilterStatus(e.target.value); + }, []); + + /** + * Handle tab change + * @param {string} tab - Tab identifier + */ + const handleTabChange = useCallback((tab) => { + setActiveTab(tab); + }, []); + + // Loading state if (loading) { return ; } return (
-
- {/* Header */} - +
+ {/* Header Section */} +
+
+

My Appointments

+

+ Manage and view your medical appointments +

+
+ + {/* Search and Filter */} +
+
+
+ +
+
+
+
{/* Error State */} - {error && } + {error && ( +
+ +
+

Error

+

{error}

+
+ +
+ )} + + {/* Tabs */} +
+
+
+ {[ + { id: 'upcoming', label: 'Upcoming', count: tabCounts.upcoming }, + { id: 'today', label: 'Today', count: tabCounts.today }, + { id: 'past', label: 'Past', count: tabCounts.past }, + ].map((tab) => ( + + ))} +
+
+
{/* Main Content */} - navigate(`/patient/appointment/${appointmentId}`)} - /> - - {/* Quick Stats */} - +
+ {tabAppointments.length === 0 ? ( +
+
+ ) : ( +
+ {tabAppointments.map((appointment) => ( + handleAppointmentClick(appointment._id)} + /> + ))} +
+ )} +
+ + {/* Stats Cards */} +
+ } + iconBg="bg-gray-100" + iconColor="text-gray-600" + /> + } + iconBg="bg-blue-100" + iconColor="text-blue-600" + /> + } + iconBg="bg-green-100" + iconColor="text-green-600" + /> + } + iconBg="bg-purple-100" + iconColor="text-purple-600" + /> +
+
+
+ ); +}; + +/** + * AppointmentCard Component + * Displays individual appointment information + * + * @param {Object} props - Component props + * @param {Object} props.appointment - Appointment data + * @param {boolean} props.hasPrescription - Whether prescription is available + * @param {Function} props.onClick - Click handler + */ +const AppointmentCard = ({ appointment, hasPrescription, onClick }) => { + const getStatusBadge = (status) => { + const statusConfig = { + scheduled: { + label: 'Pending', + bg: 'bg-yellow-50', + text: 'text-yellow-700', + border: 'border-yellow-200', + }, + completed: { + label: 'Completed', + bg: 'bg-green-50', + text: 'text-green-700', + border: 'border-green-200', + }, + cancelled: { + label: 'Cancelled', + bg: 'bg-red-50', + text: 'text-red-700', + border: 'border-red-200', + }, + }; + + const config = statusConfig[status] || statusConfig.scheduled; + + return ( + + ⏱ {config.label} + + ); + }; + + const getInitials = (firstName, lastName) => { + const first = firstName?.charAt(0) || ''; + const last = lastName?.charAt(0) || ''; + return `${first}${last}`.toUpperCase() || 'NA'; + }; + + return ( +
{ + if (e.key === 'Enter' || e.key === ' ') { + onClick(); + } + }} + aria-label={`View appointment with Dr. ${appointment.doctorId?.firstName} ${appointment.doctorId?.lastName}`} + > + {/* Header with Avatar and Status */} +
+
+
+ {getInitials(appointment.doctorId?.firstName, appointment.doctorId?.lastName)} +
+
+

+ Dr. {appointment.doctorId?.firstName || 'N/A'} {appointment.doctorId?.lastName || ''} +

+

+ {appointment.doctorId?.specialization || 'General'} +

+
+
+ {getStatusBadge(appointment.status)} +
+ + {/* Date and Time */} +
+
+
+
+
+ {appointment.visitType && ( +
+
+ )} +
+ + {/* Reason */} + {appointment.reason && ( +
+

Reason:

+

{appointment.reason}

+
+ )} + + {/* Prescription Status */} +
+
+
+ {hasPrescription && ( + + )} +
+
+ ); +}; + +/** + * StatCard Component + * Displays appointment statistics + * + * @param {Object} props - Component props + * @param {string} props.label - Stat label + * @param {number} props.value - Stat value + * @param {JSX.Element} props.icon - Icon component + * @param {string} props.iconBg - Icon background color class + * @param {string} props.iconColor - Icon color class + */ +const StatCard = ({ label, value, icon, iconBg, iconColor }) => { + return ( +
+
+
+

{label}

+

{value}

+
+
);