diff --git a/app/(admin)/admin/booking/[id]/page.tsx b/app/(admin)/admin/booking/[id]/page.tsx index 46a96ce..883faaf 100644 --- a/app/(admin)/admin/booking/[id]/page.tsx +++ b/app/(admin)/admin/booking/[id]/page.tsx @@ -18,9 +18,10 @@ import { ExternalLink, Copy, MapPin, + Pencil, } from "lucide-react"; import { useAppTheme } from "@/components/ThemeProvider"; -import { getAppointmentDetail, scheduleAppointment, rejectAppointment, listAppointments, startMeeting, endMeeting } from "@/lib/actions/appointments"; +import { getAppointmentDetail, scheduleAppointment, rejectAppointment, listAppointments, startMeeting, endMeeting, rescheduleAppointment } from "@/lib/actions/appointments"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -43,12 +44,17 @@ export default function AppointmentDetailPage() { const [loading, setLoading] = useState(true); const [scheduleDialogOpen, setScheduleDialogOpen] = useState(false); const [rejectDialogOpen, setRejectDialogOpen] = useState(false); + const [rescheduleDialogOpen, setRescheduleDialogOpen] = useState(false); const [scheduledDate, setScheduledDate] = useState(undefined); const [scheduledTime, setScheduledTime] = useState("09:00"); const [scheduledDuration, setScheduledDuration] = useState(60); + const [rescheduleDate, setRescheduleDate] = useState(undefined); + const [rescheduleTime, setRescheduleTime] = useState("09:00"); + const [rescheduleDuration, setRescheduleDuration] = useState(60); const [rejectionReason, setRejectionReason] = useState(""); const [isScheduling, setIsScheduling] = useState(false); const [isRejecting, setIsRejecting] = useState(false); + const [isRescheduling, setIsRescheduling] = useState(false); const [isStartingMeeting, setIsStartingMeeting] = useState(false); const [isEndingMeeting, setIsEndingMeeting] = useState(false); const { theme } = useAppTheme(); @@ -745,10 +751,14 @@ export default function AppointmentDetailPage() { return ( ); } diff --git a/app/(user)/user/appointments/[id]/page.tsx b/app/(user)/user/appointments/[id]/page.tsx index 45c8782..11a716c 100644 --- a/app/(user)/user/appointments/[id]/page.tsx +++ b/app/(user)/user/appointments/[id]/page.tsx @@ -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(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 (
@@ -610,9 +641,66 @@ export default function UserAppointmentDetailPage() {
)} + + {/* Cancel Appointment Button */} + {appointment.status === "scheduled" && ( +
+
+ +
+
+ )} + + {/* Cancel Appointment Confirmation Dialog */} + + + + + Cancel Appointment + + + Are you sure you want to cancel this appointment? This action cannot be undone. + + + + + + + + ); } diff --git a/lib/actions/appointments.ts b/lib/actions/appointments.ts index 2721651..f00c0c5 100644 --- a/lib/actions/appointments.ts +++ b/lib/actions/appointments.ts @@ -801,3 +801,76 @@ export async function endMeeting(id: string): Promise { // So we need to refetch the appointment to get the updated state return await getAppointmentDetail(id); } + +export interface RescheduleAppointmentInput { + new_scheduled_datetime: string; // ISO datetime string + new_scheduled_duration: number; // in minutes + timezone: string; +} + +export async function rescheduleAppointment(id: string, input: RescheduleAppointmentInput): Promise { + const tokens = getStoredTokens(); + if (!tokens.access) { + throw new Error("Authentication required."); + } + + const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // Get user's timezone + + const payload: any = { + new_scheduled_datetime: input.new_scheduled_datetime, + new_scheduled_duration: input.new_scheduled_duration, + timezone: input.timezone !== undefined ? input.timezone : userTimezone, + }; + + const response = await fetch(`${API_ENDPOINTS.meetings.listAppointments}${id}/reschedule/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${tokens.access}`, + }, + body: JSON.stringify(payload), + }); + + const data = await parseResponse(response); + if (!response.ok) { + throw new Error(extractErrorMessage(data as unknown as ApiError)); + } + + // Refetch the appointment to get the updated state + return await getAppointmentDetail(id); +} + +export interface CancelAppointmentInput { + action?: string; + metadata?: string; + recording_url?: string; +} + +export async function cancelAppointment(id: string, input?: CancelAppointmentInput): Promise { + const tokens = getStoredTokens(); + if (!tokens.access) { + throw new Error("Authentication required."); + } + + const payload: any = {}; + if (input?.action) payload.action = input.action; + if (input?.metadata) payload.metadata = input.metadata; + if (input?.recording_url) payload.recording_url = input.recording_url; + + const response = await fetch(`${API_ENDPOINTS.meetings.base}meetings/${id}/cancel/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${tokens.access}`, + }, + body: JSON.stringify(payload), + }); + + const data = await parseResponse(response); + if (!response.ok) { + throw new Error(extractErrorMessage(data as unknown as ApiError)); + } + + // Refetch the appointment to get the updated state + return await getAppointmentDetail(id); +}