39d8085766
Co-authored-by: Camille Moussu <cmoussu@linagora.com>
867 lines
26 KiB
TypeScript
867 lines
26 KiB
TypeScript
import React from "react";
|
|
import {
|
|
Box,
|
|
Button,
|
|
Checkbox,
|
|
FormControl,
|
|
FormControlLabel,
|
|
IconButton,
|
|
InputLabel,
|
|
MenuItem,
|
|
Select,
|
|
SelectChangeEvent,
|
|
TextField,
|
|
Typography,
|
|
ToggleButtonGroup,
|
|
ToggleButton,
|
|
} from "@mui/material";
|
|
import {
|
|
Description as DescriptionIcon,
|
|
Public as PublicIcon,
|
|
Lock as LockIcon,
|
|
CameraAlt as VideocamIcon,
|
|
ContentCopy as CopyIcon,
|
|
Close as DeleteIcon,
|
|
} from "@mui/icons-material";
|
|
import AttendeeSelector from "../Attendees/AttendeeSearch";
|
|
import RepeatEvent from "./EventRepeat";
|
|
import { RepetitionObject } from "../../features/Events/EventsTypes";
|
|
import { userAttendee } from "../../features/User/userDataTypes";
|
|
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
|
import {
|
|
generateMeetingLink,
|
|
addVideoConferenceToDescription,
|
|
} from "../../utils/videoConferenceUtils";
|
|
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
|
|
import { CalendarItemList } from "../Calendar/CalendarItemList";
|
|
|
|
// Helper component for field with label
|
|
export const FieldWithLabel = React.memo(
|
|
({
|
|
label,
|
|
isExpanded,
|
|
children,
|
|
}: {
|
|
label: string | React.ReactNode;
|
|
isExpanded: boolean;
|
|
children: React.ReactNode;
|
|
}) => {
|
|
if (!isExpanded) {
|
|
// Normal mode: label on top
|
|
return (
|
|
<Box>
|
|
<Typography
|
|
component="label"
|
|
sx={{
|
|
display: "block",
|
|
marginBottom: "4px",
|
|
fontSize: "0.875rem",
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{label}
|
|
</Typography>
|
|
{children}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Extended mode: label on left
|
|
return (
|
|
<Box display="flex" alignItems="center">
|
|
<Typography
|
|
component="label"
|
|
sx={{
|
|
minWidth: "115px",
|
|
marginRight: "12px",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{label}
|
|
</Typography>
|
|
<Box flexGrow={1}>{children}</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
);
|
|
|
|
FieldWithLabel.displayName = "FieldWithLabel";
|
|
|
|
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
|
|
userPersonnalCalendars: Calendars[];
|
|
timezoneList: {
|
|
zones: string[];
|
|
browserTz: string;
|
|
getTimezoneOffset: (tzName: string) => 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;
|
|
}
|
|
|
|
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,
|
|
userPersonnalCalendars,
|
|
timezoneList,
|
|
onStartChange,
|
|
onEndChange,
|
|
onAllDayChange,
|
|
onCalendarChange,
|
|
onValidationChange,
|
|
showValidationErrors = false,
|
|
}: EventFormFieldsProps) {
|
|
// Store original time before toggling to all-day
|
|
const originalTimeRef = React.useRef<{ start: string; end: string } | null>(
|
|
null
|
|
);
|
|
|
|
// Ref for title input field to enable auto-focus
|
|
const titleInputRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
// Track previous showMore state to detect changes
|
|
const prevShowMoreRef = React.useRef<boolean | undefined>(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]);
|
|
|
|
// Validation logic
|
|
const validateForm = React.useCallback(() => {
|
|
// Title validation
|
|
const isTitleValid = title.trim().length > 0;
|
|
const shouldShowTitleError = showValidationErrors && !isTitleValid;
|
|
|
|
// Date/time validation
|
|
let isDateTimeValid = true;
|
|
let dateTimeError = "";
|
|
let startError = "";
|
|
|
|
// Convert to string if needed
|
|
const startStr = typeof start === "string" ? start : String(start || "");
|
|
const endStr = typeof end === "string" ? end : String(end || "");
|
|
|
|
// Check if start date is provided and valid
|
|
if (!start || startStr.trim() === "") {
|
|
isDateTimeValid = false;
|
|
dateTimeError = "Start date/time is required";
|
|
startError = "Start date/time is required";
|
|
} else if (isNaN(new Date(start).getTime())) {
|
|
isDateTimeValid = false;
|
|
dateTimeError = "Invalid start date/time";
|
|
startError = "Invalid start date/time";
|
|
}
|
|
// Check if end date is provided and valid
|
|
else if (!end || endStr.trim() === "") {
|
|
isDateTimeValid = false;
|
|
dateTimeError = "End date/time is required";
|
|
} else if (isNaN(new Date(end).getTime())) {
|
|
isDateTimeValid = false;
|
|
dateTimeError = "Invalid end date/time";
|
|
}
|
|
// Check if end is after start
|
|
else {
|
|
const startDate = new Date(start);
|
|
const endDate = new Date(end);
|
|
if (endDate <= startDate) {
|
|
isDateTimeValid = false;
|
|
dateTimeError = "End time must be after start time";
|
|
}
|
|
}
|
|
|
|
const isValid = isTitleValid && isDateTimeValid;
|
|
|
|
return {
|
|
isValid,
|
|
errors: {
|
|
title: shouldShowTitleError ? "Title is required" : "",
|
|
start: showValidationErrors ? startError : "",
|
|
dateTime: showValidationErrors ? dateTimeError : "",
|
|
},
|
|
};
|
|
}, [title, start, end, showValidationErrors]);
|
|
|
|
// Notify parent about validation changes
|
|
React.useEffect(() => {
|
|
const validation = validateForm();
|
|
onValidationChange?.(validation.isValid);
|
|
}, [validateForm, onValidationChange]);
|
|
|
|
const validation = validateForm();
|
|
|
|
const handleAddVideoConference = () => {
|
|
const newMeetingLink = generateMeetingLink();
|
|
const updatedDescription = addVideoConferenceToDescription(
|
|
description,
|
|
newMeetingLink
|
|
);
|
|
setDescription(updatedDescription);
|
|
setHasVideoConference(true);
|
|
setMeetingLink(newMeetingLink);
|
|
};
|
|
|
|
const handleCopyMeetingLink = async () => {
|
|
if (meetingLink) {
|
|
try {
|
|
await navigator.clipboard.writeText(meetingLink);
|
|
} catch (err) {
|
|
console.error("Failed to copy link:", err);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleDeleteVideoConference = () => {
|
|
// Remove video conference footer from description
|
|
const updatedDescription = description.replace(
|
|
/\nVisio: https?:\/\/[^\s]+/,
|
|
""
|
|
);
|
|
setDescription(updatedDescription);
|
|
setHasVideoConference(false);
|
|
setMeetingLink(null);
|
|
};
|
|
|
|
const handleStartChange = (newStart: string) => {
|
|
if (onStartChange) {
|
|
onStartChange(newStart);
|
|
} else {
|
|
setStart(newStart);
|
|
}
|
|
};
|
|
|
|
const handleEndChange = (newEnd: string) => {
|
|
if (onEndChange) {
|
|
onEndChange(newEnd);
|
|
} else {
|
|
setEnd(newEnd);
|
|
}
|
|
};
|
|
|
|
const handleAllDayChange = (
|
|
newAllDay: boolean,
|
|
newStart: string,
|
|
newEnd: string
|
|
) => {
|
|
setAllDay(newAllDay);
|
|
onAllDayChange?.(newAllDay, newStart, newEnd);
|
|
};
|
|
|
|
const handleCalendarChange = (newCalendarId: string) => {
|
|
setCalendarid(newCalendarId);
|
|
onCalendarChange?.(newCalendarId);
|
|
};
|
|
|
|
const getRoundedCurrentTime = () => {
|
|
const now = new Date();
|
|
const minutes = now.getMinutes();
|
|
const roundedMinutes = minutes < 30 ? 0 : 30;
|
|
now.setMinutes(roundedMinutes);
|
|
now.setSeconds(0);
|
|
now.setMilliseconds(0);
|
|
return now;
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<FieldWithLabel
|
|
label={
|
|
<>
|
|
Title <span style={{ color: "red" }}>*</span>
|
|
</>
|
|
}
|
|
isExpanded={showMore}
|
|
>
|
|
<TextField
|
|
fullWidth
|
|
label={!showMore ? "Title" : ""}
|
|
placeholder="Add title"
|
|
value={title}
|
|
onChange={(e) => {
|
|
setTitle(e.target.value);
|
|
}}
|
|
error={!!validation.errors.title}
|
|
helperText={validation.errors.title}
|
|
size="small"
|
|
margin="dense"
|
|
inputRef={titleInputRef}
|
|
/>
|
|
</FieldWithLabel>
|
|
|
|
{!showDescription && (
|
|
<FieldWithLabel label=" " isExpanded={showMore}>
|
|
<Box display="flex" gap={1} mb={1}>
|
|
<Button
|
|
startIcon={<DescriptionIcon />}
|
|
onClick={() => setShowDescription(true)}
|
|
size="small"
|
|
sx={{
|
|
textTransform: "none",
|
|
color: "text.secondary",
|
|
}}
|
|
>
|
|
Add description
|
|
</Button>
|
|
</Box>
|
|
</FieldWithLabel>
|
|
)}
|
|
|
|
{showDescription && (
|
|
<FieldWithLabel label="Description" isExpanded={showMore}>
|
|
<TextField
|
|
fullWidth
|
|
label={!showMore ? "Description" : ""}
|
|
placeholder="Add description"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
size="small"
|
|
margin="dense"
|
|
multiline
|
|
minRows={2}
|
|
maxRows={10}
|
|
sx={{
|
|
"& .MuiInputBase-root": {
|
|
maxHeight: "33%",
|
|
overflowY: "auto",
|
|
},
|
|
"& textarea": {
|
|
resize: "vertical",
|
|
},
|
|
}}
|
|
/>
|
|
</FieldWithLabel>
|
|
)}
|
|
|
|
<FieldWithLabel label="Date & Time" isExpanded={showMore}>
|
|
<Box display="flex" gap={2} flexDirection="column">
|
|
<Box display="flex" gap={2}>
|
|
<Box flexGrow={1}>
|
|
{showMore && (
|
|
<Typography variant="caption" display="block" mb={0.5}>
|
|
Start
|
|
</Typography>
|
|
)}
|
|
<TextField
|
|
fullWidth
|
|
label={!showMore ? "Start" : ""}
|
|
type={allday ? "date" : "datetime-local"}
|
|
value={allday ? start.split("T")[0] : start}
|
|
onChange={(e) => handleStartChange(e.target.value)}
|
|
error={!!validation.errors.start}
|
|
helperText={validation.errors.start}
|
|
size="small"
|
|
margin="dense"
|
|
InputLabelProps={{ shrink: true }}
|
|
/>
|
|
</Box>
|
|
<Box flexGrow={1}>
|
|
{showMore && (
|
|
<Typography variant="caption" display="block" mb={0.5}>
|
|
End
|
|
</Typography>
|
|
)}
|
|
<TextField
|
|
fullWidth
|
|
label={!showMore ? "End" : ""}
|
|
type={allday ? "date" : "datetime-local"}
|
|
value={allday ? end.split("T")[0] : end}
|
|
onChange={(e) => handleEndChange(e.target.value)}
|
|
error={!!validation.errors.dateTime}
|
|
helperText={validation.errors.dateTime}
|
|
size="small"
|
|
margin="dense"
|
|
InputLabelProps={{ shrink: true }}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</FieldWithLabel>
|
|
|
|
<FieldWithLabel label=" " isExpanded={showMore}>
|
|
<Box display="flex" gap={2} alignItems="center">
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={allday}
|
|
onChange={() => {
|
|
const newAllDay = !allday;
|
|
let newStart = start;
|
|
let newEnd = end;
|
|
|
|
if (newAllDay) {
|
|
// OFF => ON: Save original time before converting to all-day
|
|
if (start.includes("T")) {
|
|
originalTimeRef.current = { start, end };
|
|
}
|
|
|
|
// Convert to date-only format only if both dates are valid
|
|
if (start && end) {
|
|
const startDate = start.includes("T")
|
|
? new Date(start)
|
|
: new Date(start + "T00:00:00");
|
|
const endDate = end.includes("T")
|
|
? new Date(end)
|
|
: new Date(end + "T00:00:00");
|
|
|
|
// Check if dates are valid before proceeding
|
|
if (
|
|
!isNaN(startDate.getTime()) &&
|
|
!isNaN(endDate.getTime())
|
|
) {
|
|
// If same day, extend end to next day
|
|
if (endDate.getDate() === startDate.getDate()) {
|
|
endDate.setDate(startDate.getDate() + 1);
|
|
}
|
|
|
|
const formattedEnd = formatLocalDateTime(endDate);
|
|
if (formattedEnd) {
|
|
newEnd = formattedEnd;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// ON => OFF: Restore original time if available
|
|
if (originalTimeRef.current) {
|
|
// Extract hours/minutes from original time strings
|
|
const originalStartMatch =
|
|
originalTimeRef.current.start.match(/T(\d{2}):(\d{2})/);
|
|
const originalEndMatch =
|
|
originalTimeRef.current.end.match(/T(\d{2}):(\d{2})/);
|
|
|
|
if (originalStartMatch && originalEndMatch) {
|
|
// Parse current date (YYYY-MM-DD)
|
|
const currentDate = start.split("T")[0];
|
|
|
|
// Reconstruct datetime with original time
|
|
newStart = `${currentDate}T${originalStartMatch[1]}:${originalStartMatch[2]}`;
|
|
newEnd = `${currentDate}T${originalEndMatch[1]}:${originalEndMatch[2]}`;
|
|
}
|
|
|
|
// Clear stored time after use
|
|
originalTimeRef.current = null;
|
|
} else if (start) {
|
|
// No original time, use rounded current time
|
|
const startDate = start.includes("T")
|
|
? new Date(start)
|
|
: new Date(start + "T00:00:00");
|
|
|
|
// Check if start date is valid
|
|
if (!isNaN(startDate.getTime())) {
|
|
const currentTime = getRoundedCurrentTime();
|
|
|
|
startDate.setHours(currentTime.getHours());
|
|
startDate.setMinutes(currentTime.getMinutes());
|
|
const formattedStart = formatLocalDateTime(startDate);
|
|
if (formattedStart) {
|
|
newStart = formattedStart;
|
|
}
|
|
|
|
const endDate = new Date(startDate);
|
|
endDate.setMinutes(endDate.getMinutes() + 30);
|
|
const formattedEnd = formatLocalDateTime(endDate);
|
|
if (formattedEnd) {
|
|
newEnd = formattedEnd;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only update local state if no callback (for backwards compatibility)
|
|
if (!onAllDayChange) {
|
|
setStart(newStart);
|
|
setEnd(newEnd);
|
|
setAllDay(newAllDay);
|
|
} else {
|
|
// Let callback handle all state updates to avoid duplicate renders
|
|
handleAllDayChange(newAllDay, newStart, newEnd);
|
|
}
|
|
}}
|
|
/>
|
|
}
|
|
label="All day"
|
|
/>
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={
|
|
showRepeat || (typeOfAction === "solo" && !!repetition?.freq)
|
|
}
|
|
disabled={typeOfAction === "solo"}
|
|
onChange={() => {
|
|
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="Repeat"
|
|
/>
|
|
<TimezoneAutocomplete
|
|
value={timezone}
|
|
onChange={setTimezone}
|
|
zones={timezoneList.zones}
|
|
getTimezoneOffset={timezoneList.getTimezoneOffset}
|
|
showIcon={true}
|
|
width={240}
|
|
size="small"
|
|
placeholder="Select timezone"
|
|
/>
|
|
</Box>
|
|
</FieldWithLabel>
|
|
|
|
{(showRepeat || (typeOfAction === "solo" && repetition?.freq)) && (
|
|
<FieldWithLabel label=" " isExpanded={showMore}>
|
|
<RepeatEvent
|
|
repetition={repetition}
|
|
eventStart={new Date(start)}
|
|
setRepetition={setRepetition}
|
|
isOwn={typeOfAction !== "solo"}
|
|
/>
|
|
</FieldWithLabel>
|
|
)}
|
|
|
|
<FieldWithLabel label="Participants" isExpanded={showMore}>
|
|
<AttendeeSelector attendees={attendees} setAttendees={setAttendees} />
|
|
</FieldWithLabel>
|
|
|
|
<FieldWithLabel label="Video meeting" isExpanded={showMore}>
|
|
<Box display="flex" gap={1} alignItems="center">
|
|
<Button
|
|
startIcon={<VideocamIcon />}
|
|
onClick={handleAddVideoConference}
|
|
size="medium"
|
|
sx={{
|
|
textTransform: "none",
|
|
color: "text.secondary",
|
|
display: hasVideoConference ? "none" : "flex",
|
|
}}
|
|
>
|
|
Add Visio conference
|
|
</Button>
|
|
|
|
{hasVideoConference && meetingLink && (
|
|
<>
|
|
<Button
|
|
startIcon={<VideocamIcon />}
|
|
onClick={() => window.open(meetingLink, "_blank")}
|
|
size="medium"
|
|
variant="contained"
|
|
sx={{
|
|
textTransform: "none",
|
|
mr: 1,
|
|
}}
|
|
>
|
|
Join Visio conference
|
|
</Button>
|
|
<IconButton
|
|
onClick={handleCopyMeetingLink}
|
|
size="small"
|
|
sx={{ color: "primary.main" }}
|
|
aria-label="Copy meeting link"
|
|
title="Copy meeting link"
|
|
>
|
|
<CopyIcon />
|
|
</IconButton>
|
|
<IconButton
|
|
onClick={handleDeleteVideoConference}
|
|
size="small"
|
|
sx={{ color: "error.main" }}
|
|
aria-label="Remove video conference"
|
|
title="Remove video conference"
|
|
>
|
|
<DeleteIcon />
|
|
</IconButton>
|
|
</>
|
|
)}
|
|
</Box>
|
|
</FieldWithLabel>
|
|
|
|
<FieldWithLabel label="Location" isExpanded={showMore}>
|
|
<TextField
|
|
fullWidth
|
|
label={!showMore ? "Location" : ""}
|
|
placeholder="Add location"
|
|
value={location}
|
|
onChange={(e) => setLocation(e.target.value)}
|
|
size="small"
|
|
margin="dense"
|
|
/>
|
|
</FieldWithLabel>
|
|
|
|
<FieldWithLabel label="Calendar" isExpanded={showMore}>
|
|
<FormControl fullWidth margin="dense" size="small">
|
|
{!showMore && (
|
|
<InputLabel id="calendar-select-label">Calendar</InputLabel>
|
|
)}
|
|
<Select
|
|
labelId="calendar-select-label"
|
|
value={calendarid.toString()}
|
|
label={!showMore ? "Calendar" : ""}
|
|
displayEmpty
|
|
onChange={(e: SelectChangeEvent) =>
|
|
handleCalendarChange(e.target.value)
|
|
}
|
|
>
|
|
{CalendarItemList(userPersonnalCalendars)}
|
|
</Select>
|
|
</FormControl>
|
|
</FieldWithLabel>
|
|
|
|
{/* Extended options */}
|
|
{showMore && (
|
|
<>
|
|
<FieldWithLabel label="Notification" isExpanded={showMore}>
|
|
<FormControl fullWidth margin="dense" size="small">
|
|
<Select
|
|
labelId="notification"
|
|
value={alarm}
|
|
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)}
|
|
>
|
|
<MenuItem value={""}>No Notification</MenuItem>
|
|
<MenuItem value={"-PT1M"}>1 minute</MenuItem>
|
|
<MenuItem value={"-PT5M"}>2 minutes</MenuItem>
|
|
<MenuItem value={"-PT10M"}>10 minutes</MenuItem>
|
|
<MenuItem value={"-PT15M"}>15 minutes</MenuItem>
|
|
<MenuItem value={"-PT30M"}>30 minutes</MenuItem>
|
|
<MenuItem value={"-PT1H"}>1 hours</MenuItem>
|
|
<MenuItem value={"-PT2H"}>2 hours</MenuItem>
|
|
<MenuItem value={"-PT5H"}>5 hours</MenuItem>
|
|
<MenuItem value={"-PT12H"}>12 hours</MenuItem>
|
|
<MenuItem value={"-PT1D"}>1 day</MenuItem>
|
|
<MenuItem value={"-PT2D"}>2 days</MenuItem>
|
|
<MenuItem value={"-PT1W"}>1 week</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</FieldWithLabel>
|
|
|
|
<FieldWithLabel label="Show me as" isExpanded={showMore}>
|
|
<FormControl fullWidth margin="dense" size="small">
|
|
<Select
|
|
labelId="busy"
|
|
value={busy}
|
|
onChange={(e: SelectChangeEvent) => setBusy(e.target.value)}
|
|
>
|
|
<MenuItem value={"TRANSPARENT"}>Free</MenuItem>
|
|
<MenuItem value={"OPAQUE"}>Busy </MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</FieldWithLabel>
|
|
|
|
<FieldWithLabel label="Visible to" isExpanded={showMore}>
|
|
<ToggleButtonGroup
|
|
value={eventClass}
|
|
exclusive
|
|
onChange={(e, newValue) => {
|
|
if (newValue !== null) {
|
|
setEventClass(newValue);
|
|
}
|
|
}}
|
|
size="small"
|
|
>
|
|
<ToggleButton value="PUBLIC" sx={{ width: "140px" }}>
|
|
<PublicIcon sx={{ mr: 1, fontSize: "16px" }} />
|
|
All
|
|
</ToggleButton>
|
|
<ToggleButton value="PRIVATE" sx={{ width: "140px" }}>
|
|
<LockIcon sx={{ mr: 1, fontSize: "16px" }} />
|
|
Participants
|
|
</ToggleButton>
|
|
</ToggleButtonGroup>
|
|
</FieldWithLabel>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function formatLocalDateTime(date: Date, timeZone?: string): string {
|
|
// Guard against invalid or undefined dates
|
|
if (!date || isNaN(date.getTime())) {
|
|
return "";
|
|
}
|
|
|
|
if (timeZone) {
|
|
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: false,
|
|
timeZone,
|
|
});
|
|
const formatted = formatter.format(date);
|
|
return formatted.replace(", ", "T");
|
|
}
|
|
|
|
const pad = (n: number) => n.toString().padStart(2, "0");
|
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
|
|
date.getDate()
|
|
)}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
}
|
|
|
|
export function formatDateTimeInTimezone(
|
|
isoString: string,
|
|
timezone: string
|
|
): string {
|
|
// Parse the ISO string as UTC
|
|
const utcDate = new Date(isoString);
|
|
|
|
// Format the date in the target timezone
|
|
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
timeZone: timezone,
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: false,
|
|
});
|
|
|
|
const parts = formatter.formatToParts(utcDate);
|
|
const getValue = (type: string) =>
|
|
parts.find((p) => p.type === type)?.value || "";
|
|
|
|
return `${getValue("year")}-${getValue("month")}-${getValue("day")}T${getValue("hour")}:${getValue("minute")}`;
|
|
}
|