import { DateSelectArg } from "@fullcalendar/core"; import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; import CircleIcon from "@mui/icons-material/Circle"; import CloseIcon from "@mui/icons-material/Close"; import EditIcon from "@mui/icons-material/Edit"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined"; import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined"; import LockOutlineIcon from "@mui/icons-material/LockOutline"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import NotificationsNoneIcon from "@mui/icons-material/NotificationsNone"; import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined"; import RepeatIcon from "@mui/icons-material/Repeat"; import SubjectIcon from "@mui/icons-material/Subject"; import VideocamIcon from "@mui/icons-material/Videocam"; import { Box, Typography } from "@mui/material"; import EventPopover from "./EventModal"; import { Button, Chip, DialogActions, IconButton, Menu, MenuItem, Tooltip, } from "@mui/material"; import AvatarGroup from "@mui/material/AvatarGroup"; import { useEffect, useState } from "react"; import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { CalendarName } from "../../components/Calendar/CalendarName"; import { getTimezoneOffset } from "../../components/Calendar/TimezoneSelector"; import { formatEventChipTitle, updateTempCalendar, } from "../../components/Calendar/utils/calendarUtils"; import ResponsiveDialog from "../../components/Dialog/ResponsiveDialog"; import { EditModeDialog } from "../../components/Event/EditModeDialog"; import EventDuplication from "../../components/Event/EventDuplicate"; import { handleDelete, handleRSVP, } from "../../components/Event/eventHandlers/eventHandlers"; import { InfoRow } from "../../components/Event/InfoRow"; import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils"; import { getCalendarRange } from "../../utils/dateUtils"; import { deleteEventAsync } from "../Calendars/CalendarSlice"; import { dlEvent } from "./EventApi"; import { CalendarEvent } from "./EventsTypes"; import EventUpdateModal from "./EventUpdateModal"; import { useI18n } from "cozy-ui/transpiled/react/providers/I18n"; export default function EventPreviewModal({ eventId, calId, tempEvent, open, onClose, }: { eventId: string; calId: string; tempEvent?: boolean; open: boolean; onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void; }) { const { t } = useI18n(); const dispatch = useAppDispatch(); const calendars = useAppSelector((state) => state.calendars); const timezone = useAppSelector((state) => state.calendars.timeZone) ?? Intl.DateTimeFormat().resolvedOptions().timeZone; const calendarList = Object.values( useAppSelector((state) => state.calendars.list) ); const calendar = tempEvent ? calendars.templist[calId] : calendars.list[calId]; const event = calendar.events[eventId]; const user = useAppSelector((state) => state.user); const isRecurring = event?.uid?.includes("/"); const isOwn = calendar.ownerEmails?.includes(user.userData.email); const [showAllAttendees, setShowAllAttendees] = useState(false); const [openUpdateModal, setOpenUpdateModal] = useState(false); const [openDuplicateModal, setOpenDuplicateModal] = useState(false); const [hidePreview, setHidePreview] = useState(false); const [openEditModePopup, setOpenEditModePopup] = useState( null ); const [typeOfAction, setTypeOfAction] = useState<"solo" | "all" | undefined>( undefined ); const [afterChoiceFunc, setAfterChoiceFunc] = useState(); const [toggleActionMenu, setToggleActionMenu] = useState( null ); const mailSpaUrl = (window as any).MAIL_SPA_URL ?? null; useEffect(() => { if (!event || !calendar) { onClose({}, "backdropClick"); } }, [event, calendar, onClose]); if (!event || !calendar) return null; const attendeeDisplayLimit = 3; const attendees = event.attendee?.filter( (a) => a.cal_address !== event.organizer?.cal_address ) || []; const visibleAttendees = showAllAttendees ? attendees : attendees.slice(0, attendeeDisplayLimit); const currentUserAttendee = event.attendee?.find( (person) => person.cal_address === user.userData.email ); const organizer = event.attendee?.find( (a) => a.cal_address === event.organizer?.cal_address ); const updateTempList = async () => { if (calendars.templist) { const calendarRange = getCalendarRange(new Date(event.start)); await updateTempCalendar( calendars.templist, event, dispatch, calendarRange ); } }; return ( <> onClose({}, "backdropClick")} showHeaderActions={false} actionsBorderTop={currentUserAttendee && isOwn} actionsJustifyContent="center" style={{ overflow: "auto" }} title={ <> {(window as any).DEBUG && ( { const icsContent = await dlEvent(event); const blob = new Blob([icsContent], { type: "text/calendar", }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${eventId}.ics`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }} > )} {user.userData.email === event.organizer?.cal_address && calendar.ownerEmails?.includes(user.userData.email) && ( { if (isRecurring) { setAfterChoiceFunc(() => () => { setHidePreview(true); setOpenUpdateModal(true); }); setOpenEditModePopup("edit"); } else { setHidePreview(true); setOpenUpdateModal(true); } }} > )} {((event.class !== "PRIVATE" && !isOwn) || isOwn) && ( setToggleActionMenu(e.currentTarget)} > )} setToggleActionMenu(null)} anchorEl={toggleActionMenu} > {mailSpaUrl && attendees.length > 0 && ( window.open( `${mailSpaUrl}/mailto/?uri=mailto:${event.attendee .map((a) => a.cal_address) .filter((mail) => mail !== user.userData.email) .join(",")}?subject=${event.title}` ) } > {t("eventPreview.emailAttendees")} )} setToggleActionMenu(null)} onOpenDuplicate={() => { setToggleActionMenu(null); setHidePreview(true); setOpenDuplicateModal(true); }} /> {user.userData.email === event.organizer?.cal_address && ( { if (isRecurring) { setAfterChoiceFunc( () => (typeOfAction?: "solo" | "all" | undefined) => handleDelete( isRecurring, typeOfAction, onClose, dispatch, calendar, event, calId, eventId ) ); setOpenEditModePopup("edit"); } else { onClose({}, "backdropClick"); await dispatch( deleteEventAsync({ calId, eventId, eventURL: event.URL, }) ); } updateTempList(); }} > {t("eventPreview.deleteEvent")} )} onClose({}, "backdropClick")} > {event.class === "PRIVATE" && (isOwn ? ( ) : ( ))} {formatEventChipTitle(event, t)} {event.transp === "TRANSPARENT" && ( } label={t("eventPreview.free.label")} /> )} {formatDate(event.start, t, event.allday)} {event.end && formatEnd(event.start, event.end, t, event.allday) && ` – ${formatEnd(event.start, event.end, t, event.allday)} ${!event.allday ? getTimezoneOffset(timezone) : ""}`} } actions={ currentUserAttendee && isOwn && ( <> <> {t("eventPreview.attendingQuestion")} ) } > {((event.class !== "PRIVATE" && !isOwn) || isOwn) && ( <> {/* Video */} {event.x_openpass_videoconference && ( } content={ } /> )} {/* Attendees */} {attendees?.length > 0 && ( <> } content={ {t("eventPreview.guests", { count: attendees.length, })} {t("eventPreview.yesCount", { count: attendees.filter( (a) => a.partstat === "ACCEPTED" ).length, })} ,{" "} {t("eventPreview.noCount", { count: attendees.filter( (a) => a.partstat === "DECLINED" ).length, })} {!showAllAttendees && ( {organizer && renderAttendeeBadge( organizer, "org", t, showAllAttendees, true )} {visibleAttendees.map((a, idx) => renderAttendeeBadge( a, idx.toString(), t, showAllAttendees ) )} )} setShowAllAttendees(!showAllAttendees)} > {showAllAttendees ? t("eventPreview.showLess") : t("eventPreview.showMore")} } /> )} {showAllAttendees && organizer && renderAttendeeBadge(organizer, "org", t, showAllAttendees, true)} {showAllAttendees && visibleAttendees.map((a, idx) => renderAttendeeBadge(a, idx.toString(), t, showAllAttendees) )} {/* Location */} {event.location && ( } text={event.location} style={{ fontSize: "16px", fontFamily: "'Inter', sans-serif", }} /> )} {/* Description */} {event.description && ( } text={event.description} style={{ fontSize: "16px", fontFamily: "'Inter', sans-serif", }} /> )} {/* ALARM */} {event.alarm && ( } text={t("eventPreview.alarmText", { trigger: t(`event.form.notifications.${event.alarm.trigger}`), action: t(`event.form.notifications.${event.alarm.action}`), })} style={{ fontSize: "16px", fontFamily: "'Inter', sans-serif", }} /> )} {/* Repetition */} {event.repetition && ( } text={makeRecurrenceString(event, t)} style={{ fontSize: "16px", fontFamily: "'Inter', sans-serif", }} /> )} )} {event.class === "PRIVATE" && !isOwn && ( {t("eventPreview.privateEvent.hiddenDetails")} )} {/* Error */} {event.error && ( } text={event.error} style={{ fontSize: "16px", fontFamily: "'Inter', sans-serif", }} error /> )} {/* Calendar color dot */} { setTypeOfAction(type); afterChoiceFunc && afterChoiceFunc(type); }} /> { setOpenUpdateModal(false); setHidePreview(false); }} onCloseAll={() => { setOpenUpdateModal(false); onClose({}, "backdropClick"); }} eventId={eventId} calId={calId} typeOfAction={typeOfAction} /> {}} calendarRef={{ current: null }} onClose={() => { setOpenDuplicateModal(false); onClose({}, "backdropClick"); // Đóng cả preview modal }} event={event} /> ); } function makeRecurrenceString( event: CalendarEvent, t: Function ): string | undefined { if (!event.repetition) return; const recur: string[] = [ `${t("eventPreview.recurrentEvent")} · ${t( `eventPreview.freq.${event.repetition.freq}`, event.repetition.freq )}`, ]; const recurType: Record = { daily: t("eventPreview.days"), monthly: t("eventPreview.months"), yearly: t("eventPreview.years"), }; if (event.repetition.byday) { recur.push( t("eventPreview.recurrenceOnDays", { days: event.repetition.byday .map((s) => t(`eventPreview.onDays.${s}`)) .join(", "), }) ); } if (event.repetition.interval && event.repetition.interval > 1) { recur.push( t("eventPreview.everyInterval", { interval: event.repetition.interval, unit: recurType[event.repetition.freq], }) ); } if (event.repetition.occurrences) { recur.push( t("eventPreview.forOccurrences", { count: event.repetition.occurrences, }) ); } if (event.repetition.endDate) { recur.push( t("eventPreview.until", { date: new Date(event.repetition.endDate).toLocaleDateString( t("locale"), { year: "numeric", month: "long", day: "numeric", } ), }) ); } return recur.join(", "); } function formatDate(date: Date | string, t: Function, allday?: boolean) { if (allday) { return new Date(date).toLocaleDateString(t("locale"), { year: "numeric", month: "long", weekday: "long", day: "numeric", }); } else { return new Date(date).toLocaleString(t("locale"), { year: "numeric", month: "long", weekday: "long", day: "numeric", hour: "2-digit", minute: "2-digit", }); } } function formatEnd( start: Date | string, end: Date | string, t: Function, allday?: boolean ) { const startDate = new Date(start); const endDate = new Date(end); const sameDay = startDate.getFullYear() === endDate.getFullYear() && startDate.getMonth() === endDate.getMonth() && startDate.getDate() === endDate.getDate(); if (allday) { return sameDay ? null : endDate.toLocaleDateString(t("locale"), { year: "numeric", month: "short", day: "numeric", }); } else { if (sameDay) { return endDate.toLocaleTimeString(t("locale"), { hour: "2-digit", minute: "2-digit", }); } return endDate.toLocaleString(t("locale"), { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } }