Event form UX improvements:

- Add long date format display (Monday, April 23, 2024)
- Use 24h time format
- Show/hide time fields based on all-day state
- Remove auto-update of end date when start date changes
- Fix validation for all-day events
- Auto-expand for multi-day events
- Set default time when toggling off all-day from all-day slot
This commit is contained in:
lenhanphung
2025-10-30 15:35:47 +07:00
committed by Benoit TELLIER
parent 61358c081a
commit 41b4c41215
7 changed files with 288 additions and 269 deletions
+1 -1
View File
@@ -263,7 +263,7 @@ describe("EventPopover", () => {
const newEvent = { const newEvent = {
title: "Meeting", title: "Meeting",
start: "2025-07-18T00:00:00.000Z", start: "2025-07-18T00:00:00.000Z",
end: "2025-07-19T00:00:00.000Z", end: "2025-07-18T00:00:00.000Z",
allday: false, allday: false,
uid: "6045c603-11ab-43c5-bc30-0641420bb3a8", uid: "6045c603-11ab-43c5-bc30-0641420bb3a8",
description: "Discuss project", description: "Discuss project",
+7 -56
View File
@@ -11,7 +11,6 @@ import {
Select, Select,
SelectChangeEvent, SelectChangeEvent,
TextField, TextField,
Typography,
ToggleButtonGroup, ToggleButtonGroup,
ToggleButton, ToggleButton,
} from "@mui/material"; } from "@mui/material";
@@ -38,12 +37,8 @@ import { FieldWithLabel } from "./components/FieldWithLabel";
import { DateTimeFields } from "./components/DateTimeFields"; import { DateTimeFields } from "./components/DateTimeFields";
import { useAllDayToggle } from "./hooks/useAllDayToggle"; import { useAllDayToggle } from "./hooks/useAllDayToggle";
import { splitDateTime, combineDateTime } from "./utils/dateTimeHelpers"; import { splitDateTime, combineDateTime } from "./utils/dateTimeHelpers";
import { getEndDateForStartChange } from "./utils/dateRules"; import {} from "./utils/dateRules";
import { import {} from "./utils/dateTimeFormatters";
formatLocalDateTime,
formatDateTimeInTimezone,
getRoundedCurrentTime,
} from "./utils/dateTimeFormatters";
import { validateEventForm } from "./utils/formValidation"; import { validateEventForm } from "./utils/formValidation";
interface EventFormFieldsProps { interface EventFormFieldsProps {
@@ -165,10 +160,10 @@ export default function EventFormFields({
const [endTime, setEndTime] = React.useState(""); const [endTime, setEndTime] = React.useState("");
// UI state for showing/hiding end date field // UI state for showing/hiding end date field
const [showEndDate, setShowEndDate] = React.useState(false); // keep local flag if needed for future UI toggles (not used now)
// Use all-day toggle hook // Use all-day toggle hook
const { originalTimeRef, handleAllDayToggle } = useAllDayToggle({ const { handleAllDayToggle } = useAllDayToggle({
allday, allday,
start, start,
end, end,
@@ -178,7 +173,6 @@ export default function EventFormFields({
endTime, endTime,
setStartTime, setStartTime,
setEndTime, setEndTime,
setEndDate,
setStart, setStart,
setEnd, setEnd,
setAllDay, setAllDay,
@@ -250,47 +244,19 @@ export default function EventFormFields({
} }
}, [end]); }, [end]);
// Sync allday prop changes to detect all-day slot clicks
React.useEffect(() => {
if (allday && (!startTime || !endTime)) {
// This is likely from all-day slot click - set time fields empty
setStartTime("");
setEndTime("");
// Mark this as coming from all-day slot for later uncheck logic
originalTimeRef.current = {
start: "",
end: "",
endDate: endDate,
fromAllDaySlot: true,
};
}
}, [allday, startTime, endTime, endDate]);
// Change handlers for 4 separate fields // Change handlers for 4 separate fields
const handleStartDateChange = React.useCallback( const handleStartDateChange = React.useCallback(
(newDate: string) => { (newDate: string) => {
setStartDate(newDate); setStartDate(newDate);
const newStart = combineDateTime(newDate, startTime); const newStart = combineDateTime(newDate, startTime);
// Update start
if (onStartChange) { if (onStartChange) {
onStartChange(newStart); onStartChange(newStart);
} else { } else {
setStart(newStart); setStart(newStart);
} }
// Rule: update end date when start date changes
const nextEndDate = getEndDateForStartChange(newDate, allday);
setEndDate(nextEndDate);
const newEnd = combineDateTime(nextEndDate, endTime || startTime);
if (onEndChange) {
onEndChange(newEnd);
} else {
setEnd(newEnd);
}
}, },
[startTime, endTime, onStartChange, onEndChange, setStart, setEnd, allday] [startTime, onStartChange, setStart]
); );
const handleStartTimeChange = React.useCallback( const handleStartTimeChange = React.useCallback(
@@ -359,19 +325,6 @@ export default function EventFormFields({
onValidationChange?.(validation.isValid); onValidationChange?.(validation.isValid);
}, [validateForm, onValidationChange]); }, [validateForm, onValidationChange]);
// Auto-calculate end date from start date if not already set
React.useEffect(() => {
if (startDate && !endDate) {
setEndDate(startDate);
const newEnd = combineDateTime(startDate, endTime || startTime);
if (onEndChange) {
onEndChange(newEnd);
} else {
setEnd(newEnd);
}
}
}, [startDate, endDate, startTime, endTime, onEndChange, setEnd]);
// Auto-calculate end time from start time (+ 1 hour) if not already set // Auto-calculate end time from start time (+ 1 hour) if not already set
React.useEffect(() => { React.useEffect(() => {
if (startTime && !endTime && !allday) { if (startTime && !endTime && !allday) {
@@ -412,7 +365,6 @@ export default function EventFormFields({
}; };
const handleDeleteVideoConference = () => { const handleDeleteVideoConference = () => {
// Remove video conference footer from description
const updatedDescription = description.replace( const updatedDescription = description.replace(
/\nVisio: https?:\/\/[^\s]+/, /\nVisio: https?:\/\/[^\s]+/,
"" ""
@@ -510,8 +462,8 @@ export default function EventFormFields({
onStartTimeChange={handleStartTimeChange} onStartTimeChange={handleStartTimeChange}
onEndDateChange={handleEndDateChange} onEndDateChange={handleEndDateChange}
onEndTimeChange={handleEndTimeChange} onEndTimeChange={handleEndTimeChange}
showEndDate={showEndDate} showEndDate={showMore || allday}
onToggleEndDate={() => setShowEndDate(!showEndDate)} onToggleEndDate={() => {}}
/> />
</FieldWithLabel> </FieldWithLabel>
@@ -666,7 +618,6 @@ export default function EventFormFields({
</FormControl> </FormControl>
</FieldWithLabel> </FieldWithLabel>
{/* Extended options */}
{showMore && ( {showMore && (
<> <>
<FieldWithLabel label="Notification" isExpanded={showMore}> <FieldWithLabel label="Notification" isExpanded={showMore}>
@@ -1,11 +1,11 @@
import React from "react"; import React from "react";
import { Box, IconButton, Tooltip, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { TimePicker } from "@mui/x-date-pickers/TimePicker"; import { TimePicker } from "@mui/x-date-pickers/TimePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
import moment, { Moment } from "moment"; import moment, { Moment } from "moment";
import { LONG_DATE_FORMAT } from "../utils/dateTimeFormatters";
/** /**
* Props for DateTimeFields component * Props for DateTimeFields component
@@ -49,105 +49,134 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
onEndDateChange, onEndDateChange,
onEndTimeChange, onEndTimeChange,
}) => { }) => {
const isExpanded = showMore;
const shouldShowEndDateNormal = allday || !!showEndDate;
return ( return (
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="en"> <LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="en">
<Box <Box
display="flex" display="flex"
gap={1}
flexDirection="column" flexDirection="column"
sx={{ maxWidth: showMore ? "calc(100% - 145px)" : "100%" }} sx={{ maxWidth: showMore ? "calc(100% - 145px)" : "100%" }}
> >
{/* First row: 4 fields */} {isExpanded ? (
<Box display="flex" gap={1} flexDirection="row" alignItems="center"> <>
{/* Start Date - 30% */} <Box display="flex" gap={1} flexDirection="row" alignItems="center">
<Box sx={{ flexGrow: 0.3, flexBasis: "25%", maxWidth: "150px" }}> <Box sx={{ maxWidth: "280px", width: "45%" }}>
<DatePicker <DatePicker
label="Start Date" label="Start Date"
value={startDate ? moment(startDate) : null} format={LONG_DATE_FORMAT}
onChange={(newValue: Moment | null) => { value={startDate ? moment(startDate) : null}
onStartDateChange(newValue?.format("YYYY-MM-DD") || ""); onChange={(newValue: Moment | null) => {
}} onStartDateChange(newValue?.format("YYYY-MM-DD") || "");
slotProps={{ }}
textField: { slotProps={{
size: "small", textField: {
margin: "dense" as const, size: "small",
fullWidth: true, margin: "dense" as const,
InputLabelProps: { shrink: true }, fullWidth: true,
sx: { width: "100%" }, InputLabelProps: { shrink: true },
inputProps: { "data-testid": "start-date-input" }, sx: { width: "100%" },
}, inputProps: { "data-testid": "start-date-input" },
}} },
/> }}
</Box> />
{/* Start Time - 20% */}
<Box sx={{ flexGrow: 0.2, flexBasis: "25%", maxWidth: "150px" }}>
<TimePicker
label="Start Time"
value={startTime ? moment(startTime, "HH:mm") : null}
onChange={(newValue: Moment | null) => {
onStartTimeChange(newValue?.format("HH:mm") || "");
}}
disabled={allday}
slotProps={{
textField: {
size: "small",
margin: "dense" as const,
fullWidth: true,
InputLabelProps: { shrink: true },
sx: { width: "100%" },
inputProps: { "data-testid": "start-time-input" },
},
}}
/>
</Box>
{/* End Time - 20% */}
<Box sx={{ flexGrow: 0.2, flexBasis: "25%", maxWidth: "150px" }}>
<TimePicker
label="End Time"
value={endTime ? moment(endTime, "HH:mm") : null}
onChange={(newValue: Moment | null) => {
onEndTimeChange(newValue?.format("HH:mm") || "");
}}
disabled={allday}
slotProps={{
textField: {
size: "small",
margin: "dense" as const,
fullWidth: true,
InputLabelProps: { shrink: true },
error: !!validation.errors.dateTime,
sx: { width: "100%" },
inputProps: { "data-testid": "end-time-input" },
},
}}
/>
</Box>
{/* End Date - Conditional rendering */}
<Box sx={{ flexGrow: 0.3, flexBasis: "25%", maxWidth: "150px" }}>
{!showEndDate ? (
// Show "..." button to reveal end date
<Box
display="flex"
justifyContent="flex-start"
sx={{ mt: showMore ? 0 : 0 }}
>
<Tooltip title="Show end date">
<IconButton
size="small"
onClick={onToggleEndDate}
aria-label="Show end date"
>
<MoreHorizIcon />
</IconButton>
</Tooltip>
</Box> </Box>
) : ( {!allday && (
// Show End Date picker (no hide button) <Box sx={{ width: "110px" }}>
<TimePicker
label="Start Time"
ampm={false}
value={startTime ? moment(startTime, "HH:mm") : null}
onChange={(newValue: Moment | null) => {
onStartTimeChange(newValue?.format("HH:mm") || "");
}}
slotProps={{
textField: {
size: "small",
margin: "dense" as const,
fullWidth: true,
InputLabelProps: { shrink: true },
sx: { width: "100%" },
inputProps: { "data-testid": "start-time-input" },
},
}}
/>
</Box>
)}
</Box>
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
<Box sx={{ maxWidth: "280px", width: "45%" }}>
<DatePicker
label="End Date"
format={LONG_DATE_FORMAT}
value={endDate ? moment(endDate) : null}
onChange={(newValue: Moment | null) => {
onEndDateChange(newValue?.format("YYYY-MM-DD") || "");
}}
slotProps={{
textField: {
size: "small",
margin: "dense" as const,
fullWidth: true,
InputLabelProps: { shrink: true },
error: !!validation.errors.dateTime,
sx: { width: "100%" },
inputProps: { "data-testid": "end-date-input" },
},
}}
/>
</Box>
{!allday && (
<Box sx={{ width: "110px" }}>
<TimePicker
label="End Time"
ampm={false}
value={endTime ? moment(endTime, "HH:mm") : null}
onChange={(newValue: Moment | null) => {
onEndTimeChange(newValue?.format("HH:mm") || "");
}}
slotProps={{
textField: {
size: "small",
margin: "dense" as const,
fullWidth: true,
InputLabelProps: { shrink: true },
error: !!validation.errors.dateTime,
sx: { width: "100%" },
inputProps: { "data-testid": "end-time-input" },
},
}}
/>
</Box>
)}
</Box>
</>
) : shouldShowEndDateNormal ? (
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
<Box sx={{ maxWidth: "280px", width: "45%" }}>
<DatePicker
label="Start Date"
format={LONG_DATE_FORMAT}
value={startDate ? moment(startDate) : null}
onChange={(newValue: Moment | null) => {
onStartDateChange(newValue?.format("YYYY-MM-DD") || "");
}}
slotProps={{
textField: {
size: "small",
margin: "dense" as const,
fullWidth: true,
InputLabelProps: { shrink: true },
sx: { width: "100%" },
inputProps: { "data-testid": "start-date-input" },
},
}}
/>
</Box>
<Box sx={{ maxWidth: "280px", width: "45%" }}>
<DatePicker <DatePicker
label="End Date" label="End Date"
format={LONG_DATE_FORMAT}
value={endDate ? moment(endDate) : null} value={endDate ? moment(endDate) : null}
onChange={(newValue: Moment | null) => { onChange={(newValue: Moment | null) => {
onEndDateChange(newValue?.format("YYYY-MM-DD") || ""); onEndDateChange(newValue?.format("YYYY-MM-DD") || "");
@@ -164,18 +193,81 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
}, },
}} }}
/> />
)} </Box>
</Box> </Box>
</Box> ) : (
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
<Box sx={{ maxWidth: "280px", width: "45%" }}>
<DatePicker
label="Start Date"
format={LONG_DATE_FORMAT}
value={startDate ? moment(startDate) : null}
onChange={(newValue: Moment | null) => {
onStartDateChange(newValue?.format("YYYY-MM-DD") || "");
}}
slotProps={{
textField: {
size: "small",
margin: "dense" as const,
fullWidth: true,
InputLabelProps: { shrink: true },
sx: { width: "100%" },
inputProps: { "data-testid": "start-date-input" },
},
}}
/>
</Box>
<Box sx={{ maxWidth: "110px" }}>
<TimePicker
label="Start Time"
ampm={false}
value={startTime ? moment(startTime, "HH:mm") : null}
onChange={(newValue: Moment | null) => {
onStartTimeChange(newValue?.format("HH:mm") || "");
}}
disabled={allday}
slotProps={{
textField: {
size: "small",
margin: "dense" as const,
fullWidth: true,
InputLabelProps: { shrink: true },
sx: { width: "100%" },
inputProps: { "data-testid": "start-time-input" },
},
}}
/>
</Box>
<Box sx={{ maxWidth: "110px" }}>
<TimePicker
label="End Time"
ampm={false}
value={endTime ? moment(endTime, "HH:mm") : null}
onChange={(newValue: Moment | null) => {
onEndTimeChange(newValue?.format("HH:mm") || "");
}}
disabled={allday}
slotProps={{
textField: {
size: "small",
margin: "dense" as const,
fullWidth: true,
InputLabelProps: { shrink: true },
error: !!validation.errors.dateTime,
sx: { width: "100%" },
inputProps: { "data-testid": "end-time-input" },
},
}}
/>
</Box>
</Box>
)}
{/* Second row: Error message - 2 columns 50% each */} {/* Second row: Error message */}
{validation.errors.dateTime && ( {validation.errors.dateTime && (
<Box display="flex" gap={1} flexDirection="row"> <Box display="flex" gap={1} flexDirection="row">
{/* Empty left column - 50% */} <Box sx={{ width: isExpanded ? "0" : allday ? "45%" : "64%" }} />
<Box sx={{ flexGrow: 0.5, flexBasis: "50%" }} /> <Box>
{/* Error message right column - 50% */}
<Box sx={{ flexGrow: 0.5, flexBasis: "50%" }}>
<Typography variant="caption" sx={{ color: "error.main" }}> <Typography variant="caption" sx={{ color: "error.main" }}>
{validation.errors.dateTime} {validation.errors.dateTime}
</Typography> </Typography>
+21 -90
View File
@@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import { combineDateTime } from "../utils/dateTimeHelpers"; import { combineDateTime } from "../utils/dateTimeHelpers";
import { getEndDateForToggle } from "../utils/dateRules";
import { getRoundedCurrentTime } from "../utils/dateTimeFormatters"; import { getRoundedCurrentTime } from "../utils/dateTimeFormatters";
/** /**
@@ -16,7 +15,6 @@ export interface AllDayToggleParams {
endTime: string; endTime: string;
setStartTime: (time: string) => void; setStartTime: (time: string) => void;
setEndTime: (time: string) => void; setEndTime: (time: string) => void;
setEndDate: (date: string) => void;
setStart: (start: string) => void; setStart: (start: string) => void;
setEnd: (end: string) => void; setEnd: (end: string) => void;
setAllDay: (allday: boolean) => void; setAllDay: (allday: boolean) => void;
@@ -53,7 +51,6 @@ export function useAllDayToggle(
endTime, endTime,
setStartTime, setStartTime,
setEndTime, setEndTime,
setEndDate,
setStart, setStart,
setEnd, setEnd,
setAllDay, setAllDay,
@@ -73,97 +70,32 @@ export function useAllDayToggle(
let newStart = start; let newStart = start;
let newEnd = end; let newEnd = end;
if (newAllDay) { if (!newAllDay) {
// OFF => ON: Save original time AND original end date const hasTimeParts = start.includes("T") && end.includes("T");
if (start.includes("T")) { if (!hasTimeParts && !startTime && !endTime) {
originalTimeRef.current = { const now = new Date();
start: startTime, now.setSeconds(0);
end: endTime, now.setMilliseconds(0);
endDate: endDate, // Save original end date const nextHour = new Date(now);
}; nextHour.setMinutes(0);
} nextHour.setHours(now.getHours() + 1);
// Reset time to empty, keep only date part const startHours = String(nextHour.getHours()).padStart(2, "0");
setStartTime(""); const startMinutes = String(nextHour.getMinutes()).padStart(2, "0");
setEndTime(""); const startTimeStr = `${startHours}:${startMinutes}`;
newStart = startDate;
const nextEndDate = getEndDateForToggle({
nextAllDay: true,
fromAllDaySlot: !!originalTimeRef.current?.fromAllDaySlot,
startDate,
previousEndDate: endDate,
originalEndDate: originalTimeRef.current?.endDate,
});
newEnd = nextEndDate;
setEndDate(nextEndDate);
} else {
// ON => OFF: Restore original time AND original end date
if (originalTimeRef.current) {
// Check if this came from all-day slot click
if (originalTimeRef.current.fromAllDaySlot) {
// From all-day slot: set endDate = startDate, use default time
const currentTime = getRoundedCurrentTime();
const hours = String(currentTime.getHours()).padStart(2, "0");
const minutes = String(currentTime.getMinutes()).padStart(2, "0");
const timeStr = `${hours}:${minutes}`;
newStart = combineDateTime(startDate, timeStr); const endHourDate = new Date(nextHour);
endHourDate.setHours(endHourDate.getHours() + 1);
// End time = start time + 1 hour const endHours = String(endHourDate.getHours()).padStart(2, "0");
const endTimeDate = new Date(currentTime); const endMinutes = String(endHourDate.getMinutes()).padStart(2, "0");
endTimeDate.setHours(endTimeDate.getHours() + 1);
const endHours = String(endTimeDate.getHours()).padStart(2, "0");
const endMinutes = String(endTimeDate.getMinutes()).padStart(2, "0");
const endTimeStr = `${endHours}:${endMinutes}`;
newEnd = combineDateTime(startDate, endTimeStr); // Use startDate as endDate
// Update internal states
setStartTime(timeStr);
setEndTime(endTimeStr);
setEndDate(startDate); // Set endDate = startDate
} else {
// Normal case: restore original time AND original end date
const restoredEndDate = getEndDateForToggle({
nextAllDay: false,
startDate,
previousEndDate: endDate,
originalEndDate: originalTimeRef.current.endDate,
});
newStart = combineDateTime(startDate, originalTimeRef.current.start);
newEnd = combineDateTime(
restoredEndDate,
originalTimeRef.current.end
);
// Update internal states
setStartTime(originalTimeRef.current.start);
setEndTime(originalTimeRef.current.end);
setEndDate(restoredEndDate);
}
originalTimeRef.current = null;
} else {
// No original time: use rounded current time with 1 hour duration
const currentTime = getRoundedCurrentTime();
const hours = String(currentTime.getHours()).padStart(2, "0");
const minutes = String(currentTime.getMinutes()).padStart(2, "0");
const timeStr = `${hours}:${minutes}`;
newStart = combineDateTime(startDate, timeStr);
// End time = start time + 1 hour (not 30 mins)
const endTimeDate = new Date(currentTime);
endTimeDate.setHours(endTimeDate.getHours() + 1);
const endHours = String(endTimeDate.getHours()).padStart(2, "0");
const endMinutes = String(endTimeDate.getMinutes()).padStart(2, "0");
const endTimeStr = `${endHours}:${endMinutes}`; const endTimeStr = `${endHours}:${endMinutes}`;
newEnd = combineDateTime(endDate, endTimeStr); const startDateOnly = start.split("T")[0] || startDate;
const endDateOnly = end.split("T")[0] || endDate || startDateOnly;
newStart = combineDateTime(startDateOnly, startTimeStr);
newEnd = combineDateTime(endDateOnly, endTimeStr);
// Update internal states setStartTime(startTimeStr);
setStartTime(timeStr);
setEndTime(endTimeStr); setEndTime(endTimeStr);
} }
} }
@@ -185,7 +117,6 @@ export function useAllDayToggle(
endTime, endTime,
setStartTime, setStartTime,
setEndTime, setEndTime,
setEndDate,
setStart, setStart,
setEnd, setEnd,
setAllDay, setAllDay,
@@ -78,3 +78,6 @@ export function getRoundedCurrentTime(): Date {
now.setMilliseconds(0); now.setMilliseconds(0);
return now; return now;
} }
/** Long date display format for date pickers */
export const LONG_DATE_FORMAT = "dddd, MMMM D, YYYY";
+29 -14
View File
@@ -54,7 +54,7 @@ export function validateEventForm(params: ValidationParams): ValidationResult {
// Validate start time (if not all-day) // Validate start time (if not all-day)
else if (!allday && (!startTime || startTime.trim() === "")) { else if (!allday && (!startTime || startTime.trim() === "")) {
isDateTimeValid = false; isDateTimeValid = false;
dateTimeError = "Start time is required"; dateTimeError = "Start time and End time is required";
} }
// Validate end date // Validate end date
else if (!endDate || endDate.trim() === "") { else if (!endDate || endDate.trim() === "") {
@@ -66,21 +66,36 @@ export function validateEventForm(params: ValidationParams): ValidationResult {
isDateTimeValid = false; isDateTimeValid = false;
dateTimeError = "End time is required"; dateTimeError = "End time is required";
} }
// Validate end > start // Validate end vs start
else { else {
const startDateTime = allday if (allday) {
? new Date(startDate + "T00:00:00") const toLocalDate = (ymd: string) => {
: new Date(combineDateTime(startDate, startTime)); const [y, m, d] = ymd.split("-").map((v) => parseInt(v, 10));
const endDateTime = allday if (!y || !m || !d) return new Date(NaN);
? new Date(endDate + "T00:00:00") return new Date(y, m - 1, d);
: new Date(combineDateTime(endDate, endTime)); };
if (isNaN(startDateTime.getTime()) || isNaN(endDateTime.getTime())) { const startOnly = toLocalDate(startDate);
isDateTimeValid = false; const endOnly = toLocalDate(endDate);
dateTimeError = "Invalid date/time";
} else if (endDateTime <= startDateTime) { if (isNaN(startOnly.getTime()) || isNaN(endOnly.getTime())) {
isDateTimeValid = false; isDateTimeValid = false;
dateTimeError = "End time must be after start time"; dateTimeError = "Invalid date";
} else if (endOnly < startOnly) {
isDateTimeValid = false;
dateTimeError = "End date must be on or after start date";
}
} else {
const startDateTime = new Date(combineDateTime(startDate, startTime));
const endDateTime = new Date(combineDateTime(endDate, endTime));
if (isNaN(startDateTime.getTime()) || isNaN(endDateTime.getTime())) {
isDateTimeValid = false;
dateTimeError = "Invalid date/time";
} else if (endDateTime <= startDateTime) {
isDateTimeValid = false;
dateTimeError = "End time must be after start time";
}
} }
} }
+36 -9
View File
@@ -221,12 +221,21 @@ function EventPopover({
: formatLocalDateTime(selectedRange.end); : formatLocalDateTime(selectedRange.end);
// Use the string values directly to preserve the displayed time // Use the string values directly to preserve the displayed time
setStart( const startValue = selectedRange.allDay
selectedRange.allDay ? startStr.split("T")[0] : startStr.slice(0, 16) // YYYY-MM-DDTHH:mm ? startStr.split("T")[0]
); : startStr.slice(0, 16); // YYYY-MM-DDTHH:mm
setEnd( const endValue = selectedRange.allDay
selectedRange.allDay ? endStr.split("T")[0] : endStr.slice(0, 16) ? endStr.split("T")[0]
); : endStr.slice(0, 16);
setStart(startValue);
setEnd(endValue);
// If start date != end date, open extended mode
const startDateOnly = startValue.slice(0, 10);
const endDateOnly = endValue.slice(0, 10);
if (startDateOnly !== endDateOnly) {
setShowMore(true);
}
} else { } else {
// Fallback: format Date objects using local time components // Fallback: format Date objects using local time components
// Only set if both start and end are valid // Only set if both start and end are valid
@@ -234,6 +243,13 @@ function EventPopover({
const formattedEnd = formatLocalDateTime(selectedRange.end); const formattedEnd = formatLocalDateTime(selectedRange.end);
if (formattedStart) setStart(formattedStart); if (formattedStart) setStart(formattedStart);
if (formattedEnd) setEnd(formattedEnd); if (formattedEnd) setEnd(formattedEnd);
if (formattedStart && formattedEnd) {
const startDateOnly = formattedStart.slice(0, 10);
const endDateOnly = formattedEnd.slice(0, 10);
if (startDateOnly !== endDateOnly) {
setShowMore(true);
}
}
} }
} else { } else {
// No valid selectedRange - use default times // No valid selectedRange - use default times
@@ -479,7 +495,7 @@ function EventPopover({
calId: calList[calendarid].id, calId: calList[calendarid].id,
title, title,
URL: `/calendars/${calList[calendarid].id}/${newEventUID}.ics`, URL: `/calendars/${calList[calendarid].id}/${newEventUID}.ics`,
start: new Date(start).toISOString(), start: "",
allday, allday,
uid: newEventUID, uid: newEventUID,
description, description,
@@ -503,8 +519,19 @@ function EventPopover({
alarm: { trigger: alarm, action: "EMAIL" }, alarm: { trigger: alarm, action: "EMAIL" },
x_openpass_videoconference: meetingLink || undefined, x_openpass_videoconference: meetingLink || undefined,
}; };
if (end) {
newEvent.end = new Date(end).toISOString(); if (allday) {
const startDateOnly = (start || "").split("T")[0];
const endDateOnlyBase = (end || start || "").split("T")[0];
const startDateObj = new Date(`${startDateOnly}T00:00:00`);
const endDateObj = new Date(`${endDateOnlyBase}T00:00:00`);
newEvent.start = startDateObj.toISOString();
newEvent.end = endDateObj.toISOString();
} else {
newEvent.start = new Date(start).toISOString();
if (end) {
newEvent.end = new Date(end).toISOString();
}
} }
if (attendees.length > 0) { if (attendees.length > 0) {