"use client"; import { useState, useRef, useEffect } from "react"; import FullCalendar from "@fullcalendar/react"; import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; import frLocale from "@fullcalendar/core/locales/fr"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Loader2, Plus, Calendar as CalendarIcon, Check, X, User, Clock, BarChart2, Settings, ChevronRight, ChevronLeft, Bell, Users, MapPin, Tag, ChevronDown, ChevronUp, RefreshCw, Link as LinkIcon } from "lucide-react"; import { Calendar, Event } from "@prisma/client"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; 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"; import { Checkbox } from "@/components/ui/checkbox"; // Register French locale registerLocale('fr', fr); // Predefined professional color palette const colorPalette = [ "#4f46e5", // Indigo "#0891b2", // Cyan "#0e7490", // Teal "#16a34a", // Green "#65a30d", // Lime "#ca8a04", // Amber "#d97706", // Orange "#dc2626", // Red "#e11d48", // Rose "#9333ea", // Purple "#7c3aed", // Violet "#2563eb", // Blue ]; interface CalendarWithMission extends Calendar { missionId?: string | null; mission?: { id: string; creatorId: string; missionUsers: Array<{ userId: string; role: string; }>; } | null; syncConfig?: { id: string; provider: string; externalCalendarId: string | null; externalCalendarUrl: string | null; syncEnabled: boolean; lastSyncAt: Date | null; syncFrequency: number; lastSyncError: string | null; mailCredential?: { id: string; email: string; display_name: string | null; } | null; } | null; } interface CalendarClientProps { initialCalendars: (CalendarWithMission & { events: Event[] })[]; userId: string; userProfile: { name: string; email: string; avatar?: string; }; } interface EventFormData { title: string; description: string | null; start: string; end: string; allDay: boolean; location: string | null; calendarId?: string; } interface CalendarDialogProps { open: boolean; onClose: () => void; onSave: (calendarData: Partial) => Promise; onDelete?: (calendarId: string) => Promise; onSyncSetup?: (calendarId: string, mailCredentialId: string, externalCalendarUrl: string, externalCalendarId?: string, provider?: string) => Promise; initialData?: Partial; syncConfig?: { id: string; provider: string; syncEnabled: boolean; lastSyncAt: Date | null; mailCredential?: { id: string; email: string; display_name: string | null; } | null; } | null; } function CalendarDialog({ open, onClose, onSave, onDelete, onSyncSetup, initialData, syncConfig }: CalendarDialogProps) { const [name, setName] = useState(initialData?.name || ""); const [color, setColor] = useState(initialData?.color || "#4f46e5"); const [description, setDescription] = useState(initialData?.description || ""); const [isSubmitting, setIsSubmitting] = useState(false); const [customColorMode, setCustomColorMode] = useState(false); // Sync state const [showSyncSection, setShowSyncSection] = useState(false); const [availableAccounts, setAvailableAccounts] = useState>([]); const [selectedAccountId, setSelectedAccountId] = useState(""); const [availableCalendars, setAvailableCalendars] = useState>([]); const [selectedCalendarUrl, setSelectedCalendarUrl] = useState(""); const [isDiscovering, setIsDiscovering] = useState(false); const [isSettingUpSync, setIsSettingUpSync] = useState(false); const isMainCalendar = initialData?.name === "Calendrier principal"; const isMissionOrGroupCalendar = initialData?.name?.startsWith("Mission:") || initialData?.name?.startsWith("Groupe:"); const isPrivateCalendar = !isMissionOrGroupCalendar && (initialData?.name === "Privée" || initialData?.name === "Default" || !initialData?.name); useEffect(() => { if (open) { setName(initialData?.name || ""); setColor(initialData?.color || "#4f46e5"); setDescription(initialData?.description || ""); setCustomColorMode(!colorPalette.includes(initialData?.color || "#4f46e5")); setShowSyncSection(false); setSelectedAccountId(""); setAvailableCalendars([]); setSelectedCalendarUrl(""); // Load available accounts for sync if (isPrivateCalendar && !syncConfig) { loadAvailableAccounts(); } } }, [open, initialData, syncConfig, isPrivateCalendar]); const loadAvailableAccounts = async () => { try { const response = await fetch("/api/courrier/account-list"); if (response.ok) { const data = await response.json(); if (data.success && data.accounts) { // Filter Microsoft accounts only const syncableAccounts = data.accounts.filter((acc: any) => (acc.host && acc.host.includes('outlook.office365.com') && acc.use_oauth) ); setAvailableAccounts(syncableAccounts.map((acc: any) => ({ id: acc.id, email: acc.email, display_name: acc.display_name, host: acc.host }))); } } } catch (error) { console.error("Error loading accounts:", error); } }; const handleDiscoverCalendars = async () => { if (!selectedAccountId) return; setIsDiscovering(true); try { // Use Microsoft endpoint only const endpoint = "/api/calendars/sync/discover-microsoft"; const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mailCredentialId: selectedAccountId }), }); if (response.ok) { const data = await response.json(); // Normalize calendar format (Microsoft uses 'id' and 'webLink') const normalizedCalendars = (data.calendars || []).map((cal: any) => ({ id: cal.id, name: cal.name, url: cal.webLink || cal.id, // Use webLink or id as fallback })); setAvailableCalendars(normalizedCalendars); } else { const error = await response.json(); alert(error.error || "Erreur lors de la découverte des calendriers"); } } catch (error) { console.error("Error discovering calendars:", error); alert("Erreur lors de la découverte des calendriers"); } finally { setIsDiscovering(false); } }; const handleSetupSync = async () => { if (!initialData?.id || !selectedAccountId || !selectedCalendarUrl || !onSyncSetup) return; setIsSettingUpSync(true); try { // All accounts are Microsoft const selectedAccount = availableAccounts.find(acc => acc.id === selectedAccountId); const provider = 'microsoft'; // For Microsoft, use calendar ID instead of URL const externalCalendarId = availableCalendars.find(cal => cal.url === selectedCalendarUrl)?.id || selectedCalendarUrl; await onSyncSetup(initialData.id, selectedAccountId, selectedCalendarUrl, externalCalendarId, provider); setShowSyncSection(false); alert("Synchronisation configurée avec succès !"); } catch (error) { console.error("Error setting up sync:", error); alert("Erreur lors de la configuration de la synchronisation"); } finally { setIsSettingUpSync(false); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); try { // Only update color, preserve name and description from initialData await onSave({ id: initialData?.id, name: initialData?.name || name, // Keep original name color, // Only color can be changed description: initialData?.description || description // Keep original description }); resetForm(); } catch (error) { console.error("Erreur lors de la mise à jour du calendrier:", error); } finally { setIsSubmitting(false); } }; const handleDelete = async () => { if (!initialData?.id || !onDelete || isMainCalendar) return; if (!confirm("Êtes-vous sûr de vouloir supprimer ce calendrier ? Tous les événements associés seront également supprimés.")) { return; } setIsSubmitting(true); try { await onDelete(initialData.id); resetForm(); } catch (error) { console.error("Erreur lors de la suppression du calendrier:", error); } finally { setIsSubmitting(false); } }; const resetForm = () => { setName(""); setColor("#4f46e5"); setDescription(""); setCustomColorMode(false); onClose(); }; return ( !open && onClose()}> {initialData?.id ? "Paramètres du calendrier" : "Créer un nouveau calendrier"}
{/* Display calendar name (read-only) */} {initialData?.id && (
{initialData?.name || name}
)} {/* Name input only for new calendars */} {!initialData?.id && (
setName(e.target.value)} placeholder="Nom du calendrier" required className="rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 bg-white text-gray-900" />
)}
{colorPalette.map((paletteColor) => ( ))}
{customColorMode && (
setColor(e.target.value)} className="w-10 h-10 p-1 cursor-pointer rounded border-gray-300" /> setColor(e.target.value)} placeholder="#RRGGBB" className="w-28 rounded-lg" />
)}
{/* Display description (read-only) for existing calendars */} {initialData?.id && initialData?.description && (
{initialData?.description || description}
)} {/* Description input only for new calendars */} {!initialData?.id && (