"use client"; import { useState, useEffect } from "react"; import { useSession } from "next-auth/react"; import { redirect } from "next/navigation"; import { ResponsiveIframe } from "@/app/components/responsive-iframe"; import { Users, FolderKanban, Video, ArrowLeft, Loader2, Calendar, Clock, Plus, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useToast } from "@/components/ui/use-toast"; import { Calendar as CalendarComponent } from "@/components/ui/calendar"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Textarea } from "@/components/ui/textarea"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import DatePicker, { registerLocale } from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; import { fr } from "date-fns/locale"; registerLocale('fr', fr); interface Group { id: string; name: string; path: string; } interface Mission { id: string; name: string; logoUrl?: string | null; } interface ScheduledMeeting { id: string; type: "group" | "mission"; entityId: string; entityName: string; date: string; // ISO date string time: string; // HH:mm format title?: string; start: string; // ISO datetime string for start end: string; // ISO datetime string for end isAllDay?: boolean; } type ConferenceType = "group" | "mission" | null; export default function VisionPage() { const { data: session, status } = useSession(); const { toast } = useToast(); const [groups, setGroups] = useState([]); const [missions, setMissions] = useState([]); const [loading, setLoading] = useState(true); const [selectedConference, setSelectedConference] = useState<{ type: ConferenceType; id: string; name: string; } | null>(null); const [jitsiUrl, setJitsiUrl] = useState(null); const [scheduledMeetings, setScheduledMeetings] = useState([]); const [meetingsLoading, setMeetingsLoading] = useState(false); const [showMeetingDialog, setShowMeetingDialog] = useState(false); const [selectedDate, setSelectedDate] = useState(new Date()); const [meetingForm, setMeetingForm] = useState<{ type: "group" | "mission" | ""; entityId: string; entityName: string; title: string; start: string; end: string; allDay: boolean; location: string; description: string; recurrence: "none" | "daily" | "weekly" | "monthly"; }>({ type: "", entityId: "", entityName: "", title: "", start: "", end: "", allDay: false, location: "", description: "", recurrence: "none", }); // Redirect if not authenticated useEffect(() => { if (status === "unauthenticated") { redirect("/signin"); } }, [status]); // Helper function to convert calendars to ScheduledMeeting format const convertCalendarsToMeetings = (calendarsData: any[]): ScheduledMeeting[] => { const groupAndMissionCalendars = calendarsData.filter((cal: any) => { const isGroup = cal.name?.startsWith("Groupe:"); const isMission = cal.name?.startsWith("Mission:"); return isGroup || isMission; }); const meetings: ScheduledMeeting[] = []; groupAndMissionCalendars.forEach((calendar: any) => { const calendarName = calendar.name || ""; const isGroup = calendarName.startsWith("Groupe:"); const isMission = calendarName.startsWith("Mission:"); // Extract entity name from calendar name let entityName = ""; let entityId = ""; if (isGroup) { entityName = calendarName.replace("Groupe: ", ""); // Find matching group by name const matchingGroup = groups.find((g: Group) => g.name === entityName); entityId = matchingGroup?.id || ""; } else if (isMission) { entityName = calendarName.replace("Mission: ", ""); // Find matching mission by name const matchingMission = missions.find((m: Mission) => m.name === entityName); entityId = matchingMission?.id || calendar.missionId || ""; } // Convert events to ScheduledMeeting format (calendar.events || []).forEach((event: any) => { const eventStart = new Date(event.start); const eventEnd = new Date(event.end); const dateStr = eventStart.toISOString().split('T')[0]; const timeStr = event.isAllDay ? "" : eventStart.toTimeString().slice(0, 5); meetings.push({ id: event.id, type: isGroup ? "group" : "mission", entityId: entityId, entityName: entityName, date: dateStr, time: timeStr, title: event.title, start: event.start, end: event.end, isAllDay: event.isAllDay || false, }); }); }); return meetings; }; // Load meetings from database (events from group and mission calendars) useEffect(() => { const fetchMeetings = async () => { if (status !== "authenticated" || !session?.user?.id) { return; } try { setMeetingsLoading(true); // Fetch calendars with events const calendarsResponse = await fetch("/api/calendars"); if (!calendarsResponse.ok) { throw new Error("Impossible de charger les calendriers"); } const calendarsData = await calendarsResponse.json(); const meetings = convertCalendarsToMeetings(calendarsData); setScheduledMeetings(meetings); } catch (error) { console.error("Error loading meetings:", error); toast({ title: "Erreur", description: "Impossible de charger les réunions", variant: "destructive", }); } finally { setMeetingsLoading(false); } }; fetchMeetings(); }, [session, status, groups, missions, toast]); // Fetch user groups and missions useEffect(() => { const fetchData = async () => { if (status !== "authenticated" || !session?.user?.id) { return; } try { setLoading(true); const userId = session.user.id; // Fetch user groups const groupsRes = await fetch(`/api/users/${userId}/groups`); if (groupsRes.ok) { const groupsData = await groupsRes.json(); setGroups(Array.isArray(groupsData) ? groupsData : []); } else { console.error("Failed to fetch groups"); } // Fetch all missions and filter for user's missions const missionsRes = await fetch("/api/missions?limit=1000"); if (missionsRes.ok) { const missionsData = await missionsRes.json(); const allMissions = missionsData.missions || []; // Filter missions where user is creator or member const userMissions = allMissions.filter((mission: any) => { const isCreator = mission.creator?.id === userId; const isMember = mission.missionUsers?.some( (mu: any) => mu.user?.id === userId ); return isCreator || isMember; }); setMissions(userMissions); } else { console.error("Failed to fetch missions"); } } catch (error) { console.error("Error fetching data:", error); toast({ title: "Erreur", description: "Impossible de charger les données", variant: "destructive", }); } finally { setLoading(false); } }; fetchData(); }, [session, status, toast]); // Handle conference selection const handleConferenceClick = (type: ConferenceType, id: string, name: string) => { const baseUrl = process.env.NEXT_PUBLIC_IFRAME_CONFERENCE_URL || 'https://vision.slm-lab.net'; let url: string; if (type === "group") { // URL format: https://vision.slm-lab.net/Groupe-{groupId} url = `${baseUrl}/Groupe-${id}`; } else { // URL format: https://vision.slm-lab.net/{missionId} url = `${baseUrl}/${id}`; } setSelectedConference({ type, id, name }); setJitsiUrl(url); }; // Handle back to list const handleBack = () => { setSelectedConference(null); setJitsiUrl(null); }; // Check if meeting can be joined (5 minutes before start until end) const canJoinMeeting = (meeting: ScheduledMeeting): boolean => { const now = new Date(); const start = new Date(meeting.start); const end = new Date(meeting.end); // Calculate 5 minutes before start const fiveMinutesBeforeStart = new Date(start); fiveMinutesBeforeStart.setMinutes(fiveMinutesBeforeStart.getMinutes() - 5); // Can join if now is between 5 minutes before start and end return now >= fiveMinutesBeforeStart && now <= end; }; // Get today's meetings const getTodayMeetings = (): ScheduledMeeting[] => { const today = new Date(); const todayStr = today.toISOString().split('T')[0]; return scheduledMeetings.filter(meeting => meeting.date === todayStr); }; // Get meetings for a specific date const getMeetingsForDate = (date: Date): ScheduledMeeting[] => { const dateStr = date.toISOString().split('T')[0]; return scheduledMeetings.filter(meeting => meeting.date === dateStr); }; // Helper function to get date from string const getDateFromString = (dateString: string): Date | null => { if (!dateString) return null; try { return new Date(dateString); } catch { return null; } }; // Handle open meeting dialog const handleOpenMeetingDialog = (type: "group" | "mission", id: string, name: string) => { const defaultDate = selectedDate || new Date(); const startDate = new Date(defaultDate); startDate.setHours(new Date().getHours(), 0, 0, 0); const endDate = new Date(startDate); endDate.setHours(startDate.getHours() + 1); setMeetingForm({ type, entityId: id, entityName: name, title: "", start: startDate.toISOString().slice(0, 16), end: endDate.toISOString().slice(0, 16), allDay: false, location: "", description: "", recurrence: "none", }); setShowMeetingDialog(true); }; // Handle save meeting const handleSaveMeeting = async () => { if (!meetingForm.type || !meetingForm.entityId || !meetingForm.start || !meetingForm.end) { toast({ title: "Erreur", description: "Veuillez remplir tous les champs requis", variant: "destructive", }); return; } if (!session?.user?.id) { toast({ title: "Erreur", description: "Vous devez être connecté", variant: "destructive", }); return; } try { // Fetch user calendars to find the matching calendar const fetchCalendarsResponse = await fetch("/api/calendars"); if (!fetchCalendarsResponse.ok) { throw new Error("Impossible de charger les calendriers"); } const calendars = await fetchCalendarsResponse.json(); // Find the calendar for this mission or group let targetCalendar = null; if (meetingForm.type === "mission") { // For missions, look for calendar with missionId or name starting with "Mission:" targetCalendar = calendars.find((cal: any) => cal.missionId === meetingForm.entityId || cal.name === `Mission: ${meetingForm.entityName}` ); } else if (meetingForm.type === "group") { // For groups, look for calendar with name starting with "Groupe:" targetCalendar = calendars.find((cal: any) => cal.name === `Groupe: ${meetingForm.entityName}` ); } if (!targetCalendar) { toast({ title: "Erreur", description: `Calendrier ${meetingForm.type === "group" ? "du groupe" : "de la mission"} introuvable`, variant: "destructive", }); return; } const startDate = new Date(meetingForm.start); const endDate = new Date(meetingForm.end); const timeDiff = endDate.getTime() - startDate.getTime(); // Create events based on recurrence const eventsToCreate: Array<{ start: Date; end: Date }> = []; if (meetingForm.recurrence === "none") { // Single event eventsToCreate.push({ start: startDate, end: endDate }); } else { // Recurring events - create for next 12 occurrences const recurrenceCount = 12; let currentStart = new Date(startDate); for (let i = 0; i < recurrenceCount; i++) { const currentEnd = new Date(currentStart.getTime() + timeDiff); eventsToCreate.push({ start: new Date(currentStart), end: currentEnd }); // Calculate next occurrence if (meetingForm.recurrence === "daily") { currentStart.setDate(currentStart.getDate() + 1); } else if (meetingForm.recurrence === "weekly") { currentStart.setDate(currentStart.getDate() + 7); } else if (meetingForm.recurrence === "monthly") { currentStart.setMonth(currentStart.getMonth() + 1); } } } // Create all events via API const eventPromises = eventsToCreate.map(async ({ start, end }) => { const response = await fetch("/api/events", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ title: meetingForm.title || `Réunion ${meetingForm.type === "group" ? "du groupe" : "de la mission"}: ${meetingForm.entityName}`, description: meetingForm.description || null, start: start.toISOString(), end: end.toISOString(), allDay: meetingForm.allDay, location: meetingForm.location || null, calendarId: targetCalendar.id, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Erreur lors de la création de l'événement"); } return await response.json(); }); await Promise.all(eventPromises); // Refresh meetings from database after creation const refreshCalendarsResponse = await fetch("/api/calendars"); if (refreshCalendarsResponse.ok) { const calendarsData = await refreshCalendarsResponse.json(); const meetings = convertCalendarsToMeetings(calendarsData); setScheduledMeetings(meetings); } setShowMeetingDialog(false); setMeetingForm({ type: "", entityId: "", entityName: "", title: "", start: "", end: "", allDay: false, location: "", description: "", recurrence: "none", }); toast({ title: "Succès", description: `${eventsToCreate.length} réunion${eventsToCreate.length > 1 ? 's' : ''} planifiée${eventsToCreate.length > 1 ? 's' : ''} avec succès`, }); } catch (error) { console.error("Error saving meeting:", error); toast({ title: "Erreur", description: error instanceof Error ? error.message : "Erreur lors de la planification de la réunion", variant: "destructive", }); } }; // Handle delete meeting const handleDeleteMeeting = async (meetingId: string) => { try { const response = await fetch(`/api/events/${meetingId}`, { method: "DELETE", }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Erreur lors de la suppression"); } // Refresh meetings from database after deletion // Use refresh=true to bypass cache and ensure immediate update const refreshCalendarsResponse = await fetch("/api/calendars?refresh=true"); if (refreshCalendarsResponse.ok) { const calendarsData = await refreshCalendarsResponse.json(); const meetings = convertCalendarsToMeetings(calendarsData); setScheduledMeetings(meetings); } toast({ title: "Succès", description: "Réunion supprimée", }); } catch (error) { console.error("Error deleting meeting:", error); toast({ title: "Erreur", description: error instanceof Error ? error.message : "Erreur lors de la suppression", variant: "destructive", }); } }; // Format date for display const formatDate = (dateStr: string): string => { const date = new Date(dateStr); return date.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' }); }; // Show loading state if (status === "loading" || loading) { return (

Chargement...

); } // Show Jitsi iframe if conference is selected if (selectedConference && jitsiUrl) { return (
{/* Header with back button */}

{selectedConference.type === "group" ? "Groupe" : "Mission"}: {selectedConference.name}

{/* Jitsi iframe */}
); } // Show list of groups and missions const todayMeetings = getTodayMeetings(); return (
{/* Header */}

Espaces de réunion

Sélectionnez un groupe ou une mission pour accéder à son espace de réunion Jitsi

{/* Today's Meetings Section */} {todayMeetings.length > 0 && (

Réunions du jour ({todayMeetings.length})

{todayMeetings.map((meeting) => (

{meeting.title || `${meeting.type === "group" ? "Groupe" : "Mission"}: ${meeting.entityName}`}

{meeting.time} - {formatDate(meeting.date)}

{canJoinMeeting(meeting) && ( )}
))}
)} {/* Calendar Section */}

Calendrier des réunions

{/* Colonne calendrier (étroite) */}