import { useState, useEffect, useMemo } from 'react'; import { ChevronLeft, ChevronRight, Plus, Clock, Video, Bell, Loader2, Download, Share2, Globe, Trash2, Edit, X, UserPlus, Send, ExternalLink, } from 'lucide-react'; import api from '../services/api'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import Modal from '../components/Modal'; import DateTimePicker from '../components/DateTimePicker'; import toast from 'react-hot-toast'; const COLORS = ['#6366f1', '#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899', '#14b8a6']; export default function Calendar() { const { user } = useAuth(); const { t } = useLanguage(); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const [currentDate, setCurrentDate] = useState(new Date()); const [view, setView] = useState('month'); // month | week const [rooms, setRooms] = useState([]); // Modal state const [showCreate, setShowCreate] = useState(false); const [showDetail, setShowDetail] = useState(null); const [editingEvent, setEditingEvent] = useState(null); const [showShare, setShowShare] = useState(null); const [showFedShare, setShowFedShare] = useState(null); // Create/Edit form const [form, setForm] = useState({ title: '', description: '', start_time: '', end_time: '', room_uid: '', color: '#6366f1', reminder_minutes: null, }); const [saving, setSaving] = useState(false); // Share state const [shareSearch, setShareSearch] = useState(''); const [shareResults, setShareResults] = useState([]); const [sharedUsers, setSharedUsers] = useState([]); const [pendingInvitations, setPendingInvitations] = useState([]); const [fedAddress, setFedAddress] = useState(''); const [fedSending, setFedSending] = useState(false); // Load events on month change useEffect(() => { fetchEvents(); }, [currentDate]); useEffect(() => { fetchRooms(); }, []); const fetchEvents = async () => { try { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const from = new Date(year, month - 1, 1).toISOString(); const to = new Date(year, month + 2, 0).toISOString(); const res = await api.get(`/calendar/events?from=${from}&to=${to}`); setEvents(res.data.events || []); } catch { toast.error(t('calendar.loadFailed')); } finally { setLoading(false); } }; const fetchRooms = async () => { try { const res = await api.get('/rooms'); setRooms(res.data.rooms || []); } catch { /* ignore */ } }; // Calendar grid computation const calendarDays = useMemo(() => { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); // Start from Monday (ISO week) let startOffset = firstDay.getDay() - 1; if (startOffset < 0) startOffset = 6; const calStart = new Date(year, month, 1 - startOffset); const days = []; const current = new Date(calStart); for (let i = 0; i < 42; i++) { days.push(new Date(current)); current.setDate(current.getDate() + 1); } return days; }, [currentDate]); const weekDays = useMemo(() => { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const date = currentDate.getDate(); const dayOfWeek = currentDate.getDay(); let mondayOffset = dayOfWeek - 1; if (mondayOffset < 0) mondayOffset = 6; const monday = new Date(year, month, date - mondayOffset); const days = []; for (let i = 0; i < 7; i++) { days.push(new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + i)); } return days; }, [currentDate]); const eventsForDay = (day) => { const dayStr = toLocalDateStr(day); return events.filter(ev => { const start = toLocalDateStr(new Date(ev.start_time)); const end = toLocalDateStr(new Date(ev.end_time)); return dayStr >= start && dayStr <= end; }); }; const isToday = (day) => { const today = new Date(); return day.toDateString() === today.toDateString(); }; const isCurrentMonth = (day) => { return day.getMonth() === currentDate.getMonth(); }; const navigatePrev = () => { const d = new Date(currentDate); if (view === 'month') d.setMonth(d.getMonth() - 1); else d.setDate(d.getDate() - 7); setCurrentDate(d); }; const navigateNext = () => { const d = new Date(currentDate); if (view === 'month') d.setMonth(d.getMonth() + 1); else d.setDate(d.getDate() + 7); setCurrentDate(d); }; const goToToday = () => setCurrentDate(new Date()); const monthLabel = currentDate.toLocaleString('default', { month: 'long', year: 'numeric' }); const openCreateForDay = (day) => { const start = new Date(day); start.setHours(9, 0, 0, 0); const end = new Date(day); end.setHours(10, 0, 0, 0); setForm({ title: '', description: '', start_time: toLocalDateTimeStr(start), end_time: toLocalDateTimeStr(end), room_uid: '', color: '#6366f1', reminder_minutes: null, }); setEditingEvent(null); setShowCreate(true); }; const openEdit = (ev) => { setForm({ title: ev.title, description: ev.description || '', start_time: toLocalDateTimeStr(new Date(ev.start_time)), end_time: toLocalDateTimeStr(new Date(ev.end_time)), room_uid: ev.room_uid || '', color: ev.color || '#6366f1', reminder_minutes: ev.reminder_minutes ?? null, }); setEditingEvent(ev); setShowDetail(null); setShowCreate(true); }; const handleSave = async (e) => { e.preventDefault(); setSaving(true); try { const data = { ...form, start_time: new Date(form.start_time).toISOString(), end_time: new Date(form.end_time).toISOString(), }; if (editingEvent) { await api.put(`/calendar/events/${editingEvent.id}`, data); toast.success(t('calendar.eventUpdated')); } else { await api.post('/calendar/events', data); toast.success(t('calendar.eventCreated')); } setShowCreate(false); setEditingEvent(null); fetchEvents(); } catch (err) { toast.error(err.response?.data?.error || t('calendar.saveFailed')); } finally { setSaving(false); } }; const handleDelete = async (ev) => { if (!confirm(t('calendar.deleteConfirm'))) return; try { await api.delete(`/calendar/events/${ev.id}`); toast.success(t('calendar.eventDeleted')); setShowDetail(null); fetchEvents(); } catch { toast.error(t('calendar.deleteFailed')); } }; const handleDownloadICS = async (ev) => { try { const res = await api.get(`/calendar/events/${ev.id}/ics`, { responseType: 'blob' }); const url = window.URL.createObjectURL(res.data); const a = document.createElement('a'); a.href = url; a.download = `${ev.title}.ics`; a.click(); window.URL.revokeObjectURL(url); toast.success(t('calendar.icsDownloaded')); } catch { toast.error(t('calendar.icsFailed')); } }; const buildOutlookUrl = (ev) => { const start = new Date(ev.start_time); const end = new Date(ev.end_time); const fmt = (d) => d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); const baseUrl = window.location.origin; const location = ev.room_uid ? `${baseUrl}/join/${ev.room_uid}` : ''; const body = [ev.description || '', location ? `\n\nMeeting: ${location}` : ''].join(''); const params = new URLSearchParams({ rru: 'addevent', subject: ev.title, startdt: start.toISOString(), enddt: end.toISOString(), body: body.trim(), location, allday: 'false', path: '/calendar/action/compose', }); return `https://outlook.live.com/calendar/0/action/compose?${params.toString()}`; }; const buildGoogleCalUrl = (ev) => { const fmt = (d) => new Date(d).toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); const baseUrl = window.location.origin; const location = ev.room_uid ? `${baseUrl}/join/${ev.room_uid}` : ''; const details = [ev.description || '', location ? `\nMeeting: ${location}` : ''].join(''); const params = new URLSearchParams({ action: 'TEMPLATE', text: ev.title, dates: `${fmt(ev.start_time)}/${fmt(ev.end_time)}`, details: details.trim(), location, }); return `https://calendar.google.com/calendar/render?${params.toString()}`; }; // Share functions const openShareModal = async (ev) => { setShowShare(ev); setShareSearch(''); setShareResults([]); setPendingInvitations([]); try { const res = await api.get(`/calendar/events/${ev.id}`); setSharedUsers(res.data.sharedUsers || []); setPendingInvitations(res.data.pendingInvitations || []); } catch { /* ignore */ } }; const searchUsers = async (query) => { setShareSearch(query); if (query.length < 2) { setShareResults([]); return; } try { const res = await api.get(`/rooms/users/search?q=${encodeURIComponent(query)}`); const sharedIds = new Set(sharedUsers.map(u => u.id)); const pendingIds = new Set(pendingInvitations.map(u => u.user_id)); setShareResults(res.data.users.filter(u => !sharedIds.has(u.id) && !pendingIds.has(u.id))); } catch { setShareResults([]); } }; const handleShare = async (userId) => { if (!showShare) return; try { const res = await api.post(`/calendar/events/${showShare.id}/share`, { user_id: userId }); setSharedUsers(res.data.sharedUsers); setPendingInvitations(res.data.pendingInvitations || []); setShareSearch(''); setShareResults([]); toast.success(t('calendar.invitationSent')); } catch (err) { toast.error(err.response?.data?.error || t('calendar.shareFailed')); } }; const handleUnshare = async (userId) => { if (!showShare) return; try { const res = await api.delete(`/calendar/events/${showShare.id}/share/${userId}`); setSharedUsers(res.data.sharedUsers); setPendingInvitations(res.data.pendingInvitations || []); toast.success(t('calendar.shareRemoved')); } catch { toast.error(t('calendar.shareFailed')); } }; const handleCancelInvitation = async (userId) => { if (!showShare) return; try { const res = await api.delete(`/calendar/events/${showShare.id}/share/${userId}`); setSharedUsers(res.data.sharedUsers); setPendingInvitations(res.data.pendingInvitations || []); toast.success(t('calendar.invitationCancelled')); } catch { toast.error(t('calendar.shareFailed')); } }; const handleFedSend = async (e) => { e.preventDefault(); if (!showFedShare) return; const normalized = fedAddress.startsWith('@') ? fedAddress.slice(1) : fedAddress; if (!normalized.includes('@') || normalized.endsWith('@')) { toast.error(t('federation.addressHint')); return; } setFedSending(true); try { await api.post(`/calendar/events/${showFedShare.id}/federation`, { to: fedAddress }); toast.success(t('calendar.fedSent')); setShowFedShare(null); setFedAddress(''); } catch (err) { toast.error(err.response?.data?.error || t('calendar.fedFailed')); } finally { setFedSending(false); } }; const dayNames = [ t('calendar.mon'), t('calendar.tue'), t('calendar.wed'), t('calendar.thu'), t('calendar.fri'), t('calendar.sat'), t('calendar.sun'), ]; if (loading) { return (
{t('calendar.subtitle')}
{showDetail.description}
)} {showDetail.room_uid && ( )} {showDetail.federated_from && ({t('calendar.pendingInvitations')}
{pendingInvitations.map(u => ({t('calendar.accepted')}
)} {sharedUsers.map(u => ({t('calendar.sendFederatedDesc')}