import React, { useState, useEffect, lazy, Suspense, useRef, useCallback } from 'react'; import { ChevronDown, ChevronUp, Plus, X, Bell, Users, Paperclip, MapPin, Calendar, Clock, Repeat, Link2, Eye, Activity, Check } from 'lucide-react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faGoogle, faApple, faMicrosoft } from '@fortawesome/free-brands-svg-icons'; import { faServer } from '@fortawesome/free-solid-svg-icons'; const RichTextEditor = lazy(() => import('./RichTextEditor')); function AnimatedDots() { return ( ... ); } // Reminder preset options (minutes) const REMINDER_PRESETS = [ { label: 'None', value: -1 }, { label: 'At time of event', value: 0 }, { label: '5 minutes before', value: 5 }, { label: '15 minutes before', value: 15 }, { label: '30 minutes before', value: 30 }, { label: '1 hour before', value: 60 }, { label: '2 hours before', value: 120 }, { label: '12 hours before', value: 720 }, { label: '1 day before', value: 1440 }, { label: '1 week before', value: 10080 }, ]; const CUSTOM_REMINDER_SENTINEL = -9999; function formatReminderMinutes(minutes: number, language: string): string { if (minutes === 0) return language === 'de' ? 'Zum Zeitpunkt' : 'At time of event'; if (minutes % 10080 === 0) { const w = minutes / 10080; return language === 'de' ? `${w} ${w === 1 ? 'Woche' : 'Wochen'} vorher` : `${w} week${w !== 1 ? 's' : ''} before`; } if (minutes % 1440 === 0) { const d = minutes / 1440; return language === 'de' ? `${d} ${d === 1 ? 'Tag' : 'Tage'} vorher` : `${d} day${d !== 1 ? 's' : ''} before`; } if (minutes % 60 === 0) { const h = minutes / 60; return language === 'de' ? `${h} ${h === 1 ? 'Stunde' : 'Stunden'} vorher` : `${h} hour${h !== 1 ? 's' : ''} before`; } return language === 'de' ? `${minutes} Minuten vorher` : `${minutes} minutes before`; } const BUSY_STATUS_OPTIONS = [ { label: 'Busy', value: 'busy' }, { label: 'Free', value: 'free' }, { label: 'Tentative', value: 'tentative' }, { label: 'Out of Office', value: 'oof' }, { label: 'Working Elsewhere', value: 'workingElsewhere' }, ]; const VISIBILITY_OPTIONS = [ { label: 'Default', value: 'default' }, { label: 'Public', value: 'public' }, { label: 'Private', value: 'private' }, { label: 'Confidential', value: 'confidential' }, ]; interface CalendarEventModalProps { event?: any; initialDate?: Date; initialStartTime?: string; initialEndTime?: string; connections: any[]; weekStartDay?: number; // 0=Sunday, 1=Monday language?: string; savedLocations?: string[]; onSaveLocation?: (loc: string) => void; customReminderMinutes?: number[]; onSaveCustomReminder?: (minutes: number) => void; onClose: () => void; onSave: (eventData: any) => Promise; onDelete?: (eventId: string, calendarId: string, deleteMode?: string) => Promise; } export default function CalendarEventModal({ event, initialDate, initialStartTime, initialEndTime, connections, weekStartDay = 0, language = 'en', savedLocations = [], onSaveLocation, customReminderMinutes = [], onSaveCustomReminder, onClose, onSave, onDelete }: CalendarEventModalProps) { const availableCalendars = connections .flatMap(conn => (conn.calendars || []).map((cal: any) => ({ ...cal, provider: conn.provider, providerName: conn.provider === 'google' ? 'Google Calendar' : conn.provider === 'apple' ? 'Apple Calendar' : conn.provider === 'synology' ? 'Synology Calendar' : 'Outlook Calendar' }))) .filter((cal: any) => cal.editable); const [title, setTitle] = useState(event?.title || ''); const [description, setDescription] = useState(event?.description || ''); const [location, setLocation] = useState(event?.location || ''); const [url, setUrl] = useState(event?.url || ''); const [recurrence, setRecurrence] = useState(event?.recurrence || 'none'); const [recurrenceEndType, setRecurrenceEndType] = useState<'never' | 'date' | 'count'>( event?.recurrenceCount ? 'count' : event?.recurrenceEndDate ? 'date' : 'never' ); const [recurrenceEndDateValue, setRecurrenceEndDateValue] = useState( event?.recurrenceEndDate || '' ); const [recurrenceCount, setRecurrenceCount] = useState(event?.recurrenceCount || 10); const [customInterval, setCustomInterval] = useState(event?.recurrenceInterval || 1); const [customUnit, setCustomUnit] = useState<'days' | 'weeks' | 'months' | 'years'>(event?.recurrenceUnit || 'weeks'); const startDow = (() => { const d = event?.start?.dateTime ? new Date(event.start.dateTime) : new Date(); return d.getDay(); })(); const [customDays, setCustomDays] = useState(event?.recurrenceDays || [startDow]); const [calendarId, setCalendarId] = useState(event?.calendarId || (availableCalendars.length > 0 ? availableCalendars[0].id : '')); const [reminders, setReminders] = useState>( event?.reminders || [{ method: 'display', minutes: 15 }] ); const [busyStatus, setBusyStatus] = useState(event?.busyStatus || 'busy'); const [visibility, setVisibility] = useState(event?.visibility || 'default'); const [attendees, setAttendees] = useState>( event?.attendees || [] ); const [newAttendeeEmail, setNewAttendeeEmail] = useState(''); const [attachments, setAttachments] = useState>( event?.attachments || [] ); const [newAttachmentUrl, setNewAttachmentUrl] = useState(''); const [showMoreOptions, setShowMoreOptions] = useState( !!(event?.attendees?.length || event?.attachments?.length || (event?.busyStatus && event.busyStatus !== 'busy') || (event?.visibility && event.visibility !== 'default')) ); const getInitialStart = () => { if (event?.start?.dateTime) return new Date(event.start.dateTime); if (event?.startTime) return new Date(event.startTime); if (initialDate) { const d = new Date(initialDate); if (initialStartTime) { const [h, m] = initialStartTime.split(':').map(Number); d.setHours(h, m, 0, 0); } else { const now = new Date(); d.setHours(now.getHours() + 1, 0, 0, 0); } return d; } return new Date(); }; const getInitialEnd = () => { if (event?.end?.dateTime) return new Date(event.end.dateTime); if (event?.endTime) return new Date(event.endTime); if (initialEndTime && initialDate) { const d = new Date(initialDate); const [h, m] = initialEndTime.split(':').map(Number); d.setHours(h, m, 0, 0); return d; } const start = getInitialStart(); return new Date(start.getTime() + 60 * 60 * 1000); }; const [startDate, setStartDate] = useState(getInitialStart()); const [endDate, setEndDate] = useState(getInitialEnd()); const [allDay, setAllDay] = useState(!!event?.allDay); const [isSaving, setIsSaving] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [deletingMode, setDeletingMode] = useState(null); const [error, setError] = useState(''); const [isCalendarSelectorOpen, setIsCalendarSelectorOpen] = useState(false); const calendarSelectorRef = useRef(null); const dialogRef = useRef(null); // Location autocomplete const [locationFocused, setLocationFocused] = useState(false); const [locationSuggestions, setLocationSuggestions] = useState([]); const locationRef = useRef(null); const updateLocationSuggestions = useCallback((val: string) => { if (!val.trim() || savedLocations.length === 0) { setLocationSuggestions([]); return; } const lower = val.toLowerCase(); const matches = savedLocations.filter(l => l.toLowerCase().includes(lower) && l !== val); setLocationSuggestions(matches.slice(0, 6)); }, [savedLocations]); // Custom reminder state // customReminderIdx tracks which reminder row is showing the custom editor const [customEditorIdx, setCustomEditorIdx] = useState(null); const [customAmount, setCustomAmount] = useState(30); const [reminderUnit, setReminderUnit] = useState<'minutes' | 'hours' | 'days'>('minutes'); const customAmountToMinutes = () => { if (reminderUnit === 'hours') return customAmount * 60; if (reminderUnit === 'days') return customAmount * 1440; return customAmount; }; // Build full reminder options list: presets + saved custom + "Custom..." const buildReminderOptions = () => [ ...REMINDER_PRESETS, ...customReminderMinutes .filter(m => !REMINDER_PRESETS.some(p => p.value === m)) .map(m => ({ label: formatReminderMinutes(m, language), value: m })), { label: language === 'de' ? 'Benutzerdefiniert…' : 'Custom…', value: CUSTOM_REMINDER_SENTINEL }, ]; // Close location suggestions on outside click useEffect(() => { const handler = (e: MouseEvent) => { if (locationRef.current && !locationRef.current.contains(e.target as Node)) { setLocationSuggestions([]); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); // Focus trap useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { onClose(); return; } if (e.key !== 'Tab') return; const dialog = dialogRef.current; if (!dialog) return; const focusable = Array.from( dialog.querySelectorAll('button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])') ); if (focusable.length === 0) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last.focus(); } } else { if (document.activeElement === last) { e.preventDefault(); first.focus(); } } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [onClose]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (calendarSelectorRef.current && !calendarSelectorRef.current.contains(event.target as Node)) { setIsCalendarSelectorOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const selectedCal = availableCalendars.find((c: any) => c.id === calendarId); const supportsURL = selectedCal && (selectedCal.provider !== 'google' && selectedCal.provider !== 'outlook' && selectedCal.provider !== 'synology'); const supportsAttachments = selectedCal && (selectedCal.provider === 'apple' || selectedCal.provider === 'synology'); const getProviderIcon = (provider: string) => { switch (provider) { case 'google': return ; case 'apple': return ; case 'outlook': return ; case 'synology': return ; default: return null; } }; const buildSavePayload = (editMode?: string) => { const activeReminders = reminders.filter(r => r.minutes >= 0); return { id: event?.id, title, description, location, url: url || undefined, recurrence: recurrence === 'custom' ? customUnit.replace(/s$/, '') === 'day' ? 'daily' : customUnit.replace(/s$/, '') === 'week' ? 'weekly' : customUnit.replace(/s$/, '') === 'month' ? 'monthly' : 'yearly' : recurrence !== 'none' ? recurrence : undefined, recurrenceInterval: recurrence === 'custom' ? customInterval : undefined, recurrenceDays: recurrence === 'custom' && customUnit === 'weeks' ? customDays : undefined, recurrenceEndDate: recurrence !== 'none' && recurrenceEndType === 'date' && recurrenceEndDateValue ? recurrenceEndDateValue : undefined, recurrenceCount: recurrence !== 'none' && recurrenceEndType === 'count' && recurrenceCount > 0 ? recurrenceCount : undefined, calendarId, allDay, start: { dateTime: startDate.toISOString() }, end: { dateTime: endDate.toISOString() }, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, reminders: activeReminders.length > 0 ? activeReminders : undefined, busyStatus: busyStatus, visibility: visibility !== 'default' ? visibility : undefined, attendees: attendees.length > 0 ? attendees : undefined, attachments: attachments.length > 0 ? attachments : undefined, ...(editMode ? { editMode, recurringEventId: event?.recurringEventId } : {}), }; }; const handleSubmit = async (editMode?: string) => { if (!title.trim()) { setError(language === 'de' ? 'Titel erforderlich' : 'Title is required'); return; } if (!calendarId) { setError(language === 'de' ? 'Bitte Kalender auswählen' : 'Please select a calendar'); return; } if (endDate <= startDate) { setError(language === 'de' ? 'Ende muss nach Start liegen' : 'End time must be after start time'); return; } if (event?.id && event?.isRecurring && !editMode && !showRecurringEditOptions) { setShowRecurringEditOptions(true); return; } // Save location to user profile if non-empty and new if (location.trim() && onSaveLocation) { onSaveLocation(location.trim()); } setIsSaving(true); setError(''); try { await onSave(buildSavePayload(editMode)); onClose(); } catch (err: any) { console.error(err); setError(err.message || 'Failed to save event'); setIsSaving(false); setShowRecurringEditOptions(false); } }; const [isDeleteConfirming, setIsDeleteConfirming] = useState(false); const [showRecurringDeleteOptions, setShowRecurringDeleteOptions] = useState(false); const [showRecurringEditOptions, setShowRecurringEditOptions] = useState(false); const [recurringEditMode, setRecurringEditMode] = useState<'this' | 'future' | 'all'>('this'); const handleDelete = async (mode?: string) => { if (!event?.id || !onDelete) return; if (event.isRecurring && !mode && !showRecurringDeleteOptions) { setShowRecurringDeleteOptions(true); return; } if (!event.isRecurring && !isDeleteConfirming) { setIsDeleteConfirming(true); setTimeout(() => setIsDeleteConfirming(false), 3000); return; } setIsDeleting(true); setDeletingMode(mode || 'all'); try { await onDelete(event.id, event.calendarId, mode || 'all'); onClose(); } catch (err: any) { setError(err.message || 'Failed to delete event'); setIsDeleting(false); setDeletingMode(null); setIsDeleteConfirming(false); setShowRecurringDeleteOptions(false); } }; const toLocalISOString = (date: Date) => { const offset = date.getTimezoneOffset() * 60000; const localISOTime = (new Date(date.getTime() - offset)).toISOString().slice(0, 16); return localISOTime; }; const handleStartDateChange = (val: string) => { const newStart = new Date(val); setStartDate(newStart); if (endDate <= newStart) { setEndDate(new Date(newStart.getTime() + 60 * 60 * 1000)); } }; const addAttendee = () => { const email = newAttendeeEmail.trim(); if (!email || !email.includes('@')) return; if (attendees.some(a => a.email === email)) return; setAttendees([...attendees, { email }]); setNewAttendeeEmail(''); }; const removeAttendee = (email: string) => { setAttendees(attendees.filter(a => a.email !== email)); }; const addAttachment = () => { const attachUrl = newAttachmentUrl.trim(); if (!attachUrl || (!attachUrl.startsWith('http://') && !attachUrl.startsWith('https://'))) return; if (attachments.some(a => a.url === attachUrl)) return; setAttachments([...attachments, { url: attachUrl }]); setNewAttachmentUrl(''); }; const removeAttachment = (attachUrl: string) => { setAttachments(attachments.filter(a => a.url !== attachUrl)); }; const updateReminder = (index: number, minutes: number) => { if (minutes === -1) { setReminders(reminders.filter((_, i) => i !== index)); setCustomEditorIdx(null); return; } if (minutes === CUSTOM_REMINDER_SENTINEL) { setCustomEditorIdx(index); setCustomAmount(30); setReminderUnit('minutes'); return; } const updated = [...reminders]; updated[index] = { ...updated[index], minutes }; setReminders(updated); setCustomEditorIdx(null); }; const confirmCustomReminder = (index: number) => { const mins = customAmountToMinutes(); if (mins <= 0) return; const updated = [...reminders]; updated[index] = { ...updated[index], minutes: mins }; setReminders(updated); setCustomEditorIdx(null); if (onSaveCustomReminder) onSaveCustomReminder(mins); }; const addReminder = () => { setReminders([...reminders, { method: 'display', minutes: 15 }]); }; // Compact icon-row style used throughout const iconRow: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: '8px', padding: '3px 0', minHeight: '24px', }; const iconCol: React.CSSProperties = { width: '18px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--weekly-text-light)', flexShrink: 0, opacity: 0.6, }; const fieldCol: React.CSSProperties = { flex: 1, minWidth: 0, }; const inlineSelect: React.CSSProperties = { background: 'transparent', border: 'none', fontSize: '0.78rem', cursor: 'pointer', outline: 'none', color: 'var(--weekly-text)', padding: '1px 0', }; const chipBtn = (active: boolean): React.CSSProperties => ({ padding: '3px 10px', borderRadius: '14px', fontSize: '0.72rem', fontWeight: 500, border: active ? '1.5px solid #3b82f6' : '1px solid var(--weekly-border, #ddd)', background: active ? 'rgba(59,130,246,0.1)' : 'transparent', color: active ? '#3b82f6' : 'var(--weekly-text)', cursor: 'pointer', transition: 'all 0.15s', }); const calColor = selectedCal?.backgroundColor || selectedCal?.color || '#3b82f6'; return (