Compare commits
No commits in common. "0adac50dc03bd834bb0c681f2b9b6534eaefb0f3" and "d47b542413729e3d86296b4f5cb36e571dc808e4" have entirely different histories.
0adac50dc0
...
d47b542413
@ -21,7 +21,7 @@ import {
|
||||
Pencil,
|
||||
} from "lucide-react";
|
||||
import { useAppTheme } from "@/components/ThemeProvider";
|
||||
import { getAppointmentDetail, scheduleAppointment, rejectAppointment, listAppointments, startMeeting, endMeeting, rescheduleAppointment, cancelAppointment } from "@/lib/actions/appointments";
|
||||
import { getAppointmentDetail, scheduleAppointment, rejectAppointment, listAppointments, startMeeting, endMeeting, rescheduleAppointment } from "@/lib/actions/appointments";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@ -57,8 +57,6 @@ export default function AppointmentDetailPage() {
|
||||
const [isRescheduling, setIsRescheduling] = useState(false);
|
||||
const [isStartingMeeting, setIsStartingMeeting] = useState(false);
|
||||
const [isEndingMeeting, setIsEndingMeeting] = useState(false);
|
||||
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
const { theme } = useAppTheme();
|
||||
const isDark = theme === "dark";
|
||||
|
||||
@ -212,56 +210,6 @@ export default function AppointmentDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string, label: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
toast.success(`${label} copied to clipboard`);
|
||||
@ -436,34 +384,10 @@ export default function AppointmentDetailPage() {
|
||||
{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"}`}>
|
||||
<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>
|
||||
<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">
|
||||
@ -791,22 +715,6 @@ export default function AppointmentDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* 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"}`}>
|
||||
@ -821,10 +729,10 @@ export default function AppointmentDetailPage() {
|
||||
return (
|
||||
<button
|
||||
disabled
|
||||
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"}`}
|
||||
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-4 h-4 sm:w-5 sm:h-5 shrink-0" />
|
||||
<span className="text-center">Meeting has ended</span>
|
||||
<Video className="w-5 h-5" />
|
||||
Meeting has ended
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@ -863,26 +771,26 @@ export default function AppointmentDetailPage() {
|
||||
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-sm sm:text-base font-medium transition-colors`}
|
||||
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-4 h-4 sm:w-5 sm:h-5 shrink-0" />
|
||||
<span className="text-center">Join Now</span>
|
||||
<Video className="w-5 h-5" />
|
||||
Join Now
|
||||
</a>
|
||||
<Button
|
||||
onClick={handleEndMeeting}
|
||||
disabled={isEndingMeeting}
|
||||
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" : ""}`}
|
||||
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-4 h-4 sm:w-5 sm:h-5 mr-2 animate-spin shrink-0" />
|
||||
<span className="text-center">Ending...</span>
|
||||
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
|
||||
Ending...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X className="w-4 h-4 sm:w-5 sm:h-5 mr-2 shrink-0" />
|
||||
<span className="text-center">End Meeting</span>
|
||||
<X className="w-5 h-5 mr-2" />
|
||||
End Meeting
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@ -895,17 +803,17 @@ export default function AppointmentDetailPage() {
|
||||
<Button
|
||||
onClick={handleStartMeeting}
|
||||
disabled={isStartingMeeting}
|
||||
className="w-full bg-green-600 hover:bg-green-700 text-white h-12 text-sm sm:text-base font-medium"
|
||||
className="w-full bg-green-600 hover:bg-green-700 text-white h-12 text-base font-medium"
|
||||
>
|
||||
{isStartingMeeting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 sm:w-5 sm:h-5 mr-2 animate-spin shrink-0" />
|
||||
<span className="text-center">Starting...</span>
|
||||
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
|
||||
Starting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Video className="w-4 h-4 sm:w-5 sm:h-5 mr-2 shrink-0" />
|
||||
<span className="text-center">Start Meeting</span>
|
||||
<Video className="w-5 h-5 mr-2" />
|
||||
Start Meeting
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@ -934,24 +842,6 @@ export default function AppointmentDetailPage() {
|
||||
isDark={isDark}
|
||||
/>
|
||||
|
||||
{/* 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"}
|
||||
/>
|
||||
|
||||
{/* 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"}`}>
|
||||
@ -1012,47 +902,6 @@ export default function AppointmentDetailPage() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -15,10 +15,19 @@ import {
|
||||
MessageSquare,
|
||||
CheckCircle2,
|
||||
Copy,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useAppTheme } from "@/components/ThemeProvider";
|
||||
import { getAppointmentDetail, listAppointments } from "@/lib/actions/appointments";
|
||||
import { getAppointmentDetail, listAppointments, cancelAppointment } from "@/lib/actions/appointments";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
import { toast } from "sonner";
|
||||
import type { Appointment } from "@/lib/models/appointments";
|
||||
@ -30,6 +39,8 @@ export default function UserAppointmentDetailPage() {
|
||||
|
||||
const [appointment, setAppointment] = useState<Appointment | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
const { theme } = useAppTheme();
|
||||
const isDark = theme === "dark";
|
||||
|
||||
@ -138,6 +149,26 @@ export default function UserAppointmentDetailPage() {
|
||||
toast.success(`${label} copied to clipboard`);
|
||||
};
|
||||
|
||||
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(appointmentId);
|
||||
setAppointment(updatedAppointment);
|
||||
router.push("/user/dashboard");
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to cancel appointment";
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsCancelling(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={`min-h-screen ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
|
||||
@ -611,9 +642,65 @@ export default function UserAppointmentDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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-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-5 h-5 mr-2" />
|
||||
Cancel Appointment
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -29,8 +29,6 @@ interface ScheduleAppointmentDialogProps {
|
||||
onSchedule: () => Promise<void>;
|
||||
isScheduling: boolean;
|
||||
isDark?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function ScheduleAppointmentDialog({
|
||||
@ -46,8 +44,6 @@ export function ScheduleAppointmentDialog({
|
||||
onSchedule,
|
||||
isScheduling,
|
||||
isDark = false,
|
||||
title,
|
||||
description,
|
||||
}: ScheduleAppointmentDialogProps) {
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString("en-US", {
|
||||
@ -75,12 +71,12 @@ export function ScheduleAppointmentDialog({
|
||||
<DialogContent className={`max-w-4xl max-h-[90vh] overflow-y-auto ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
||||
<DialogHeader className="pb-4">
|
||||
<DialogTitle className={`text-2xl font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||
{title || "Schedule Appointment"}
|
||||
Schedule Appointment
|
||||
</DialogTitle>
|
||||
<DialogDescription className={`text-base ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
||||
{description || (appointment
|
||||
{appointment
|
||||
? `Set date and time for ${appointment.first_name} ${appointment.last_name}'s appointment`
|
||||
: "Set date and time for this appointment")}
|
||||
: "Set date and time for this appointment"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user