diff --git a/app/(admin)/admin/booking/[id]/page.tsx b/app/(admin)/admin/booking/[id]/page.tsx index 36b531d..03c1075 100644 --- a/app/(admin)/admin/booking/[id]/page.tsx +++ b/app/(admin)/admin/booking/[id]/page.tsx @@ -20,7 +20,7 @@ import { MapPin, } from "lucide-react"; import { useAppTheme } from "@/components/ThemeProvider"; -import { getAppointmentDetail, scheduleAppointment, rejectAppointment, listAppointments } from "@/lib/actions/appointments"; +import { getAppointmentDetail, scheduleAppointment, rejectAppointment, listAppointments, startMeeting, endMeeting } from "@/lib/actions/appointments"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -49,6 +49,8 @@ export default function AppointmentDetailPage() { const [rejectionReason, setRejectionReason] = useState(""); 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"; @@ -207,6 +209,36 @@ export default function AppointmentDetailPage() { 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 (
@@ -658,29 +690,81 @@ export default function AppointmentDetailPage() {
)} - {/* Join Meeting Button (if scheduled) */} + {/* Meeting Button (if scheduled) */} {appointment.status === "scheduled" && appointment.moderator_join_url && (
-
- {appointment.can_join_as_moderator ? ( - - - ) : ( - - )} +
+ {(() => { + 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 ( + + ); + } + + if (hasStarted) { + return ( + <> + + + + + ); + } + + return ( + + ); + })()}
)} diff --git a/lib/actions/appointments.ts b/lib/actions/appointments.ts index 7266b4f..7c3c5e6 100644 --- a/lib/actions/appointments.ts +++ b/lib/actions/appointments.ts @@ -737,3 +737,47 @@ export async function getJitsiMeetingInfo(id: string): Promise return data; } + +export async function startMeeting(id: string): Promise { + const tokens = getStoredTokens(); + if (!tokens.access) { + throw new Error("Authentication required."); + } + + const response = await fetch(API_ENDPOINTS.meetings.startMeeting(id), { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${tokens.access}`, + }, + }); + + const data = await parseResponse(response); + if (!response.ok) { + throw new Error(extractErrorMessage(data as unknown as ApiError)); + } + + return (data as AppointmentResponse).appointment || data; +} + +export async function endMeeting(id: string): Promise { + const tokens = getStoredTokens(); + if (!tokens.access) { + throw new Error("Authentication required."); + } + + const response = await fetch(API_ENDPOINTS.meetings.endMeeting(id), { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${tokens.access}`, + }, + }); + + const data = await parseResponse(response); + if (!response.ok) { + throw new Error(extractErrorMessage(data as unknown as ApiError)); + } + + return (data as AppointmentResponse).appointment || data; +} diff --git a/lib/api_urls.ts b/lib/api_urls.ts index ea17857..aa36240 100644 --- a/lib/api_urls.ts +++ b/lib/api_urls.ts @@ -29,6 +29,8 @@ export const API_ENDPOINTS = { availabilityConfig: `${API_BASE_URL}/meetings/availability/config/`, checkDateAvailability: `${API_BASE_URL}/meetings/availability/check/`, availabilityOverview: `${API_BASE_URL}/meetings/availability/overview/`, + startMeeting: (id: string) => `${API_BASE_URL}/meetings/appointments/${id}/start/`, + endMeeting: (id: string) => `${API_BASE_URL}/meetings/appointments/${id}/end/`, }, } as const; diff --git a/lib/models/appointments.ts b/lib/models/appointments.ts index 2a8b1bf..43830d7 100644 --- a/lib/models/appointments.ts +++ b/lib/models/appointments.ts @@ -20,6 +20,7 @@ export interface Appointment { jitsi_room_id?: string; jitsi_meeting_created?: boolean; meeting_started_at?: string; + started_at?: string; // Alternative field name from API meeting_ended_at?: string; meeting_duration_actual?: number; meeting_info?: any;