424 lines
14 KiB
TypeScript
424 lines
14 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useSession } from "next-auth/react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { format, parseISO } from "date-fns";
|
|
import { fr } from "date-fns/locale";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Calendar as CalendarType } from "@prisma/client";
|
|
|
|
interface EventDialogProps {
|
|
open: boolean;
|
|
event?: any;
|
|
onClose: () => void;
|
|
onSave: (eventData: any) => Promise<void>;
|
|
onDelete?: (eventId: string) => Promise<void>;
|
|
calendars: CalendarType[];
|
|
}
|
|
|
|
export function EventDialog({
|
|
open,
|
|
event,
|
|
onClose,
|
|
onSave,
|
|
onDelete,
|
|
calendars,
|
|
}: EventDialogProps) {
|
|
const [title, setTitle] = useState(event?.title || "");
|
|
const [description, setDescription] = useState(event?.description || "");
|
|
const [location, setLocation] = useState(event?.location || "");
|
|
const [start, setStart] = useState(event?.start || "");
|
|
const [end, setEnd] = useState(event?.end || "");
|
|
const [allDay, setAllDay] = useState(event?.allDay || false);
|
|
const [calendarId, setCalendarId] = useState(event?.calendarId || "");
|
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
const [isVideoConference, setIsVideoConference] = useState(!!event?.location?.includes('vision.slm-lab.net') || !!event?.location?.includes('jitsi'));
|
|
const { data: session } = useSession();
|
|
const [groups, setGroups] = useState<Array<{ id: string; name: string }>>([]);
|
|
const [missions, setMissions] = useState<Array<{ id: string; name: string }>>([]);
|
|
|
|
// Reset form when event changes
|
|
useEffect(() => {
|
|
if (event) {
|
|
setTitle(event.title || "");
|
|
setDescription(event.description || "");
|
|
setLocation(event.location || "");
|
|
setStart(event.start || "");
|
|
setEnd(event.end || "");
|
|
setAllDay(event.allDay || false);
|
|
setCalendarId(event.calendarId || "");
|
|
setIsVideoConference(!!event.location?.includes('vision.slm-lab.net') || !!event.location?.includes('jitsi'));
|
|
} else {
|
|
// Reset form for new event
|
|
setTitle("");
|
|
setDescription("");
|
|
setLocation("");
|
|
setStart("");
|
|
setEnd("");
|
|
setAllDay(false);
|
|
setCalendarId("");
|
|
setIsVideoConference(false);
|
|
}
|
|
}, [event]);
|
|
|
|
// Fetch groups and missions when dialog opens
|
|
useEffect(() => {
|
|
if (open && session?.user?.id) {
|
|
const fetchData = async () => {
|
|
try {
|
|
// Fetch groups
|
|
const groupsRes = await fetch(`/api/users/${session.user.id}/groups`);
|
|
if (groupsRes.ok) {
|
|
const groupsData = await groupsRes.json();
|
|
setGroups(Array.isArray(groupsData) ? groupsData : []);
|
|
}
|
|
|
|
// Fetch 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 === session.user.id;
|
|
const isMember = mission.missionUsers?.some(
|
|
(mu: any) => mu.user?.id === session.user.id
|
|
);
|
|
return isCreator || isMember;
|
|
});
|
|
setMissions(userMissions);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching groups/missions:", error);
|
|
}
|
|
};
|
|
fetchData();
|
|
}
|
|
}, [open, session]);
|
|
|
|
// Formater les dates pour l'affichage
|
|
const formatDate = (dateStr: string) => {
|
|
if (!dateStr) return "";
|
|
try {
|
|
// @ts-ignore
|
|
const date = parseISO(dateStr);
|
|
// @ts-ignore
|
|
return format(date, allDay ? "yyyy-MM-dd" : "yyyy-MM-dd'T'HH:mm", {
|
|
// @ts-ignore
|
|
locale: fr,
|
|
});
|
|
} catch (e) {
|
|
return dateStr;
|
|
}
|
|
};
|
|
|
|
// Gérer le changement de l'option "Toute la journée"
|
|
const handleAllDayChange = (checked: boolean) => {
|
|
setAllDay(checked);
|
|
|
|
// Ajuster les dates si nécessaire
|
|
if (checked && start) {
|
|
// @ts-ignore
|
|
const startDate = parseISO(start);
|
|
// @ts-ignore
|
|
setStart(format(startDate, "yyyy-MM-dd"));
|
|
|
|
if (end) {
|
|
// @ts-ignore
|
|
const endDate = parseISO(end);
|
|
// @ts-ignore
|
|
setEnd(format(endDate, "yyyy-MM-dd"));
|
|
}
|
|
}
|
|
};
|
|
|
|
// Generate Jitsi URL based on calendar type
|
|
const generateJitsiUrl = (): string => {
|
|
const selectedCalendar = calendars.find(cal => cal.id === calendarId);
|
|
if (!selectedCalendar) return "";
|
|
|
|
const baseUrl = process.env.NEXT_PUBLIC_IFRAME_CONFERENCE_URL || 'https://vision.slm-lab.net';
|
|
const calendarName = selectedCalendar.name || "";
|
|
|
|
// Check if it's a Group calendar
|
|
if (calendarName.startsWith("Groupe:")) {
|
|
const groupName = calendarName.replace("Groupe: ", "");
|
|
// Find matching group by name to get the real ID
|
|
const matchingGroup = groups.find((g: { id: string; name: string }) => g.name === groupName);
|
|
if (matchingGroup) {
|
|
return `${baseUrl}/Groupe-${matchingGroup.id}`;
|
|
}
|
|
// Fallback: use group name as ID (same as vision page fallback)
|
|
const groupId = groupName.toLowerCase().replace(/\s+/g, '-');
|
|
return `${baseUrl}/Groupe-${groupId}`;
|
|
}
|
|
|
|
// Check if it's a Mission calendar
|
|
if (calendarName.startsWith("Mission:")) {
|
|
// Use missionId from calendar if available (most reliable)
|
|
const missionId = (selectedCalendar as any).missionId;
|
|
if (missionId) {
|
|
return `${baseUrl}/${missionId}`;
|
|
}
|
|
// Fallback: find matching mission by name
|
|
const missionName = calendarName.replace("Mission: ", "");
|
|
const matchingMission = missions.find((m: { id: string; name: string }) => m.name === missionName);
|
|
if (matchingMission) {
|
|
return `${baseUrl}/${matchingMission.id}`;
|
|
}
|
|
// Last fallback: use mission name as ID
|
|
const missionIdFromName = missionName.toLowerCase().replace(/\s+/g, '-');
|
|
return `${baseUrl}/${missionIdFromName}`;
|
|
}
|
|
|
|
// For "Mon Calendrier" or Microsoft calendars, generate a generic Jitsi room
|
|
// Use a combination of user ID and timestamp for uniqueness
|
|
const userId = session?.user?.id || 'user';
|
|
const timestamp = Date.now();
|
|
const randomId = Math.random().toString(36).substring(2, 9);
|
|
return `${baseUrl}/${randomId}-${timestamp}`;
|
|
};
|
|
|
|
// Handle video conference checkbox change
|
|
const handleVideoConferenceChange = (checked: boolean) => {
|
|
setIsVideoConference(checked);
|
|
if (checked) {
|
|
// Generate Jitsi URL and set it in location
|
|
const jitsiUrl = generateJitsiUrl();
|
|
setLocation(jitsiUrl);
|
|
} else {
|
|
// Clear location if unchecking
|
|
setLocation("");
|
|
}
|
|
};
|
|
|
|
// Update location when calendar changes and video conference is checked
|
|
useEffect(() => {
|
|
if (isVideoConference && calendarId) {
|
|
const jitsiUrl = generateJitsiUrl();
|
|
setLocation(jitsiUrl);
|
|
}
|
|
}, [calendarId, isVideoConference]);
|
|
|
|
// Update location when groups/missions are loaded (to get correct IDs for groups/missions)
|
|
useEffect(() => {
|
|
if (isVideoConference && calendarId) {
|
|
const selectedCalendar = calendars.find(cal => cal.id === calendarId);
|
|
if (selectedCalendar) {
|
|
const calendarName = selectedCalendar.name || "";
|
|
const isGroup = calendarName.startsWith("Groupe:");
|
|
const isMission = calendarName.startsWith("Mission:");
|
|
|
|
// Only update if it's a group/mission and we now have the data
|
|
if ((isGroup && groups.length > 0) || (isMission && missions.length > 0)) {
|
|
const jitsiUrl = generateJitsiUrl();
|
|
setLocation(jitsiUrl);
|
|
}
|
|
}
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [groups.length, missions.length]);
|
|
|
|
// Enregistrer l'événement
|
|
const handleSave = () => {
|
|
onSave({
|
|
id: event?.id,
|
|
title,
|
|
description,
|
|
location,
|
|
start,
|
|
end,
|
|
calendarId,
|
|
isAllDay: allDay,
|
|
});
|
|
};
|
|
|
|
// Supprimer l'événement
|
|
const handleDelete = () => {
|
|
if (onDelete && event?.id) {
|
|
onDelete(event.id);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Dialog open={open} onOpenChange={onClose}>
|
|
<DialogContent className='sm:max-w-[550px]'>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{event?.id ? "Modifier l'événement" : "Nouvel événement"}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className='grid gap-4 py-4'>
|
|
<div className='grid gap-2'>
|
|
<Label htmlFor='title'>Titre *</Label>
|
|
<Input
|
|
id='title'
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
placeholder='Ajouter un titre'
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Sélection du calendrier */}
|
|
<div className='grid gap-2'>
|
|
<Label htmlFor='calendar'>Calendrier *</Label>
|
|
<Select value={calendarId} onValueChange={setCalendarId} required>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder='Sélectionner un calendrier' />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{calendars.map((calendar) => (
|
|
<SelectItem key={calendar.id} value={calendar.id}>
|
|
<div className='flex items-center gap-2'>
|
|
<div
|
|
className='w-3 h-3 rounded-full'
|
|
style={{ backgroundColor: calendar.color }}
|
|
/>
|
|
<span>{calendar.name}</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className='grid grid-cols-2 gap-4'>
|
|
<div className='grid gap-2'>
|
|
<Label htmlFor='start-date'>Début *</Label>
|
|
<Input
|
|
type={allDay ? "date" : "datetime-local"}
|
|
id='start-date'
|
|
value={formatDate(start)}
|
|
onChange={(e) => setStart(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className='grid gap-2'>
|
|
<Label htmlFor='end-date'>Fin *</Label>
|
|
<Input
|
|
type={allDay ? "date" : "datetime-local"}
|
|
id='end-date'
|
|
value={formatDate(end)}
|
|
onChange={(e) => setEnd(e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className='flex items-center gap-2'>
|
|
<Checkbox
|
|
id='all-day'
|
|
checked={allDay}
|
|
onCheckedChange={handleAllDayChange}
|
|
/>
|
|
<Label htmlFor='all-day'>Toute la journée</Label>
|
|
</div>
|
|
|
|
<div className='flex items-center gap-2'>
|
|
<Checkbox
|
|
id='video-conference'
|
|
checked={isVideoConference}
|
|
onCheckedChange={handleVideoConferenceChange}
|
|
disabled={!calendarId}
|
|
/>
|
|
<Label htmlFor='video-conference'>Visioconférence</Label>
|
|
</div>
|
|
|
|
<div className='grid gap-2'>
|
|
<Label htmlFor='location'>Lieu (optionnel)</Label>
|
|
<Input
|
|
id='location'
|
|
value={location}
|
|
onChange={(e) => setLocation(e.target.value)}
|
|
placeholder={isVideoConference ? 'Lien Jitsi généré automatiquement' : 'Ajouter un lieu'}
|
|
disabled={isVideoConference}
|
|
/>
|
|
</div>
|
|
|
|
<div className='grid gap-2'>
|
|
<Label htmlFor='description'>Description (optionnel)</Label>
|
|
<Textarea
|
|
id='description'
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder='Ajouter une description'
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
{event?.id && onDelete && (
|
|
<Button
|
|
variant='destructive'
|
|
onClick={() => setConfirmDelete(true)}
|
|
type='button'
|
|
>
|
|
Supprimer
|
|
</Button>
|
|
)}
|
|
<Button variant='outline' onClick={onClose} type='button'>
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={!title || !start || !end || !calendarId}
|
|
type='button'
|
|
>
|
|
Enregistrer
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<AlertDialog open={confirmDelete} onOpenChange={setConfirmDelete}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Supprimer l'événement</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Êtes-vous sûr de vouloir supprimer cet événement ? Cette action
|
|
est irréversible.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleDelete}>
|
|
Supprimer
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
);
|
|
}
|