import { Calendar } from "@/features/Calendars/CalendarTypes"; import { RepetitionObject } from "@/features/Events/EventsTypes"; import { userAttendee } from "@/features/User/models/attendee"; import iconCamera from "@/static/images/icon-camera.svg"; import { addVideoConferenceToDescription, generateMeetingLink, removeVideoConferenceFromDescription, } from "@/utils/videoConferenceUtils"; import { Box, Button, Checkbox, FormControl, FormControlLabel, IconButton, MenuItem, Select, SelectChangeEvent, TextField, ToggleButton, ToggleButtonGroup, Typography, } from "@linagora/twake-mui"; import { Close as DeleteIcon, ContentCopy as CopyIcon, LocationOn as LocationIcon, Public as PublicIcon, } from "@mui/icons-material"; import LockOutlineIcon from "@mui/icons-material/LockOutline"; import SquareRoundedIcon from "@mui/icons-material/SquareRounded"; import React from "react"; import { useI18n } from "twake-i18n"; import AttendeeSelector from "../Attendees/AttendeeSearch"; import { CalendarItemList } from "../Calendar/CalendarItemList"; import { SnackbarAlert } from "../Loading/SnackBarAlert"; import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete"; import { AddDescButton } from "./AddDescButton"; import { DateTimeFields } from "./components/DateTimeFields"; import { DateTimeSummary } from "./components/DateTimeSummary"; import { FieldWithLabel } from "./components/FieldWithLabel"; import { SectionPreviewRow } from "./components/SectionPreviewRow"; import RepeatEvent from "./EventRepeat"; import { useAllDayToggle } from "./hooks/useAllDayToggle"; import { combineDateTime, splitDateTime } from "./utils/dateTimeHelpers"; import { validateEventForm } from "./utils/formValidation"; interface EventFormFieldsProps { // Form state title: string; setTitle: (title: string) => void; description: string; setDescription: (description: string) => void; location: string; setLocation: (location: string) => void; start: string; setStart: (start: string) => void; end: string; setEnd: (end: string) => void; allday: boolean; setAllDay: (allday: boolean) => void; repetition: RepetitionObject; setRepetition: (repetition: RepetitionObject) => void; typeOfAction?: "solo" | "all"; attendees: userAttendee[]; setAttendees: (attendees: userAttendee[]) => void; alarm: string; setAlarm: (alarm: string) => void; busy: string; setBusy: (busy: string) => void; eventClass: string; setEventClass: (eventClass: string) => void; timezone: string; setTimezone: (timezone: string) => void; calendarid: string; setCalendarid: (calendarid: string) => void; hasVideoConference: boolean; setHasVideoConference: (hasVideoConference: boolean) => void; meetingLink: string | null; setMeetingLink: (meetingLink: string | null) => void; // UI state showMore: boolean; showDescription: boolean; setShowDescription: (showDescription: boolean) => void; showRepeat: boolean; setShowRepeat: (showRepeat: boolean) => void; isOpen?: boolean; // Data userPersonalCalendars: Calendar[]; timezoneList: { zones: string[]; browserTz: string; getTimezoneOffset: (tzName: string, date: Date) => string; }; // Event handlers onStartChange?: (newStart: string) => void; onEndChange?: (newEnd: string) => void; onAllDayChange?: ( newAllDay: boolean, newStart: string, newEnd: string ) => void; onCalendarChange?: (newCalendarId: string) => void; // Validation onValidationChange?: (isValid: boolean) => void; showValidationErrors?: boolean; onHasEndDateChangedChange?: (has: boolean) => void; } export default function EventFormFields({ title, setTitle, description, setDescription, location, setLocation, start, setStart, end, setEnd, allday, setAllDay, repetition, setRepetition, typeOfAction, attendees, setAttendees, alarm, setAlarm, busy, setBusy, eventClass, setEventClass, timezone, setTimezone, calendarid, setCalendarid, hasVideoConference, setHasVideoConference, meetingLink, setMeetingLink, showMore, showDescription, setShowDescription, showRepeat, setShowRepeat, isOpen = false, userPersonalCalendars, timezoneList, onStartChange, onEndChange, onAllDayChange, onCalendarChange, onValidationChange, showValidationErrors = false, onHasEndDateChangedChange, }: EventFormFieldsProps) { const { t } = useI18n(); // Internal state for 4 separate fields const [startDate, setStartDate] = React.useState(""); const [startTime, setStartTime] = React.useState(""); const [endDate, setEndDate] = React.useState(""); const [endTime, setEndTime] = React.useState(""); // Track if user has manually changed end date in extended mode const [hasEndDateChanged, setHasEndDateChanged] = React.useState(false); // Track if user has clicked on datetime clickable section in normal mode // Once clicked, normal mode will always show full fields until modal closes const [hasClickedDateTimeSection, setHasClickedDateTimeSection] = React.useState(false); // Track if user has clicked on location section in normal mode const [hasClickedLocationSection, setHasClickedLocationSection] = React.useState(false); // Track if user has clicked on calendar section in normal mode const [hasClickedCalendarSection, setHasClickedCalendarSection] = React.useState(false); // Reset hasEndDateChanged and hasClickedDateTimeSection when modal closes React.useEffect(() => { if (!isOpen) { setHasEndDateChanged(false); setHasClickedDateTimeSection(false); setHasClickedLocationSection(false); setHasClickedCalendarSection(false); } }, [isOpen]); // Notify parent about end date change visibility state React.useEffect(() => { onHasEndDateChangedChange?.(hasEndDateChanged); }, [hasEndDateChanged, onHasEndDateChangedChange]); // Reset hasEndDateChanged when startDate === endDate in normal mode React.useEffect(() => { if ( !showMore && hasEndDateChanged && startDate && endDate && startDate === endDate ) { setHasEndDateChanged(false); } }, [showMore, hasEndDateChanged, startDate, endDate]); // Use all-day toggle hook const { handleAllDayToggle } = useAllDayToggle({ allday, start, end, startDate, startTime, endDate, endTime, setStartTime, setEndTime, setStart, setEnd, setAllDay, onAllDayChange, }); // Ref for title input field to enable auto-focus const titleInputRef = React.useRef(null); const locationInputRef = React.useRef(null); // Track previous showMore state to detect changes const prevShowMoreRef = React.useRef(undefined); // Focus location field when user clicks the location preview row React.useEffect(() => { if (hasClickedLocationSection && process.env.NODE_ENV !== "test") { locationInputRef.current?.focus(); } }, [hasClickedLocationSection]); // Auto-focus title field when modal opens (skip in test environment) React.useEffect(() => { if (isOpen) { if (titleInputRef.current && process.env.NODE_ENV !== "test") { // Use setTimeout to ensure the dialog is fully rendered const timer = setTimeout(() => { titleInputRef.current?.focus(); }, 100); return () => clearTimeout(timer); } } }, [isOpen]); // Auto-focus title field when toggling between normal and extended mode React.useEffect(() => { // Skip on initial render (when prevShowMoreRef is undefined) if ( prevShowMoreRef.current !== undefined && isOpen && process.env.NODE_ENV !== "test" ) { const hasChanged = prevShowMoreRef.current !== showMore; if (hasChanged) { // Simple setTimeout approach with sufficient delay for layout changes const timer = setTimeout(() => { if (titleInputRef.current) { titleInputRef.current.focus(); } }, 150); // Update previous value before returning cleanup prevShowMoreRef.current = showMore; return () => clearTimeout(timer); } } // Always update previous value prevShowMoreRef.current = showMore; }, [showMore, isOpen]); // Sync start prop to startDate/startTime React.useEffect(() => { if (start) { const { date, time } = splitDateTime(start); setStartDate(date); setStartTime(time); } }, [start]); // Sync end prop to endDate/endTime React.useEffect(() => { if (end) { const { date, time } = splitDateTime(end); setEndDate(date); setEndTime(time); } }, [end]); // Change handlers for 4 separate fields const handleStartDateChange = React.useCallback( (newDate: string, newTime?: string) => { setStartDate(newDate); const newStart = combineDateTime(newDate, newTime ? newTime : startTime); if (onStartChange) { onStartChange(newStart); } else { setStart(newStart); } }, [startTime, onStartChange, setStart] ); const handleStartTimeChange = React.useCallback( (newTime: string, newDate?: string) => { setStartTime(newTime); const newStart = combineDateTime(newDate ? newDate : startDate, newTime); if (onStartChange) { onStartChange(newStart); } else { setStart(newStart); } }, [startDate, onStartChange, setStart] ); const handleEndDateChange = React.useCallback( (newDate: string, newTime?: string) => { setEndDate(newDate); // Track if user changed end date in extended mode if (showMore) { setHasEndDateChanged(true); } const newEnd = combineDateTime(newDate, newTime ? newTime : endTime); if (onEndChange) { onEndChange(newEnd); } else { setEnd(newEnd); } }, [endTime, onEndChange, setEnd, showMore] ); const handleEndTimeChange = React.useCallback( (newTime: string, newDate?: string) => { setEndTime(newTime); const newEnd = combineDateTime(newDate ? newDate : endDate, newTime); if (onEndChange) { onEndChange(newEnd); } else { setEnd(newEnd); } }, [endDate, onEndChange, setEnd] ); // Validation logic const validateForm = React.useCallback(() => { return validateEventForm({ startDate, startTime, endDate, endTime, allday, showValidationErrors, hasEndDateChanged, showMore, }); }, [ startDate, startTime, endDate, endTime, allday, showValidationErrors, hasEndDateChanged, showMore, ]); // Notify parent about validation changes React.useEffect(() => { const validation = validateForm(); onValidationChange?.(validation.isValid); }, [validateForm, onValidationChange]); // Auto-calculate end time from start time (+ 1 hour) if not already set React.useEffect(() => { if (startTime && !endTime && !allday) { const [hours, minutes] = startTime.split(":"); const endHour = (parseInt(hours) + 1) % 24; const calculatedEndTime = `${endHour.toString().padStart(2, "0")}:${minutes}`; setEndTime(calculatedEndTime); const newEnd = combineDateTime(endDate || startDate, calculatedEndTime); if (onEndChange) { onEndChange(newEnd); } else { setEnd(newEnd); } } }, [startTime, endTime, allday, endDate, startDate, onEndChange, setEnd]); const validation = validateForm(); const handleAddVideoConference = () => { const newMeetingLink = generateMeetingLink(); const updatedDescription = addVideoConferenceToDescription( description, newMeetingLink ); setDescription(updatedDescription); setHasVideoConference(true); setMeetingLink(newMeetingLink); if (showMore) { setShowDescription(true); } }; const [openToast, setOpenToast] = React.useState(false); const handleCopyMeetingLink = async () => { if (meetingLink) { try { await navigator.clipboard.writeText(meetingLink); setOpenToast(true); } catch (err) { console.error("Failed to copy link:", err); } } }; const handleDeleteVideoConference = () => { setDescription(removeVideoConferenceFromDescription(description)); setHasVideoConference(false); setMeetingLink(null); }; const handleCalendarChange = (newCalendarId: string) => { setCalendarid(newCalendarId); onCalendarChange?.(newCalendarId); }; return ( <> { setTitle(e.target.value); }} size="small" margin="dense" inputRef={titleInputRef} /> {!showMore && !hasClickedDateTimeSection ? ( setHasClickedDateTimeSection(true)} /> ) : ( {}} /> )} {!(!showMore && !hasClickedDateTimeSection) && ( } label={ {t("event.form.allDay")} } /> { const newShowRepeat = !showRepeat; setShowRepeat(newShowRepeat); if (newShowRepeat) { setRepetition({ freq: "daily", interval: 1, occurrences: 0, endDate: "", byday: null, } as RepetitionObject); } else { setRepetition({ freq: "", interval: 1, occurrences: 0, endDate: "", byday: null, } as RepetitionObject); } }} /> } label={ {t("event.form.repeat")} } /> timezoneList.getTimezoneOffset(tzName, new Date(start)) } showIcon={false} width={220} size="small" placeholder={t("event.form.timezonePlaceholder")} /> )} {!(!showMore && !hasClickedDateTimeSection) && (showRepeat || (typeOfAction === "solo" && repetition?.freq)) && ( )} } /> {!showMore ? ( hasVideoConference && meetingLink ? ( ) : ( } onClick={handleAddVideoConference} > {t("event.form.addVisioConference")} ) ) : ( {hasVideoConference && meetingLink && ( <> )} )} {!showMore && !hasClickedLocationSection ? ( } onClick={() => setHasClickedLocationSection(true)} > {location || t("event.form.locationPlaceholder")} ) : ( setLocation(e.target.value)} size="small" margin="dense" /> )} {!showMore && !hasClickedCalendarSection ? ( cal.id === calendarid) ?.color?.light ?? "#3788D8", width: 24, height: 24, }} /> } onClick={() => setHasClickedCalendarSection(true)} > {userPersonalCalendars.find((cal) => cal.id === calendarid)?.name || t("event.form.calendar")} ) : ( )} {showMore && ( <> { if (newValue !== null) { setEventClass(newValue); } }} size="medium" > {t("event.form.visibleAll")} {t("event.form.visibleParticipants")} )} ); }