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.
This commit is contained in:
parent
97b04e7593
commit
74d7a35e60
@ -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<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";
|
||||
|
||||
@ -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 (
|
||||
<div className={`min-h-[calc(100vh-4rem)] flex items-center justify-center ${isDark ? "bg-gray-900" : "bg-gray-50"}`}>
|
||||
@ -658,29 +690,81 @@ export default function AppointmentDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Join Meeting Button (if scheduled) */}
|
||||
{/* 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">
|
||||
{appointment.can_join_as_moderator ? (
|
||||
<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 Meeting as Moderator
|
||||
</a>
|
||||
) : (
|
||||
<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 Yet
|
||||
</button>
|
||||
)}
|
||||
<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>
|
||||
)}
|
||||
|
||||
@ -737,3 +737,47 @@ export async function getJitsiMeetingInfo(id: string): Promise<JitsiMeetingInfo>
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function startMeeting(id: string): Promise<Appointment> {
|
||||
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<Appointment> {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user