import React from "react"; import { Box, Button, Checkbox, FormControl, FormControlLabel, IconButton, InputLabel, MenuItem, Select, SelectChangeEvent, TextField, ToggleButtonGroup, ToggleButton, Typography, } from "@linagora/twake-mui"; import { Description as DescriptionIcon, Public as PublicIcon, VideocamOutlined as VideocamOutlinedIcon, ContentCopy as CopyIcon, Close as DeleteIcon, } from "@mui/icons-material"; import iconCamera from "../../static/images/icon-camera.svg"; import LockOutlineIcon from "@mui/icons-material/LockOutline"; import AttendeeSelector from "../Attendees/AttendeeSearch"; import RepeatEvent from "./EventRepeat"; import { RepetitionObject } from "../../features/Events/EventsTypes"; import { userAttendee } from "../../features/User/models/attendee"; import { Calendar } from "../../features/Calendars/CalendarTypes"; import { generateMeetingLink, addVideoConferenceToDescription, } from "../../utils/videoConferenceUtils"; import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete"; import { CalendarItemList } from "../Calendar/CalendarItemList"; import { useI18n } from "twake-i18n"; import { FieldWithLabel } from "./components/FieldWithLabel"; import { DateTimeFields } from "./components/DateTimeFields"; import { useAllDayToggle } from "./hooks/useAllDayToggle"; import { splitDateTime, combineDateTime } from "./utils/dateTimeHelpers"; import {} from "./utils/dateRules"; import {} from "./utils/dateTimeFormatters"; import { validateEventForm } from "./utils/formValidation"; import { SnackbarAlert } from "../Loading/SnackBarAlert"; import { AddDescButton } from "./AddDescButton"; 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); // Reset hasEndDateChanged when modal closes React.useEffect(() => { if (!isOpen) { setHasEndDateChanged(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); // Track previous showMore state to detect changes const prevShowMoreRef = React.useRef(undefined); // 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, }); }, [ title, 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); }; 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 = () => { const updatedDescription = description.replace( /\nVisio: https?:\/\/[^\s]+/, "" ); setDescription(updatedDescription); setHasVideoConference(false); setMeetingLink(null); }; const handleCalendarChange = (newCalendarId: string) => { setCalendarid(newCalendarId); onCalendarChange?.(newCalendarId); }; return ( <> { setTitle(e.target.value); }} size="small" margin="dense" inputRef={titleInputRef} /> {}} /> } 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={240} size="small" placeholder={t("event.form.timezonePlaceholder")} /> {(showRepeat || (typeOfAction === "solo" && repetition?.freq)) && ( )} } /> {hasVideoConference && meetingLink && ( <> )} setLocation(e.target.value)} size="small" margin="dense" /> {showMore && ( <> { if (newValue !== null) { setEventClass(newValue); } }} size="medium" > {t("event.form.visibleAll")} {t("event.form.visibleParticipants")} )} ); }