website/app/(admin)/admin/booking/[id]/page.tsx
iamkiddy 74d7a35e60 Add meeting management functionality to appointment detail page
Implement start and end meeting features in the appointment detail component. Introduce new API endpoints for starting and ending meetings, and update the appointment model to include meeting status fields. Enhance UI to provide buttons for starting and ending meetings, improving user interaction and experience.
2025-12-04 15:36:14 +00:00

854 lines
39 KiB
TypeScript

"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,
} from "lucide-react";
import { useAppTheme } from "@/components/ThemeProvider";
import { getAppointmentDetail, scheduleAppointment, rejectAppointment, listAppointments, startMeeting, endMeeting } from "@/lib/actions/appointments";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ScheduleAppointmentDialog } from "@/components/ScheduleAppointmentDialog";
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);
const [scheduledDate, setScheduledDate] = useState<Date | undefined>(undefined);
const [scheduledTime, setScheduledTime] = useState<string>("09:00");
const [scheduledDuration, setScheduledDuration] = useState<number>(60);
const [rejectionReason, setRejectionReason] = useState<string>("");
const [isScheduling, setIsScheduling] = useState(false);
const [isRejecting, setIsRejecting] = useState(false);
const [isStartingMeeting, setIsStartingMeeting] = useState(false);
const [isEndingMeeting, setIsEndingMeeting] = useState(false);
const { theme } = useAppTheme();
const isDark = theme === "dark";
useEffect(() => {
const fetchAppointment = async () => {
if (!appointmentId) return;
setLoading(true);
try {
// 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);
} 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);
}
};
const copyToClipboard = (text: string, label: string) => {
navigator.clipboard.writeText(text);
toast.success(`${label} copied to clipboard`);
};
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);
}
};
if (loading) {
return (
<div className={`min-h-[calc(100vh-4rem)] flex items-center justify-center ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
<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 (
<div className={`min-h-[calc(100vh-4rem)] flex items-center justify-center ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
<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"}`}>
<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">
<Button
variant="ghost"
onClick={() => router.push("/admin/booking")}
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"}`}
>
<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">
<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"}`}>
{appointment.first_name[0]}{appointment.last_name[0]}
</div>
<div>
<h1 className={`text-2xl sm:text-3xl lg:text-4xl font-bold ${isDark ? "text-white" : "text-gray-900"}`}>
{appointment.first_name} {appointment.last_name}
</h1>
<p className={`text-xs sm:text-sm mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
Appointment Request
</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<span
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(
appointment.status
)}`}
>
{appointment.status === "scheduled" && <CheckCircle2 className="w-3 h-3 sm:w-4 sm:h-4" />}
{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"}`}>
<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>
</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>
)}
{/* Selected Slots */}
{appointment.selected_slots && Array.isArray(appointment.selected_slots) && appointment.selected_slots.length > 0 && (
<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"}`}>
<CalendarCheck className={`w-5 h-5 ${isDark ? "text-green-400" : "text-green-600"}`} />
Selected Time Slots
{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>
)}
</h2>
</div>
<div className="p-6">
{(() => {
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",
};
// Time slot order: morning, afternoon (lunchtime), evening
const timeSlotOrder: Record<string, number> = {
morning: 0,
afternoon: 1,
evening: 2,
};
// 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);
});
// Sort dates and slots within each date
const sortedDates = Object.keys(slotsByDate).sort((a, b) => {
return new Date(a).getTime() - new Date(b).getTime();
});
return (
<div className="space-y-4">
{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)}
</p>
{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>
</div>
);
})}
</div>
);
})()}
</div>
</div>
)}
{/* 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 */}
{appointment.moderator_join_url && (
<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
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"}
>
<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"}`}>
Moderator Meeting Link
</p>
<div className="flex items-center gap-2">
{appointment.can_join_as_moderator ? (
<>
<a
href={appointment.moderator_join_url}
target="_blank"
rel="noopener noreferrer"
className={`flex-1 text-sm px-3 py-2 rounded-lg truncate ${isDark ? "bg-gray-800 text-blue-400 hover:bg-gray-700" : "bg-white text-blue-600 hover:bg-gray-50 border border-gray-200"}`}
>
{appointment.moderator_join_url}
</a>
<a
href={appointment.moderator_join_url}
target="_blank"
rel="noopener noreferrer"
className={`px-4 py-2 rounded-lg font-medium transition-colors ${isDark ? "bg-blue-600 hover:bg-blue-700 text-white" : "bg-blue-600 hover:bg-blue-700 text-white"}`}
>
<ExternalLink className="w-4 h-4" />
</a>
</>
) : (
<>
<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"}`}>
{appointment.moderator_join_url}
</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"}`}
>
<ExternalLink className="w-4 h-4" />
</button>
</>
)}
</div>
</div>
{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")}`}>
{appointment.can_join_as_moderator ? "Meeting is active - You can join as moderator" : "Meeting is not available yet"}
</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>
</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>
)}
{/* Meeting Button (if scheduled) */}
{appointment.status === "scheduled" && appointment.moderator_join_url && (
<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="p-6 space-y-3">
{(() => {
const canJoin = appointment.can_join_as_moderator === true || appointment.can_join_as_moderator === "true";
const startedAt = appointment.started_at || appointment.meeting_started_at;
const hasStarted = startedAt != null && startedAt !== "";
if (!canJoin) {
return (
<button
disabled
className={`flex items-center justify-center gap-2 w-full cursor-not-allowed h-12 rounded-lg text-base font-medium transition-colors ${isDark ? "bg-gray-700 text-gray-500" : "bg-gray-300 text-gray-500"}`}
>
<Video className="w-5 h-5" />
Meeting Not Available
</button>
);
}
if (hasStarted) {
return (
<>
<a
href={appointment.moderator_join_url}
target="_blank"
rel="noopener noreferrer"
className={`flex items-center justify-center gap-2 w-full bg-blue-600 hover:bg-blue-700 text-white h-12 rounded-lg text-base font-medium transition-colors`}
>
<Video className="w-5 h-5" />
Join Now
</a>
<Button
onClick={handleEndMeeting}
disabled={isEndingMeeting}
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" : ""}`}
>
{isEndingMeeting ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Ending...
</>
) : (
<>
<X className="w-5 h-5 mr-2" />
End Meeting
</>
)}
</Button>
</>
);
}
return (
<Button
onClick={handleStartMeeting}
disabled={isStartingMeeting}
className="w-full bg-green-600 hover:bg-green-700 text-white h-12 text-base font-medium"
>
{isStartingMeeting ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Starting...
</>
) : (
<>
<Video className="w-5 h-5 mr-2" />
Start Meeting
</>
)}
</Button>
);
})()}
</div>
</div>
)}
</div>
</div>
</main>
{/* 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}
/>
{/* 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>
</div>
);
}