2025-11-24 16:04:39 +00:00
"use client" ;
import { useState , useEffect } from "react" ;
import { useParams , useRouter } from "next/navigation" ;
import {
Calendar ,
Clock ,
User ,
Video ,
CalendarCheck ,
X ,
Loader2 ,
ArrowLeft ,
Mail ,
Phone as PhoneIcon ,
MessageSquare ,
CheckCircle2 ,
ExternalLink ,
Copy ,
MapPin ,
2025-12-05 18:19:19 +00:00
Pencil ,
2025-11-24 16:04:39 +00:00
} from "lucide-react" ;
import { useAppTheme } from "@/components/ThemeProvider" ;
2025-12-05 18:35:20 +00:00
import { getAppointmentDetail , scheduleAppointment , rejectAppointment , listAppointments , startMeeting , endMeeting , rescheduleAppointment , cancelAppointment } from "@/lib/actions/appointments" ;
2025-11-24 16:04:39 +00:00
import { Button } from "@/components/ui/button" ;
import {
Dialog ,
DialogContent ,
DialogDescription ,
DialogFooter ,
DialogHeader ,
DialogTitle ,
} from "@/components/ui/dialog" ;
2025-11-27 19:18:59 +00:00
import { ScheduleAppointmentDialog } from "@/components/ScheduleAppointmentDialog" ;
2025-11-24 16:04:39 +00:00
import { toast } from "sonner" ;
import type { Appointment } from "@/lib/models/appointments" ;
export default function AppointmentDetailPage() {
const params = useParams ( ) ;
const router = useRouter ( ) ;
const appointmentId = params . id as string ;
const [ appointment , setAppointment ] = useState < Appointment | null > ( null ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ scheduleDialogOpen , setScheduleDialogOpen ] = useState ( false ) ;
const [ rejectDialogOpen , setRejectDialogOpen ] = useState ( false ) ;
2025-12-05 18:19:19 +00:00
const [ rescheduleDialogOpen , setRescheduleDialogOpen ] = useState ( false ) ;
2025-11-24 16:04:39 +00:00
const [ scheduledDate , setScheduledDate ] = useState < Date | undefined > ( undefined ) ;
const [ scheduledTime , setScheduledTime ] = useState < string > ( "09:00" ) ;
const [ scheduledDuration , setScheduledDuration ] = useState < number > ( 60 ) ;
2025-12-05 18:19:19 +00:00
const [ rescheduleDate , setRescheduleDate ] = useState < Date | undefined > ( undefined ) ;
const [ rescheduleTime , setRescheduleTime ] = useState < string > ( "09:00" ) ;
const [ rescheduleDuration , setRescheduleDuration ] = useState < number > ( 60 ) ;
2025-11-24 16:04:39 +00:00
const [ rejectionReason , setRejectionReason ] = useState < string > ( "" ) ;
const [ isScheduling , setIsScheduling ] = useState ( false ) ;
const [ isRejecting , setIsRejecting ] = useState ( false ) ;
2025-12-05 18:19:19 +00:00
const [ isRescheduling , setIsRescheduling ] = useState ( false ) ;
2025-12-04 15:36:14 +00:00
const [ isStartingMeeting , setIsStartingMeeting ] = useState ( false ) ;
const [ isEndingMeeting , setIsEndingMeeting ] = useState ( false ) ;
2025-12-05 18:35:20 +00:00
const [ cancelDialogOpen , setCancelDialogOpen ] = useState ( false ) ;
const [ isCancelling , setIsCancelling ] = useState ( false ) ;
2025-11-24 16:04:39 +00:00
const { theme } = useAppTheme ( ) ;
const isDark = theme === "dark" ;
useEffect ( ( ) = > {
const fetchAppointment = async ( ) = > {
if ( ! appointmentId ) return ;
setLoading ( true ) ;
try {
2025-12-03 18:27:09 +00:00
// Fetch both detail and list to get selected_slots from list endpoint
const [ detailData , listData ] = await Promise . all ( [
getAppointmentDetail ( appointmentId ) ,
listAppointments ( ) . catch ( ( ) = > [ ] ) // Fallback to empty array if list fails
] ) ;
// Find matching appointment in list to get selected_slots
const listAppointment = Array . isArray ( listData )
? listData . find ( ( apt : Appointment ) = > apt . id === appointmentId )
: null ;
// Merge selected_slots from list into detail data
if ( listAppointment && listAppointment . selected_slots && Array . isArray ( listAppointment . selected_slots ) && listAppointment . selected_slots . length > 0 ) {
detailData . selected_slots = listAppointment . selected_slots ;
}
setAppointment ( detailData ) ;
2025-11-24 16:04:39 +00:00
} catch ( error ) {
toast . error ( "Failed to load appointment details" ) ;
router . push ( "/admin/booking" ) ;
} finally {
setLoading ( false ) ;
}
} ;
fetchAppointment ( ) ;
} , [ appointmentId , router ] ) ;
const formatDate = ( dateString : string ) = > {
const date = new Date ( dateString ) ;
return date . toLocaleDateString ( "en-US" , {
weekday : "long" ,
month : "long" ,
day : "numeric" ,
year : "numeric" ,
} ) ;
} ;
const formatTime = ( dateString : string ) = > {
const date = new Date ( dateString ) ;
return date . toLocaleTimeString ( "en-US" , {
hour : "numeric" ,
minute : "2-digit" ,
hour12 : true ,
} ) ;
} ;
const formatShortDate = ( dateString : string ) = > {
const date = new Date ( dateString ) ;
return date . toLocaleDateString ( "en-US" , {
month : "short" ,
day : "numeric" ,
year : "numeric" ,
} ) ;
} ;
const getStatusColor = ( status : string ) = > {
const normalized = status . toLowerCase ( ) ;
if ( isDark ) {
switch ( normalized ) {
case "scheduled" :
return "bg-blue-500/20 text-blue-300 border-blue-500/30" ;
case "completed" :
return "bg-green-500/20 text-green-300 border-green-500/30" ;
case "rejected" :
case "cancelled" :
return "bg-red-500/20 text-red-300 border-red-500/30" ;
case "pending_review" :
case "pending" :
return "bg-yellow-500/20 text-yellow-300 border-yellow-500/30" ;
default :
return "bg-gray-700 text-gray-200 border-gray-600" ;
}
}
switch ( normalized ) {
case "scheduled" :
return "bg-blue-50 text-blue-700 border-blue-200" ;
case "completed" :
return "bg-green-50 text-green-700 border-green-200" ;
case "rejected" :
case "cancelled" :
return "bg-red-50 text-red-700 border-red-200" ;
case "pending_review" :
case "pending" :
return "bg-yellow-50 text-yellow-700 border-yellow-200" ;
default :
return "bg-gray-100 text-gray-700 border-gray-300" ;
}
} ;
const formatStatus = ( status : string ) = > {
return status . replace ( "_" , " " ) . replace ( /\b\w/g , ( l ) = > l . toUpperCase ( ) ) ;
} ;
const handleSchedule = async ( ) = > {
if ( ! appointment || ! scheduledDate ) return ;
setIsScheduling ( true ) ;
try {
const dateTime = new Date ( scheduledDate ) ;
const [ hours , minutes ] = scheduledTime . split ( ":" ) . map ( Number ) ;
dateTime . setHours ( hours , minutes , 0 , 0 ) ;
await scheduleAppointment ( appointment . id , {
scheduled_datetime : dateTime.toISOString ( ) ,
scheduled_duration : scheduledDuration ,
} ) ;
toast . success ( "Appointment scheduled successfully" ) ;
setScheduleDialogOpen ( false ) ;
// Refresh appointment data
const updated = await getAppointmentDetail ( appointment . id ) ;
setAppointment ( updated ) ;
} catch ( error : any ) {
toast . error ( error . message || "Failed to schedule appointment" ) ;
} finally {
setIsScheduling ( false ) ;
}
} ;
const handleReject = async ( ) = > {
if ( ! appointment ) return ;
setIsRejecting ( true ) ;
try {
await rejectAppointment ( appointment . id , {
rejection_reason : rejectionReason || undefined ,
} ) ;
toast . success ( "Appointment rejected successfully" ) ;
setRejectDialogOpen ( false ) ;
// Refresh appointment data
const updated = await getAppointmentDetail ( appointment . id ) ;
setAppointment ( updated ) ;
} catch ( error : any ) {
toast . error ( error . message || "Failed to reject appointment" ) ;
} finally {
setIsRejecting ( false ) ;
}
} ;
2025-12-05 18:35:20 +00:00
const handleReschedule = async ( ) = > {
if ( ! appointment || ! rescheduleDate ) return ;
setIsRescheduling ( true ) ;
try {
const dateTime = new Date ( rescheduleDate ) ;
const [ hours , minutes ] = rescheduleTime . split ( ":" ) . map ( Number ) ;
dateTime . setHours ( hours , minutes , 0 , 0 ) ;
const userTimezone = Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone ;
await rescheduleAppointment ( appointment . id , {
new_scheduled_datetime : dateTime.toISOString ( ) ,
new_scheduled_duration : rescheduleDuration ,
timezone : userTimezone ,
} ) ;
toast . success ( "Appointment rescheduled successfully" ) ;
setRescheduleDialogOpen ( false ) ;
// Refresh appointment data
const updated = await getAppointmentDetail ( appointment . id ) ;
setAppointment ( updated ) ;
} catch ( error : any ) {
toast . error ( error . message || "Failed to reschedule appointment" ) ;
} finally {
setIsRescheduling ( false ) ;
}
} ;
const handleCancelAppointment = async ( ) = > {
if ( ! appointment ) return ;
setIsCancelling ( true ) ;
try {
await cancelAppointment ( appointment . id ) ;
toast . success ( "Appointment cancelled successfully" ) ;
setCancelDialogOpen ( false ) ;
// Refetch appointment to get updated status
const updatedAppointment = await getAppointmentDetail ( appointment . id ) ;
setAppointment ( updatedAppointment ) ;
router . push ( "/admin/booking" ) ;
} catch ( error ) {
const errorMessage = error instanceof Error ? error . message : "Failed to cancel appointment" ;
toast . error ( errorMessage ) ;
} finally {
setIsCancelling ( false ) ;
}
} ;
2025-11-24 16:04:39 +00:00
const copyToClipboard = ( text : string , label : string ) = > {
navigator . clipboard . writeText ( text ) ;
toast . success ( ` ${ label } copied to clipboard ` ) ;
} ;
2025-12-04 15:36:14 +00:00
const handleStartMeeting = async ( ) = > {
if ( ! appointment ) return ;
setIsStartingMeeting ( true ) ;
try {
const updated = await startMeeting ( appointment . id ) ;
setAppointment ( updated ) ;
toast . success ( "Meeting started successfully" ) ;
} catch ( error : any ) {
toast . error ( error . message || "Failed to start meeting" ) ;
} finally {
setIsStartingMeeting ( false ) ;
}
} ;
const handleEndMeeting = async ( ) = > {
if ( ! appointment ) return ;
setIsEndingMeeting ( true ) ;
try {
const updated = await endMeeting ( appointment . id ) ;
setAppointment ( updated ) ;
toast . success ( "Meeting ended successfully" ) ;
} catch ( error : any ) {
toast . error ( error . message || "Failed to end meeting" ) ;
} finally {
setIsEndingMeeting ( false ) ;
}
} ;
2025-11-24 16:04:39 +00:00
if ( loading ) {
return (
2025-11-26 11:42:31 +00:00
< div className = { ` min-h-[calc(100vh-4rem)] flex items-center justify-center ${ isDark ? "bg-gray-900" : "bg-gray-50" } ` } >
2025-11-24 16:04:39 +00:00
< div className = "text-center" >
< Loader2 className = { ` w-12 h-12 animate-spin mx-auto mb-4 ${ isDark ? "text-rose-400" : "text-rose-600" } ` } / >
< p className = { ` text-sm ${ isDark ? "text-gray-400" : "text-gray-600" } ` } > Loading appointment details . . . < / p >
< / div >
< / div >
) ;
}
if ( ! appointment ) {
return (
2025-11-26 11:42:31 +00:00
< div className = { ` min-h-[calc(100vh-4rem)] flex items-center justify-center ${ isDark ? "bg-gray-900" : "bg-gray-50" } ` } >
2025-11-24 16:04:39 +00:00
< div className = "text-center" >
< p className = { ` text-lg mb-4 ${ isDark ? "text-gray-400" : "text-gray-600" } ` } > Appointment not found < / p >
< Button
onClick = { ( ) = > router . push ( "/admin/booking" ) }
className = "bg-rose-600 hover:bg-rose-700 text-white"
>
< ArrowLeft className = "w-4 h-4 mr-2" / >
Back to Bookings
< / Button >
< / div >
< / div >
) ;
}
return (
< div className = { ` min-h-screen ${ isDark ? "bg-gray-900" : "bg-gray-50" } ` } >
2025-11-26 11:42:31 +00:00
< main className = "p-3 sm:p-4 md:p-6 lg:p-8" >
{ /* Page Header */ }
< div className = "mb-4 sm:mb-6 flex flex-col gap-3 sm:gap-4" >
2025-11-24 16:04:39 +00:00
< Button
variant = "ghost"
onClick = { ( ) = > router . push ( "/admin/booking" ) }
2025-11-26 11:42:31 +00:00
className = { ` flex items-center gap-2 w-fit ${ isDark ? "text-gray-300 hover:bg-gray-800 hover:text-white" : "text-gray-600 hover:bg-gray-100" } ` }
2025-11-24 16:04:39 +00:00
>
< ArrowLeft className = "w-4 h-4" / >
Back to Bookings
< / Button >
< div className = "flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4" >
< div >
< div className = "flex items-center gap-3 mb-2" >
2025-11-26 11:42:31 +00:00
< div className = { ` h-12 w-12 sm:h-16 sm:w-16 rounded-full flex items-center justify-center text-xl sm:text-2xl font-bold ${ isDark ? "bg-gradient-to-br from-rose-500 to-pink-600 text-white" : "bg-gradient-to-br from-rose-100 to-pink-100 text-rose-600" } ` } >
2025-11-24 16:04:39 +00:00
{ appointment . first_name [ 0 ] } { appointment . last_name [ 0 ] }
< / div >
< div >
2025-11-26 11:42:31 +00:00
< h1 className = { ` text-2xl sm:text-3xl lg:text-4xl font-bold ${ isDark ? "text-white" : "text-gray-900" } ` } >
2025-11-24 16:04:39 +00:00
{ appointment . first_name } { appointment . last_name }
< / h1 >
2025-11-26 11:42:31 +00:00
< p className = { ` text-xs sm:text-sm mt-1 ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
2025-11-24 16:04:39 +00:00
Appointment Request
< / p >
< / div >
< / div >
< / div >
< div className = "flex items-center gap-3" >
< span
2025-11-26 11:42:31 +00:00
className = { ` px-3 sm:px-4 py-2 inline-flex items-center gap-2 text-xs sm:text-sm font-semibold rounded-full border ${ getStatusColor (
2025-11-24 16:04:39 +00:00
appointment . status
) } ` }
>
2025-11-26 11:42:31 +00:00
{ appointment . status === "scheduled" && < CheckCircle2 className = "w-3 h-3 sm:w-4 sm:h-4" / > }
2025-11-24 16:04:39 +00:00
{ formatStatus ( appointment . status ) }
< / span >
< / div >
< / div >
< / div >
< div className = "grid grid-cols-1 lg:grid-cols-3 gap-6" >
{ /* Main Content - Left Column (2/3) */ }
< div className = "lg:col-span-2 space-y-6" >
{ /* Patient Information Card */ }
< div className = { ` rounded-2xl border shadow-sm overflow-hidden ${ isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200" } ` } >
< div className = { ` px-6 py-4 border-b ${ isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50" } ` } >
< h2 className = { ` text-lg font-semibold flex items-center gap-2 ${ isDark ? "text-white" : "text-gray-900" } ` } >
< User className = { ` w-5 h-5 ${ isDark ? "text-rose-400" : "text-rose-600" } ` } / >
Patient Information
< / h2 >
< / div >
< div className = "p-6" >
< div className = "grid grid-cols-1 sm:grid-cols-2 gap-6" >
< div className = "space-y-1" >
< p className = { ` text-xs font-medium uppercase tracking-wider ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
Full Name
< / p >
< p className = { ` text-base font-medium ${ isDark ? "text-white" : "text-gray-900" } ` } >
{ appointment . first_name } { appointment . last_name }
< / p >
< / div >
< div className = "space-y-1" >
< p className = { ` text-xs font-medium uppercase tracking-wider flex items-center gap-1 ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
< Mail className = "w-3 h-3" / >
Email Address
< / p >
< div className = "flex items-center gap-2" >
< p className = { ` text-base font-medium ${ isDark ? "text-white" : "text-gray-900" } ` } >
{ appointment . email }
< / p >
< button
onClick = { ( ) = > copyToClipboard ( appointment . email , "Email" ) }
className = { ` p-1.5 rounded-lg hover:bg-opacity-80 transition-colors ${ isDark ? "hover:bg-gray-700" : "hover:bg-gray-100" } ` }
title = "Copy email"
>
< Copy className = { ` w-4 h-4 ${ isDark ? "text-gray-400" : "text-gray-500" } ` } / >
< / button >
< / div >
< / div >
{ appointment . phone && (
< div className = "space-y-1" >
< p className = { ` text-xs font-medium uppercase tracking-wider flex items-center gap-1 ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
< PhoneIcon className = "w-3 h-3" / >
Phone Number
< / p >
< div className = "flex items-center gap-2" >
< p className = { ` text-base font-medium ${ isDark ? "text-white" : "text-gray-900" } ` } >
{ appointment . phone }
< / p >
< button
onClick = { ( ) = > copyToClipboard ( appointment . phone ! , "Phone" ) }
className = { ` p-1.5 rounded-lg hover:bg-opacity-80 transition-colors ${ isDark ? "hover:bg-gray-700" : "hover:bg-gray-100" } ` }
title = "Copy phone"
>
< Copy className = { ` w-4 h-4 ${ isDark ? "text-gray-400" : "text-gray-500" } ` } / >
< / button >
< / div >
< / div >
) }
< / div >
< / div >
< / div >
{ /* Appointment Details Card */ }
{ appointment . scheduled_datetime && (
< div className = { ` rounded-2xl border shadow-sm overflow-hidden ${ isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200" } ` } >
< div className = { ` px-6 py-4 border-b ${ isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50" } ` } >
2025-12-05 18:35:20 +00:00
< div className = "flex items-center justify-between" >
< h2 className = { ` text-lg font-semibold flex items-center gap-2 ${ isDark ? "text-white" : "text-gray-900" } ` } >
< Calendar className = { ` w-5 h-5 ${ isDark ? "text-rose-400" : "text-rose-600" } ` } / >
Scheduled Appointment
< / h2 >
{ appointment . status === "scheduled" && (
< button
onClick = { ( ) = > {
// Initialize reschedule fields with current appointment values
if ( appointment . scheduled_datetime ) {
const scheduledDate = new Date ( appointment . scheduled_datetime ) ;
setRescheduleDate ( scheduledDate ) ;
const hours = scheduledDate . getHours ( ) . toString ( ) . padStart ( 2 , "0" ) ;
const minutes = scheduledDate . getMinutes ( ) . toString ( ) . padStart ( 2 , "0" ) ;
setRescheduleTime ( ` ${ hours } : ${ minutes } ` ) ;
}
if ( appointment . scheduled_duration ) {
setRescheduleDuration ( appointment . scheduled_duration ) ;
}
setRescheduleDialogOpen ( true ) ;
} }
className = { ` p-2 rounded-lg transition-colors ${ isDark ? "hover:bg-gray-700 text-gray-300 hover:text-white" : "hover:bg-gray-100 text-gray-600 hover:text-gray-900" } ` }
title = "Reschedule Appointment"
>
< Pencil className = "w-4 h-4" / >
< / button >
) }
< / div >
2025-11-24 16:04:39 +00:00
< / div >
< div className = "p-6" >
< div className = "flex items-start gap-4" >
< div className = { ` p-4 rounded-xl ${ isDark ? "bg-blue-500/10 border border-blue-500/20" : "bg-blue-50 border border-blue-100" } ` } >
< Calendar className = { ` w-6 h-6 ${ isDark ? "text-blue-400" : "text-blue-600" } ` } / >
< / div >
< div className = "flex-1" >
< p className = { ` text-2xl font-bold mb-1 ${ isDark ? "text-white" : "text-gray-900" } ` } >
{ formatDate ( appointment . scheduled_datetime ) }
< / p >
< div className = "flex items-center gap-4 mt-2" >
< div className = "flex items-center gap-2" >
< Clock className = { ` w-4 h-4 ${ isDark ? "text-gray-400" : "text-gray-500" } ` } / >
< p className = { ` text-base ${ isDark ? "text-gray-300" : "text-gray-700" } ` } >
{ formatTime ( appointment . scheduled_datetime ) }
< / p >
< / div >
{ appointment . scheduled_duration && (
< div className = "flex items-center gap-2" >
< span className = { ` text-base ${ isDark ? "text-gray-400" : "text-gray-500" } ` } > • < / span >
< p className = { ` text-base ${ isDark ? "text-gray-300" : "text-gray-700" } ` } >
{ appointment . scheduled_duration } minutes
< / p >
< / div >
) }
< / div >
< / div >
< / div >
< / div >
< / div >
) }
2025-12-03 18:27:09 +00:00
{ /* Selected Slots */ }
2025-12-03 11:03:01 +00:00
{ appointment . selected_slots && Array . isArray ( appointment . selected_slots ) && appointment . selected_slots . length > 0 && (
2025-11-27 19:18:59 +00:00
< div className = { ` rounded-2xl border shadow-sm overflow-hidden ${ isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200" } ` } >
< div className = { ` px-6 py-4 border-b ${ isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50" } ` } >
< h2 className = { ` text-lg font-semibold flex items-center gap-2 ${ isDark ? "text-white" : "text-gray-900" } ` } >
2025-12-03 16:25:28 +00:00
< CalendarCheck className = { ` w-5 h-5 ${ isDark ? "text-green-400" : "text-green-600" } ` } / >
2025-12-03 11:03:01 +00:00
Selected Time Slots
2025-12-03 16:25:28 +00:00
{ appointment . are_preferences_available !== undefined && (
< span className = { ` ml-auto px-3 py-1 text-xs font-medium rounded-full ${ appointment . are_preferences_available ? ( isDark ? "bg-green-500/20 text-green-300 border border-green-500/30" : "bg-green-50 text-green-700 border border-green-200" ) : ( isDark ? "bg-yellow-500/20 text-yellow-300 border border-yellow-500/30" : "bg-yellow-50 text-yellow-700 border border-yellow-200" ) } ` } >
{ appointment . are_preferences_available ? "All Available" : "Partially Available" }
< / span >
) }
2025-11-27 19:18:59 +00:00
< / h2 >
< / div >
< div className = "p-6" >
2025-12-03 18:27:09 +00:00
{ ( ( ) = > {
const dayNames : Record < number , string > = {
0 : "Monday" ,
1 : "Tuesday" ,
2 : "Wednesday" ,
3 : "Thursday" ,
4 : "Friday" ,
5 : "Saturday" ,
6 : "Sunday" ,
} ;
const timeSlotLabels : Record < string , string > = {
morning : "Morning" ,
afternoon : "Lunchtime" ,
evening : "Evening" ,
} ;
2025-12-04 10:48:14 +00:00
// Time slot order: morning, afternoon (lunchtime), evening
const timeSlotOrder : Record < string , number > = {
morning : 0 ,
afternoon : 1 ,
evening : 2 ,
} ;
2025-12-03 18:27:09 +00:00
// Group slots by date
const slotsByDate : Record < string , typeof appointment.selected_slots > = { } ;
appointment . selected_slots . forEach ( ( slot : any ) = > {
const date = slot . date || "" ;
if ( ! slotsByDate [ date ] ) {
slotsByDate [ date ] = [ ] ;
}
slotsByDate [ date ] . push ( slot ) ;
} ) ;
2025-12-04 10:48:14 +00:00
// Sort dates and slots within each date
const sortedDates = Object . keys ( slotsByDate ) . sort ( ( a , b ) = > {
return new Date ( a ) . getTime ( ) - new Date ( b ) . getTime ( ) ;
} ) ;
2025-12-03 18:27:09 +00:00
return (
< div className = "space-y-4" >
2025-12-04 10:48:14 +00:00
{ sortedDates . map ( ( date ) = > {
// Sort slots within this date by time slot order
const slots = slotsByDate [ date ] . sort ( ( a : any , b : any ) = > {
const aSlot = String ( a . time_slot ) . toLowerCase ( ) . trim ( ) ;
const bSlot = String ( b . time_slot ) . toLowerCase ( ) . trim ( ) ;
const aOrder = timeSlotOrder [ aSlot ] ? ? 999 ;
const bOrder = timeSlotOrder [ bSlot ] ? ? 999 ;
return aOrder - bOrder ;
} ) ;
return (
< div key = { date } className = { ` p-4 rounded-xl border ${ isDark ? "bg-gray-700/50 border-gray-600" : "bg-gray-50 border-gray-200" } ` } >
< div className = "mb-3" >
< p className = { ` text-base font-semibold ${ isDark ? "text-white" : "text-gray-900" } ` } >
{ formatShortDate ( date ) }
2025-12-03 18:27:09 +00:00
< / p >
2025-12-04 10:48:14 +00:00
{ slots . length > 0 && slots [ 0 ] ? . day !== undefined && (
< p className = { ` text-sm mt-1 ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
{ dayNames [ slots [ 0 ] . day ] || ` Day ${ slots [ 0 ] . day } ` }
< / p >
) }
< / div >
< div className = "flex flex-wrap gap-2" >
{ slots . map ( ( slot : any , idx : number ) = > {
const timeSlot = String ( slot . time_slot ) . toLowerCase ( ) . trim ( ) ;
const timeLabel = timeSlotLabels [ timeSlot ] || slot . time_slot ;
return (
< span
key = { idx }
className = { ` px-3 py-1.5 rounded-lg text-sm font-medium ${ isDark ? "bg-green-500/20 text-green-300 border border-green-500/30" : "bg-green-50 text-green-700 border border-green-200" } ` }
>
{ timeLabel }
< / span >
) ;
} ) }
< / div >
2025-12-03 18:27:09 +00:00
< / div >
2025-12-04 10:48:14 +00:00
) ;
} ) }
2025-12-03 18:27:09 +00:00
< / div >
) ;
} ) ( ) }
2025-11-27 19:18:59 +00:00
< / div >
< / div >
) }
2025-11-24 16:04:39 +00:00
{ /* Reason */ }
{ appointment . reason && (
< div className = { ` rounded-2xl border shadow-sm overflow-hidden ${ isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200" } ` } >
< div className = { ` px-6 py-4 border-b ${ isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50" } ` } >
< h2 className = { ` text-lg font-semibold flex items-center gap-2 ${ isDark ? "text-white" : "text-gray-900" } ` } >
< MessageSquare className = { ` w-5 h-5 ${ isDark ? "text-rose-400" : "text-rose-600" } ` } / >
Reason for Appointment
< / h2 >
< / div >
< div className = "p-6" >
< p className = { ` text-base leading-relaxed ${ isDark ? "text-gray-300" : "text-gray-700" } ` } >
{ appointment . reason }
< / p >
< / div >
< / div >
) }
{ /* Rejection Reason */ }
{ appointment . rejection_reason && (
< div className = { ` rounded-2xl border shadow-sm overflow-hidden ${ isDark ? "bg-red-900/20 border-red-800/50" : "bg-red-50 border-red-200" } ` } >
< div className = { ` px-6 py-4 border-b ${ isDark ? "border-red-800/50" : "border-red-200" } ` } >
< h2 className = { ` text-lg font-semibold ${ isDark ? "text-red-300" : "text-red-900" } ` } >
Rejection Reason
< / h2 >
< / div >
< div className = "p-6" >
< p className = { ` text-base leading-relaxed ${ isDark ? "text-red-200" : "text-red-800" } ` } >
{ appointment . rejection_reason }
< / p >
< / div >
< / div >
) }
{ /* Meeting Information */ }
2025-12-03 18:39:30 +00:00
{ appointment . moderator_join_url && (
2025-11-24 16:04:39 +00:00
< div className = { ` rounded-2xl border shadow-sm overflow-hidden ${ isDark ? "bg-gradient-to-br from-blue-900/20 to-purple-900/20 border-blue-800/30" : "bg-gradient-to-br from-blue-50 to-purple-50 border-blue-200" } ` } >
< div className = { ` px-6 py-4 border-b ${ isDark ? "border-blue-800/30" : "border-blue-200" } ` } >
< h2 className = { ` text-lg font-semibold flex items-center gap-2 ${ isDark ? "text-white" : "text-gray-900" } ` } >
< Video className = { ` w-5 h-5 ${ isDark ? "text-blue-400" : "text-blue-600" } ` } / >
Video Meeting
< / h2 >
< / div >
< div className = "p-6 space-y-4" >
{ appointment . jitsi_room_id && (
< div >
< p className = { ` text-xs font-medium mb-2 uppercase tracking-wider ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
Meeting Room ID
< / p >
< div className = "flex items-center gap-2" >
< p className = { ` text-sm font-mono px-3 py-2 rounded-lg ${ isDark ? "bg-gray-800 text-gray-200" : "bg-white text-gray-900 border border-gray-200" } ` } >
{ appointment . jitsi_room_id }
< / p >
< button
2025-12-03 18:39:30 +00:00
onClick = { ( ) = > appointment . can_join_as_moderator && copyToClipboard ( appointment . jitsi_room_id ! , "Room ID" ) }
disabled = { ! appointment . can_join_as_moderator }
className = { ` p-2 rounded-lg transition-colors ${ appointment . can_join_as_moderator ? ( isDark ? "hover:bg-gray-700" : "hover:bg-gray-100" ) : ( isDark ? "opacity-50 cursor-not-allowed" : "opacity-50 cursor-not-allowed" ) } ` }
title = { appointment . can_join_as_moderator ? "Copy room ID" : "Meeting not available" }
2025-11-24 16:04:39 +00:00
>
< Copy className = { ` w-4 h-4 ${ isDark ? "text-gray-400" : "text-gray-500" } ` } / >
< / button >
< / div >
< / div >
) }
< div >
< p className = { ` text-xs font-medium mb-2 uppercase tracking-wider ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
2025-12-03 18:39:30 +00:00
Moderator Meeting Link
2025-11-24 16:04:39 +00:00
< / p >
< div className = "flex items-center gap-2" >
2025-12-04 19:30:08 +00:00
< div className = { ` flex-1 text-sm px-3 py-2 rounded-lg truncate ${ isDark ? "bg-gray-800/50 text-gray-500 border border-gray-700" : "bg-gray-100 text-gray-400 border border-gray-300" } ` } >
2025-12-03 18:39:30 +00:00
{ appointment . moderator_join_url }
2025-12-04 19:30:08 +00:00
< / div >
< button
disabled
className = { ` px-4 py-2 rounded-lg font-medium cursor-not-allowed ${ isDark ? "bg-gray-700 text-gray-500" : "bg-gray-300 text-gray-500" } ` }
2025-12-01 17:35:28 +00:00
>
< ExternalLink className = "w-4 h-4" / >
2025-12-04 19:30:08 +00:00
< / button >
2025-11-24 16:04:39 +00:00
< / div >
< / div >
2025-12-03 18:39:30 +00:00
{ appointment . can_join_as_moderator !== undefined && (
< div className = { ` flex items-center gap-2 px-4 py-3 rounded-lg ${ appointment . can_join_as_moderator ? ( isDark ? "bg-green-500/20 border border-green-500/30" : "bg-green-50 border border-green-200" ) : ( isDark ? "bg-gray-800 border border-gray-700" : "bg-gray-50 border border-gray-200" ) } ` } >
< div className = { ` h-2 w-2 rounded-full ${ appointment . can_join_as_moderator ? ( isDark ? "bg-green-400" : "bg-green-600" ) : ( isDark ? "bg-gray-500" : "bg-gray-400" ) } ` } / >
< p className = { ` text-sm font-medium ${ appointment . can_join_as_moderator ? ( isDark ? "text-green-300" : "text-green-700" ) : ( isDark ? "text-gray-400" : "text-gray-500" ) } ` } >
2025-12-05 18:00:54 +00:00
{ appointment . can_join_as_moderator
? "Meeting is active - You can join as moderator"
: appointment . scheduled_datetime
? ` Meeting would be available to join starting at ${ formatTime ( appointment . scheduled_datetime ) } `
: "Meeting would be available shortly" }
2025-11-24 16:04:39 +00:00
< / p >
< / div >
) }
< / div >
< / div >
) }
< / div >
{ /* Sidebar - Right Column (1/3) */ }
< div className = "space-y-6" >
{ /* Quick Info Card */ }
< div className = { ` rounded-2xl border shadow-sm overflow-hidden ${ isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200" } ` } >
< div className = { ` px-6 py-4 border-b ${ isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50" } ` } >
< h2 className = { ` text-lg font-semibold ${ isDark ? "text-white" : "text-gray-900" } ` } >
Quick Info
< / h2 >
< / div >
< div className = "p-6 space-y-4" >
< div >
< p className = { ` text-xs font-medium mb-1 uppercase tracking-wider ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
Created
< / p >
< p className = { ` text-sm font-medium ${ isDark ? "text-white" : "text-gray-900" } ` } >
{ formatShortDate ( appointment . created_at ) }
< / p >
< p className = { ` text-xs mt-0.5 ${ isDark ? "text-gray-500" : "text-gray-500" } ` } >
{ formatTime ( appointment . created_at ) }
< / p >
< / div >
< div className = "pt-4 border-t border-gray-200 dark:border-gray-700" >
< p className = { ` text-xs font-medium mb-1 uppercase tracking-wider ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
Status
< / p >
< span
className = { ` inline-flex items-center gap-2 px-3 py-1.5 text-sm font-semibold rounded-lg border ${ getStatusColor (
appointment . status
) } ` }
>
{ appointment . status === "scheduled" && < CheckCircle2 className = "w-4 h-4" / > }
{ formatStatus ( appointment . status ) }
< / span >
< / div >
2025-12-04 19:19:23 +00:00
{ appointment . scheduled_datetime && (
< div className = "pt-4 border-t border-gray-200 dark:border-gray-700" >
< p className = { ` text-xs font-medium mb-2 uppercase tracking-wider ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
Meeting Information
< / p >
< div className = "space-y-3" >
< div >
< p className = { ` text-xs font-medium mb-1 ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
Meeting Start Time :
< / p >
< p className = { ` text-sm font-medium ${ isDark ? "text-white" : "text-gray-900" } ` } >
{ formatDate ( appointment . scheduled_datetime ) } at { formatTime ( appointment . scheduled_datetime ) }
< / p >
< / div >
< div >
< p className = { ` text-xs font-medium mb-1 ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
How to Access :
< / p >
< p className = { ` text-sm ${ isDark ? "text-gray-300" : "text-gray-700" } ` } >
2025-12-05 18:03:20 +00:00
Up to 10 minutes before the meeting is scheduled to begin , click the "Start Meeting" button below to begin the session . Once started , participants can join using the meeting link . You can also use the { " " }
{ appointment . moderator_join_url ? (
< a
href = { appointment . moderator_join_url }
target = "_blank"
rel = "noopener noreferrer"
className = { ` font-medium underline hover:opacity-80 ${ isDark ? "text-blue-400" : "text-blue-600" } ` }
>
moderator link
< / a >
) : (
"moderator link"
) } { " " }
to join directly .
2025-12-04 19:19:23 +00:00
< / p >
< / div >
< / div >
< / div >
) }
2025-11-24 16:04:39 +00:00
< / div >
< / div >
{ /* Action Buttons */ }
{ appointment . status === "pending_review" && (
< div className = { ` rounded-2xl border shadow-sm overflow-hidden ${ isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200" } ` } >
< div className = "p-6 space-y-3" >
< Button
onClick = { ( ) = > setScheduleDialogOpen ( true ) }
className = "w-full bg-blue-600 hover:bg-blue-700 text-white h-12 text-base font-medium"
>
< CalendarCheck className = "w-5 h-5 mr-2" / >
Schedule Appointment
< / Button >
< Button
onClick = { ( ) = > setRejectDialogOpen ( true ) }
variant = "outline"
className = { ` w-full h-12 text-base font-medium border-red-600 text-red-600 hover:bg-red-50 ${ isDark ? "hover:bg-red-900/20" : "" } ` }
>
< X className = "w-5 h-5 mr-2" / >
Reject Request
< / Button >
< / div >
< / div >
) }
2025-12-05 18:35:20 +00:00
{ /* Cancel Appointment Button */ }
{ appointment . status === "scheduled" && (
< div className = { ` rounded-2xl border shadow-sm overflow-hidden ${ isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200" } ` } >
< div className = "p-6" >
< Button
onClick = { ( ) = > setCancelDialogOpen ( true ) }
variant = "outline"
className = { ` w-full h-12 text-sm sm:text-base font-medium border-red-600 text-red-600 hover:bg-red-50 ${ isDark ? "hover:bg-red-900/20 border-red-500" : "" } ` }
>
< X className = "w-4 h-4 sm:w-5 sm:h-5 mr-2 shrink-0" / >
< span className = "text-center" > Cancel Appointment < / span >
< / Button >
< / div >
< / div >
) }
2025-12-04 15:36:14 +00:00
{ /* Meeting Button (if scheduled) */ }
2025-12-03 18:39:30 +00:00
{ appointment . status === "scheduled" && appointment . moderator_join_url && (
2025-11-24 16:04:39 +00:00
< div className = { ` rounded-2xl border shadow-sm overflow-hidden ${ isDark ? "bg-gradient-to-br from-blue-900/20 to-purple-900/20 border-blue-800/30" : "bg-gradient-to-br from-blue-50 to-purple-50 border-blue-200" } ` } >
2025-12-04 15:36:14 +00:00
< div className = "p-6 space-y-3" >
{ ( ( ) = > {
2025-12-05 13:03:58 +00:00
// Check if meeting has ended
const endedAt = appointment . meeting_ended_at ;
const hasEnded = endedAt != null && endedAt !== "" ;
// If meeting has ended, show "Meeting has ended"
if ( hasEnded ) {
return (
< button
disabled
2025-12-05 18:35:20 +00:00
className = { ` flex items-center justify-center gap-2 w-full cursor-not-allowed h-12 rounded-lg text-sm sm:text-base font-medium transition-colors ${ isDark ? "bg-gray-700 text-gray-500" : "bg-gray-300 text-gray-500" } ` }
2025-12-05 13:03:58 +00:00
>
2025-12-05 18:35:20 +00:00
< Video className = "w-4 h-4 sm:w-5 sm:h-5 shrink-0" / >
< span className = "text-center" > Meeting has ended < / span >
2025-12-05 13:03:58 +00:00
< / button >
) ;
}
2025-12-04 15:56:53 +00:00
// Check if can join as moderator (handle both boolean and string values)
const canJoinAsModerator = appointment . can_join_as_moderator === true || appointment . can_join_as_moderator === "true" ;
// Check if meeting has started (handle both field names)
2025-12-04 15:36:14 +00:00
const startedAt = appointment . started_at || appointment . meeting_started_at ;
const hasStarted = startedAt != null && startedAt !== "" ;
2025-12-04 18:07:27 +00:00
// If can_join_as_moderator != true, display "Meeting would be available shortly"
2025-12-04 15:56:53 +00:00
if ( ! canJoinAsModerator ) {
2025-12-05 18:00:54 +00:00
const meetingTime = appointment . scheduled_datetime
? formatTime ( appointment . scheduled_datetime )
: "the scheduled time" ;
2025-12-04 15:36:14 +00:00
return (
< button
disabled
2025-12-05 18:19:19 +00:00
className = { ` flex items-center justify-center gap-2 w-full cursor-not-allowed h-12 rounded-lg text-sm sm:text-base font-medium transition-colors ${ isDark ? "bg-gray-700 text-gray-500" : "bg-gray-300 text-gray-500" } ` }
2025-12-04 15:36:14 +00:00
>
2025-12-05 18:19:19 +00:00
< Video className = "w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0" / >
< span className = "text-center px-1" >
< span className = "hidden sm:inline" > Meeting would be available to join starting at < / span >
< span className = "sm:hidden" > Available at < / span >
< span className = "font-semibold" > { meetingTime } < / span >
< / span >
2025-12-04 15:36:14 +00:00
< / button >
) ;
}
2025-12-04 15:56:53 +00:00
// If can_join_as_moderator == true && started_at != null, show "Join Now" button
2025-12-04 15:36:14 +00:00
if ( hasStarted ) {
return (
< >
< a
href = { appointment . moderator_join_url }
target = "_blank"
rel = "noopener noreferrer"
2025-12-05 18:35:20 +00:00
className = { ` flex items-center justify-center gap-2 w-full bg-blue-600 hover:bg-blue-700 text-white h-12 rounded-lg text-sm sm:text-base font-medium transition-colors ` }
2025-12-04 15:36:14 +00:00
>
2025-12-05 18:35:20 +00:00
< Video className = "w-4 h-4 sm:w-5 sm:h-5 shrink-0" / >
< span className = "text-center" > Join Now < / span >
2025-12-04 15:36:14 +00:00
< / a >
< Button
onClick = { handleEndMeeting }
disabled = { isEndingMeeting }
variant = "outline"
2025-12-05 18:35:20 +00:00
className = { ` w-full h-12 text-sm sm:text-base font-medium border-red-600 text-red-600 hover:bg-red-50 ${ isDark ? "hover:bg-red-900/20" : "" } ` }
2025-12-04 15:36:14 +00:00
>
{ isEndingMeeting ? (
< >
2025-12-05 18:35:20 +00:00
< Loader2 className = "w-4 h-4 sm:w-5 sm:h-5 mr-2 animate-spin shrink-0" / >
< span className = "text-center" > Ending . . . < / span >
2025-12-04 15:36:14 +00:00
< / >
) : (
< >
2025-12-05 18:35:20 +00:00
< X className = "w-4 h-4 sm:w-5 sm:h-5 mr-2 shrink-0" / >
< span className = "text-center" > End Meeting < / span >
2025-12-04 15:36:14 +00:00
< / >
) }
< / Button >
< / >
) ;
}
2025-12-04 15:56:53 +00:00
// If can_join_as_moderator == true && started_at == null, show "Start Meeting" button
2025-12-04 15:36:14 +00:00
return (
< Button
onClick = { handleStartMeeting }
disabled = { isStartingMeeting }
2025-12-05 18:35:20 +00:00
className = "w-full bg-green-600 hover:bg-green-700 text-white h-12 text-sm sm:text-base font-medium"
2025-12-04 15:36:14 +00:00
>
{ isStartingMeeting ? (
< >
2025-12-05 18:35:20 +00:00
< Loader2 className = "w-4 h-4 sm:w-5 sm:h-5 mr-2 animate-spin shrink-0" / >
< span className = "text-center" > Starting . . . < / span >
2025-12-04 15:36:14 +00:00
< / >
) : (
< >
2025-12-05 18:35:20 +00:00
< Video className = "w-4 h-4 sm:w-5 sm:h-5 mr-2 shrink-0" / >
< span className = "text-center" > Start Meeting < / span >
2025-12-04 15:36:14 +00:00
< / >
) }
< / Button >
) ;
} ) ( ) }
2025-11-24 16:04:39 +00:00
< / div >
< / div >
) }
< / div >
2025-11-27 19:18:59 +00:00
< / div >
< / main >
2025-11-24 16:04:39 +00:00
2025-11-27 19:18:59 +00:00
{ /* Schedule Appointment Dialog */ }
< ScheduleAppointmentDialog
open = { scheduleDialogOpen }
onOpenChange = { setScheduleDialogOpen }
appointment = { appointment }
scheduledDate = { scheduledDate }
setScheduledDate = { setScheduledDate }
scheduledTime = { scheduledTime }
setScheduledTime = { setScheduledTime }
scheduledDuration = { scheduledDuration }
setScheduledDuration = { setScheduledDuration }
onSchedule = { handleSchedule }
isScheduling = { isScheduling }
isDark = { isDark }
/ >
2025-11-24 16:04:39 +00:00
2025-12-05 18:35:20 +00:00
{ /* Reschedule Appointment Dialog */ }
< ScheduleAppointmentDialog
open = { rescheduleDialogOpen }
onOpenChange = { setRescheduleDialogOpen }
appointment = { appointment }
scheduledDate = { rescheduleDate }
setScheduledDate = { setRescheduleDate }
scheduledTime = { rescheduleTime }
setScheduledTime = { setRescheduleTime }
scheduledDuration = { rescheduleDuration }
setScheduledDuration = { setRescheduleDuration }
onSchedule = { handleReschedule }
isScheduling = { isRescheduling }
isDark = { isDark }
title = "Reschedule Appointment"
description = { appointment ? ` Change the date and time for ${ appointment . first_name } ${ appointment . last_name } 's appointment ` : "Change the date and time for this appointment" }
/ >
2025-11-24 16:04:39 +00:00
{ /* Reject Appointment Dialog */ }
< Dialog open = { rejectDialogOpen } onOpenChange = { setRejectDialogOpen } >
< DialogContent className = { ` max-w-2xl ${ isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200" } ` } >
< DialogHeader >
< DialogTitle className = { ` text-2xl font-semibold ${ isDark ? "text-white" : "text-gray-900" } ` } >
Reject Appointment Request
< / DialogTitle >
< DialogDescription className = { ` text-base ${ isDark ? "text-gray-400" : "text-gray-500" } ` } >
Reject appointment request from { appointment . first_name } { appointment . last_name }
< / DialogDescription >
< / DialogHeader >
< div className = "space-y-4 py-4" >
< div className = "space-y-2" >
< label className = { ` text-sm font-semibold ${ isDark ? "text-gray-300" : "text-gray-700" } ` } >
Rejection Reason ( Optional )
< / label >
< textarea
value = { rejectionReason }
onChange = { ( e ) = > setRejectionReason ( e . target . value ) }
placeholder = "Enter reason for rejection..."
rows = { 5 }
className = { ` w-full rounded-xl border px-4 py-3 text-base ${
isDark
? "bg-gray-700 border-gray-600 text-white placeholder:text-gray-400"
: "bg-white border-gray-300 text-gray-900 placeholder:text-gray-500"
} ` }
/ >
< / div >
< / div >
< DialogFooter className = "gap-3 pt-4" >
< Button
variant = "outline"
onClick = { ( ) = > setRejectDialogOpen ( false ) }
disabled = { isRejecting }
className = { ` h-12 px-6 ${ isDark ? "border-gray-700 text-gray-300 hover:bg-gray-700" : "" } ` }
>
Cancel
< / Button >
< Button
onClick = { handleReject }
disabled = { isRejecting }
className = "h-12 px-6 bg-red-600 hover:bg-red-700 text-white"
>
{ isRejecting ? (
< >
< Loader2 className = "w-5 h-5 mr-2 animate-spin" / >
Rejecting . . .
< / >
) : (
< >
< X className = "w-5 h-5 mr-2" / >
Reject Appointment
< / >
) }
< / Button >
< / DialogFooter >
< / DialogContent >
< / Dialog >
2025-12-05 18:35:20 +00:00
{ /* Cancel Appointment Confirmation Dialog */ }
< Dialog open = { cancelDialogOpen } onOpenChange = { setCancelDialogOpen } >
< DialogContent className = { isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200" } >
< DialogHeader >
< DialogTitle className = { isDark ? "text-white" : "text-gray-900" } >
Cancel Appointment
< / DialogTitle >
< DialogDescription className = { isDark ? "text-gray-400" : "text-gray-500" } >
Are you sure you want to cancel this appointment ? This action cannot be undone .
< / DialogDescription >
< / DialogHeader >
< DialogFooter >
< Button
variant = "outline"
onClick = { ( ) = > setCancelDialogOpen ( false ) }
disabled = { isCancelling }
className = { isDark ? "border-gray-600 text-gray-300 hover:bg-gray-700" : "" }
>
No , Keep Appointment
< / Button >
< Button
onClick = { handleCancelAppointment }
disabled = { isCancelling }
className = "bg-red-600 hover:bg-red-700 text-white"
>
{ isCancelling ? (
< >
< Loader2 className = "w-4 h-4 mr-2 animate-spin" / >
Cancelling . . .
< / >
) : (
< >
< X className = "w-4 h-4 mr-2" / >
Yes , Cancel Appointment
< / >
) }
< / Button >
< / DialogFooter >
< / DialogContent >
< / Dialog >
2025-11-24 16:04:39 +00:00
< / div >
) ;
}