From 74d7a35e60d06ef93b4513ce96bb693c7a90e880 Mon Sep 17 00:00:00 2001 From: iamkiddy Date: Thu, 4 Dec 2025 15:36:14 +0000 Subject: [PATCH] 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. --- app/(admin)/admin/booking/[id]/page.tsx | 128 ++++++++++++++++++++---- lib/actions/appointments.ts | 44 ++++++++ lib/api_urls.ts | 2 + lib/models/appointments.ts | 1 + 4 files changed, 153 insertions(+), 22 deletions(-) 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; -- 2.39.5