feat: improve date/time fields UI and logic
- Change 'Start Date' label to 'Date' when only single date field is visible in normal mode - Fix all-day slot display: show correct start/end dates for single click and multi-day drag - Update API save logic: add +1 day to end date for all-day events (API requirement) - Migrate from moment to dayjs for date handling with AdapterDayjs - Remove manual date clamping logic - MUI/dayjs handles invalid dates automatically - Add validation guard to prevent invalid dates from clearing field values - Update test case to match new all-day event API behavior
This commit is contained in:
committed by
Benoit TELLIER
parent
41b4c41215
commit
c1f37e3ebd
@@ -312,11 +312,15 @@ describe("EventPopover", () => {
|
|||||||
new Date(receivedPayload.newEvent.start)
|
new Date(receivedPayload.newEvent.start)
|
||||||
).split("T")[0]
|
).split("T")[0]
|
||||||
).toBe(formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.start)).split("T")[0]);
|
).toBe(formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.start)).split("T")[0]);
|
||||||
|
|
||||||
|
const expectedAllDayEnd = new Date(newEvent.end);
|
||||||
|
expectedAllDayEnd.setUTCDate(expectedAllDayEnd.getUTCDate() + 1);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
formatDateToYYYYMMDDTHHMMSS(
|
formatDateToYYYYMMDDTHHMMSS(
|
||||||
new Date(receivedPayload.newEvent.end || new Date())
|
new Date(receivedPayload.newEvent.end || new Date())
|
||||||
).split("T")[0]
|
).split("T")[0]
|
||||||
).toBe(formatDateToYYYYMMDDTHHMMSS(new Date(newEvent.end)).split("T")[0]);
|
).toBe(formatDateToYYYYMMDDTHHMMSS(expectedAllDayEnd).split("T")[0]);
|
||||||
expect(receivedPayload.newEvent.location).toBe(newEvent.location);
|
expect(receivedPayload.newEvent.location).toBe(newEvent.location);
|
||||||
expect(receivedPayload.newEvent.organizer).toEqual(newEvent.organizer);
|
expect(receivedPayload.newEvent.organizer).toEqual(newEvent.organizer);
|
||||||
expect(receivedPayload.newEvent.color).toEqual(
|
expect(receivedPayload.newEvent.color).toEqual(
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ interface EventFormFieldsProps {
|
|||||||
// Validation
|
// Validation
|
||||||
onValidationChange?: (isValid: boolean) => void;
|
onValidationChange?: (isValid: boolean) => void;
|
||||||
showValidationErrors?: boolean;
|
showValidationErrors?: boolean;
|
||||||
|
onHasEndDateChangedChange?: (has: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EventFormFields({
|
export default function EventFormFields({
|
||||||
@@ -152,6 +153,7 @@ export default function EventFormFields({
|
|||||||
onCalendarChange,
|
onCalendarChange,
|
||||||
onValidationChange,
|
onValidationChange,
|
||||||
showValidationErrors = false,
|
showValidationErrors = false,
|
||||||
|
onHasEndDateChangedChange,
|
||||||
}: EventFormFieldsProps) {
|
}: EventFormFieldsProps) {
|
||||||
// Internal state for 4 separate fields
|
// Internal state for 4 separate fields
|
||||||
const [startDate, setStartDate] = React.useState("");
|
const [startDate, setStartDate] = React.useState("");
|
||||||
@@ -159,8 +161,20 @@ export default function EventFormFields({
|
|||||||
const [endDate, setEndDate] = React.useState("");
|
const [endDate, setEndDate] = React.useState("");
|
||||||
const [endTime, setEndTime] = React.useState("");
|
const [endTime, setEndTime] = React.useState("");
|
||||||
|
|
||||||
// UI state for showing/hiding end date field
|
// Track if user has manually changed end date in extended mode
|
||||||
// keep local flag if needed for future UI toggles (not used now)
|
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]);
|
||||||
|
|
||||||
// Use all-day toggle hook
|
// Use all-day toggle hook
|
||||||
const { handleAllDayToggle } = useAllDayToggle({
|
const { handleAllDayToggle } = useAllDayToggle({
|
||||||
@@ -275,6 +289,10 @@ export default function EventFormFields({
|
|||||||
const handleEndDateChange = React.useCallback(
|
const handleEndDateChange = React.useCallback(
|
||||||
(newDate: string) => {
|
(newDate: string) => {
|
||||||
setEndDate(newDate);
|
setEndDate(newDate);
|
||||||
|
// Track if user changed end date in extended mode
|
||||||
|
if (showMore) {
|
||||||
|
setHasEndDateChanged(true);
|
||||||
|
}
|
||||||
const newEnd = combineDateTime(newDate, endTime);
|
const newEnd = combineDateTime(newDate, endTime);
|
||||||
if (onEndChange) {
|
if (onEndChange) {
|
||||||
onEndChange(newEnd);
|
onEndChange(newEnd);
|
||||||
@@ -282,7 +300,7 @@ export default function EventFormFields({
|
|||||||
setEnd(newEnd);
|
setEnd(newEnd);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[endTime, onEndChange, setEnd]
|
[endTime, onEndChange, setEnd, showMore]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEndTimeChange = React.useCallback(
|
const handleEndTimeChange = React.useCallback(
|
||||||
@@ -308,6 +326,8 @@ export default function EventFormFields({
|
|||||||
endTime,
|
endTime,
|
||||||
allday,
|
allday,
|
||||||
showValidationErrors,
|
showValidationErrors,
|
||||||
|
hasEndDateChanged,
|
||||||
|
showMore,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
title,
|
title,
|
||||||
@@ -317,6 +337,8 @@ export default function EventFormFields({
|
|||||||
endTime,
|
endTime,
|
||||||
allday,
|
allday,
|
||||||
showValidationErrors,
|
showValidationErrors,
|
||||||
|
hasEndDateChanged,
|
||||||
|
showMore,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Notify parent about validation changes
|
// Notify parent about validation changes
|
||||||
@@ -457,6 +479,7 @@ export default function EventFormFields({
|
|||||||
endTime={endTime}
|
endTime={endTime}
|
||||||
allday={allday}
|
allday={allday}
|
||||||
showMore={showMore}
|
showMore={showMore}
|
||||||
|
hasEndDateChanged={hasEndDateChanged}
|
||||||
validation={validation}
|
validation={validation}
|
||||||
onStartDateChange={handleStartDateChange}
|
onStartDateChange={handleStartDateChange}
|
||||||
onStartTimeChange={handleStartTimeChange}
|
onStartTimeChange={handleStartTimeChange}
|
||||||
@@ -606,7 +629,7 @@ export default function EventFormFields({
|
|||||||
)}
|
)}
|
||||||
<Select
|
<Select
|
||||||
labelId="calendar-select-label"
|
labelId="calendar-select-label"
|
||||||
value={calendarid}
|
value={calendarid ?? ""}
|
||||||
label={!showMore ? "Calendar" : ""}
|
label={!showMore ? "Calendar" : ""}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
onChange={(e: SelectChangeEvent) =>
|
onChange={(e: SelectChangeEvent) =>
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ 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 { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||||
import moment, { Moment } from "moment";
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
|
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||||
import { LONG_DATE_FORMAT } from "../utils/dateTimeFormatters";
|
import { LONG_DATE_FORMAT } from "../utils/dateTimeFormatters";
|
||||||
|
|
||||||
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for DateTimeFields component
|
* Props for DateTimeFields component
|
||||||
*/
|
*/
|
||||||
@@ -17,6 +20,7 @@ export interface DateTimeFieldsProps {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
allday: boolean;
|
allday: boolean;
|
||||||
showMore: boolean;
|
showMore: boolean;
|
||||||
|
hasEndDateChanged: boolean;
|
||||||
showEndDate: boolean;
|
showEndDate: boolean;
|
||||||
onToggleEndDate: () => void;
|
onToggleEndDate: () => void;
|
||||||
validation: {
|
validation: {
|
||||||
@@ -41,6 +45,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
endTime,
|
endTime,
|
||||||
allday,
|
allday,
|
||||||
showMore,
|
showMore,
|
||||||
|
hasEndDateChanged,
|
||||||
showEndDate,
|
showEndDate,
|
||||||
onToggleEndDate,
|
onToggleEndDate,
|
||||||
validation,
|
validation,
|
||||||
@@ -51,23 +56,33 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const isExpanded = showMore;
|
const isExpanded = showMore;
|
||||||
const shouldShowEndDateNormal = allday || !!showEndDate;
|
const shouldShowEndDateNormal = allday || !!showEndDate;
|
||||||
|
const shouldShowFullFieldsInNormal = !allday && hasEndDateChanged;
|
||||||
|
const showSingleDateField =
|
||||||
|
!isExpanded && !shouldShowEndDateNormal && !shouldShowFullFieldsInNormal;
|
||||||
|
const startDateLabel = showSingleDateField ? "Date" : "Start Date";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="en">
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="en">
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
sx={{ maxWidth: showMore ? "calc(100% - 145px)" : "100%" }}
|
sx={{ maxWidth: showMore ? "calc(100% - 145px)" : "100%" }}
|
||||||
>
|
>
|
||||||
{isExpanded ? (
|
{isExpanded || shouldShowFullFieldsInNormal ? (
|
||||||
<>
|
<>
|
||||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||||
<Box sx={{ maxWidth: "280px", width: "45%" }}>
|
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="Start Date"
|
label="Start Date"
|
||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={startDate ? moment(startDate) : null}
|
value={startDate ? dayjs(startDate) : null}
|
||||||
onChange={(newValue: Moment | null) => {
|
onChange={(newValue) => {
|
||||||
onStartDateChange(newValue?.format("YYYY-MM-DD") || "");
|
const value = newValue as Dayjs | null;
|
||||||
|
if (!value || !value.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formatted = value.format("YYYY-MM-DD");
|
||||||
|
onStartDateChange(formatted);
|
||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: {
|
textField: {
|
||||||
@@ -86,9 +101,14 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
<TimePicker
|
<TimePicker
|
||||||
label="Start Time"
|
label="Start Time"
|
||||||
ampm={false}
|
ampm={false}
|
||||||
value={startTime ? moment(startTime, "HH:mm") : null}
|
value={startTime ? dayjs(startTime, "HH:mm") : null}
|
||||||
onChange={(newValue: Moment | null) => {
|
onChange={(newValue) => {
|
||||||
onStartTimeChange(newValue?.format("HH:mm") || "");
|
const value = newValue as Dayjs | null;
|
||||||
|
if (!value || !value.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formatted = value?.format("HH:mm") || "";
|
||||||
|
onStartTimeChange(formatted);
|
||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: {
|
textField: {
|
||||||
@@ -105,13 +125,18 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||||
<Box sx={{ maxWidth: "280px", width: "45%" }}>
|
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="End Date"
|
label="End Date"
|
||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={endDate ? moment(endDate) : null}
|
value={endDate ? dayjs(endDate) : null}
|
||||||
onChange={(newValue: Moment | null) => {
|
onChange={(newValue) => {
|
||||||
onEndDateChange(newValue?.format("YYYY-MM-DD") || "");
|
const value = newValue as Dayjs | null;
|
||||||
|
if (!value || !value.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formatted = value.format("YYYY-MM-DD");
|
||||||
|
onEndDateChange(formatted);
|
||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: {
|
textField: {
|
||||||
@@ -131,9 +156,14 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
<TimePicker
|
<TimePicker
|
||||||
label="End Time"
|
label="End Time"
|
||||||
ampm={false}
|
ampm={false}
|
||||||
value={endTime ? moment(endTime, "HH:mm") : null}
|
value={endTime ? dayjs(endTime, "HH:mm") : null}
|
||||||
onChange={(newValue: Moment | null) => {
|
onChange={(newValue) => {
|
||||||
onEndTimeChange(newValue?.format("HH:mm") || "");
|
const value = newValue as Dayjs | null;
|
||||||
|
if (!value || !value.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formatted = value?.format("HH:mm") || "";
|
||||||
|
onEndTimeChange(formatted);
|
||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: {
|
textField: {
|
||||||
@@ -153,13 +183,18 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
</>
|
</>
|
||||||
) : shouldShowEndDateNormal ? (
|
) : shouldShowEndDateNormal ? (
|
||||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||||
<Box sx={{ maxWidth: "280px", width: "45%" }}>
|
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="Start Date"
|
label="Start Date"
|
||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={startDate ? moment(startDate) : null}
|
value={startDate ? dayjs(startDate) : null}
|
||||||
onChange={(newValue: Moment | null) => {
|
onChange={(newValue) => {
|
||||||
onStartDateChange(newValue?.format("YYYY-MM-DD") || "");
|
const value = newValue as Dayjs | null;
|
||||||
|
if (!value || !value.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formatted = value.format("YYYY-MM-DD");
|
||||||
|
onStartDateChange(formatted);
|
||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: {
|
textField: {
|
||||||
@@ -173,13 +208,18 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ maxWidth: "280px", width: "45%" }}>
|
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="End Date"
|
label="End Date"
|
||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={endDate ? moment(endDate) : null}
|
value={endDate ? dayjs(endDate) : null}
|
||||||
onChange={(newValue: Moment | null) => {
|
onChange={(newValue) => {
|
||||||
onEndDateChange(newValue?.format("YYYY-MM-DD") || "");
|
const value = newValue as Dayjs | null;
|
||||||
|
if (!value || !value.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formatted = value.format("YYYY-MM-DD");
|
||||||
|
onEndDateChange(formatted);
|
||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: {
|
textField: {
|
||||||
@@ -197,13 +237,18 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||||
<Box sx={{ maxWidth: "280px", width: "45%" }}>
|
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="Start Date"
|
label={startDateLabel}
|
||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={startDate ? moment(startDate) : null}
|
value={startDate ? dayjs(startDate) : null}
|
||||||
onChange={(newValue: Moment | null) => {
|
onChange={(newValue) => {
|
||||||
onStartDateChange(newValue?.format("YYYY-MM-DD") || "");
|
const value = newValue as Dayjs | null;
|
||||||
|
if (!value || !value.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formatted = value.format("YYYY-MM-DD");
|
||||||
|
onStartDateChange(formatted);
|
||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: {
|
textField: {
|
||||||
@@ -221,9 +266,14 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
<TimePicker
|
<TimePicker
|
||||||
label="Start Time"
|
label="Start Time"
|
||||||
ampm={false}
|
ampm={false}
|
||||||
value={startTime ? moment(startTime, "HH:mm") : null}
|
value={startTime ? dayjs(startTime, "HH:mm") : null}
|
||||||
onChange={(newValue: Moment | null) => {
|
onChange={(newValue) => {
|
||||||
onStartTimeChange(newValue?.format("HH:mm") || "");
|
const value = newValue as Dayjs | null;
|
||||||
|
if (!value || !value.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formatted = value?.format("HH:mm") || "";
|
||||||
|
onStartTimeChange(formatted);
|
||||||
}}
|
}}
|
||||||
disabled={allday}
|
disabled={allday}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@@ -242,9 +292,14 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
<TimePicker
|
<TimePicker
|
||||||
label="End Time"
|
label="End Time"
|
||||||
ampm={false}
|
ampm={false}
|
||||||
value={endTime ? moment(endTime, "HH:mm") : null}
|
value={endTime ? dayjs(endTime, "HH:mm") : null}
|
||||||
onChange={(newValue: Moment | null) => {
|
onChange={(newValue) => {
|
||||||
onEndTimeChange(newValue?.format("HH:mm") || "");
|
const value = newValue as Dayjs | null;
|
||||||
|
if (!value || !value.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formatted = value?.format("HH:mm") || "";
|
||||||
|
onEndTimeChange(formatted);
|
||||||
}}
|
}}
|
||||||
disabled={allday}
|
disabled={allday}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@@ -266,7 +321,11 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
{/* Second row: Error message */}
|
{/* Second row: Error message */}
|
||||||
{validation.errors.dateTime && (
|
{validation.errors.dateTime && (
|
||||||
<Box display="flex" gap={1} flexDirection="row">
|
<Box display="flex" gap={1} flexDirection="row">
|
||||||
<Box sx={{ width: isExpanded ? "0" : allday ? "45%" : "64%" }} />
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "1%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="caption" sx={{ color: "error.main" }}>
|
<Typography variant="caption" sx={{ color: "error.main" }}>
|
||||||
{validation.errors.dateTime}
|
{validation.errors.dateTime}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export interface ValidationParams {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
allday: boolean;
|
allday: boolean;
|
||||||
showValidationErrors: boolean;
|
showValidationErrors: boolean;
|
||||||
|
hasEndDateChanged?: boolean;
|
||||||
|
showMore?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,6 +40,8 @@ export function validateEventForm(params: ValidationParams): ValidationResult {
|
|||||||
endTime,
|
endTime,
|
||||||
allday,
|
allday,
|
||||||
showValidationErrors,
|
showValidationErrors,
|
||||||
|
hasEndDateChanged = false,
|
||||||
|
showMore = false,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const isTitleValid = title.trim().length > 0;
|
const isTitleValid = title.trim().length > 0;
|
||||||
@@ -46,6 +50,11 @@ export function validateEventForm(params: ValidationParams): ValidationResult {
|
|||||||
let isDateTimeValid = true;
|
let isDateTimeValid = true;
|
||||||
let dateTimeError = "";
|
let dateTimeError = "";
|
||||||
|
|
||||||
|
// Determine which fields are visible based on UI mode
|
||||||
|
const showFullFields = showMore || hasEndDateChanged;
|
||||||
|
const showTimeOnly = !allday && !showFullFields;
|
||||||
|
const showDateOnly = allday;
|
||||||
|
|
||||||
// Validate start date
|
// Validate start date
|
||||||
if (!startDate || startDate.trim() === "") {
|
if (!startDate || startDate.trim() === "") {
|
||||||
isDateTimeValid = false;
|
isDateTimeValid = false;
|
||||||
@@ -54,21 +63,78 @@ 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 and End time is required";
|
dateTimeError = "Start time is required";
|
||||||
}
|
}
|
||||||
// Validate end date
|
// Validate end fields based on UI mode
|
||||||
else if (!endDate || endDate.trim() === "") {
|
else if (showFullFields) {
|
||||||
isDateTimeValid = false;
|
// 4 fields mode: validate both end date and end time
|
||||||
dateTimeError = "End date is required";
|
if (!endDate || endDate.trim() === "") {
|
||||||
}
|
isDateTimeValid = false;
|
||||||
// Validate end time (if not all-day)
|
dateTimeError = "End date is required";
|
||||||
else if (!allday && (!endTime || endTime.trim() === "")) {
|
} else if (!allday && (!endTime || endTime.trim() === "")) {
|
||||||
isDateTimeValid = false;
|
isDateTimeValid = false;
|
||||||
dateTimeError = "End time is required";
|
dateTimeError = "End time is required";
|
||||||
}
|
} else {
|
||||||
// Validate end vs start
|
// Validate total datetime
|
||||||
else {
|
if (allday) {
|
||||||
if (allday) {
|
const toLocalDate = (ymd: string) => {
|
||||||
|
const [y, m, d] = ymd.split("-").map((v) => parseInt(v, 10));
|
||||||
|
if (!y || !m || !d) return new Date(NaN);
|
||||||
|
return new Date(y, m - 1, d);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startOnly = toLocalDate(startDate);
|
||||||
|
const endOnly = toLocalDate(endDate);
|
||||||
|
|
||||||
|
if (isNaN(startOnly.getTime()) || isNaN(endOnly.getTime())) {
|
||||||
|
isDateTimeValid = false;
|
||||||
|
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 date/time must be after start date/time";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (showTimeOnly) {
|
||||||
|
// 3 fields mode: validate time only (end time > start time, same day)
|
||||||
|
if (!endTime || endTime.trim() === "") {
|
||||||
|
isDateTimeValid = false;
|
||||||
|
dateTimeError = "End time is required";
|
||||||
|
} else {
|
||||||
|
// Compare times only (same day is assumed)
|
||||||
|
const startTimeParts = startTime.split(":");
|
||||||
|
const endTimeParts = endTime.split(":");
|
||||||
|
if (startTimeParts.length === 2 && endTimeParts.length === 2) {
|
||||||
|
const startMinutes =
|
||||||
|
parseInt(startTimeParts[0]) * 60 + parseInt(startTimeParts[1]);
|
||||||
|
const endMinutes =
|
||||||
|
parseInt(endTimeParts[0]) * 60 + parseInt(endTimeParts[1]);
|
||||||
|
if (endMinutes <= startMinutes) {
|
||||||
|
isDateTimeValid = false;
|
||||||
|
dateTimeError = "End time must be after start time";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isDateTimeValid = false;
|
||||||
|
dateTimeError = "Invalid time format";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (showDateOnly) {
|
||||||
|
// 2 fields mode: validate date only
|
||||||
|
if (!endDate || endDate.trim() === "") {
|
||||||
|
isDateTimeValid = false;
|
||||||
|
dateTimeError = "End date is required";
|
||||||
|
} else {
|
||||||
const toLocalDate = (ymd: string) => {
|
const toLocalDate = (ymd: string) => {
|
||||||
const [y, m, d] = ymd.split("-").map((v) => parseInt(v, 10));
|
const [y, m, d] = ymd.split("-").map((v) => parseInt(v, 10));
|
||||||
if (!y || !m || !d) return new Date(NaN);
|
if (!y || !m || !d) return new Date(NaN);
|
||||||
@@ -85,17 +151,6 @@ export function validateEventForm(params: ValidationParams): ValidationResult {
|
|||||||
isDateTimeValid = false;
|
isDateTimeValid = false;
|
||||||
dateTimeError = "End date must be on or after start date";
|
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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
formatLocalDateTime,
|
formatLocalDateTime,
|
||||||
formatDateTimeInTimezone,
|
formatDateTimeInTimezone,
|
||||||
} from "../../components/Event/utils/dateTimeFormatters";
|
} from "../../components/Event/utils/dateTimeFormatters";
|
||||||
|
import { addDays } from "../../components/Event/utils/dateRules";
|
||||||
|
|
||||||
function EventPopover({
|
function EventPopover({
|
||||||
anchorEl,
|
anchorEl,
|
||||||
@@ -96,7 +97,7 @@ function EventPopover({
|
|||||||
const [start, setStart] = useState(event?.start ? event.start : "");
|
const [start, setStart] = useState(event?.start ? event.start : "");
|
||||||
const [end, setEnd] = useState(event?.end ? event.end : "");
|
const [end, setEnd] = useState(event?.end ? event.end : "");
|
||||||
const [calendarid, setCalendarid] = useState(
|
const [calendarid, setCalendarid] = useState(
|
||||||
event?.calId ?? userPersonnalCalendars[0]?.id
|
event?.calId ?? userPersonnalCalendars[0]?.id ?? ""
|
||||||
);
|
);
|
||||||
const [allday, setAllDay] = useState(event?.allday ?? false);
|
const [allday, setAllDay] = useState(event?.allday ?? false);
|
||||||
const [repetition, setRepetition] = useState<RepetitionObject>(
|
const [repetition, setRepetition] = useState<RepetitionObject>(
|
||||||
@@ -121,6 +122,7 @@ function EventPopover({
|
|||||||
);
|
);
|
||||||
const [isFormValid, setIsFormValid] = useState(false);
|
const [isFormValid, setIsFormValid] = useState(false);
|
||||||
const [showValidationErrors, setShowValidationErrors] = useState(false);
|
const [showValidationErrors, setShowValidationErrors] = useState(false);
|
||||||
|
const [hasEndDateChanged, setHasEndDateChanged] = useState(false);
|
||||||
|
|
||||||
// Use ref to track if we've already initialized to avoid infinite loop
|
// Use ref to track if we've already initialized to avoid infinite loop
|
||||||
const isInitializedRef = useRef(false);
|
const isInitializedRef = useRef(false);
|
||||||
@@ -147,7 +149,13 @@ function EventPopover({
|
|||||||
setLocation("");
|
setLocation("");
|
||||||
setStart("");
|
setStart("");
|
||||||
setEnd("");
|
setEnd("");
|
||||||
setCalendarid(userPersonnalCalendars[0]?.id);
|
if (
|
||||||
|
userPersonnalCalendars &&
|
||||||
|
userPersonnalCalendars.length > 0 &&
|
||||||
|
userPersonnalCalendars[0]?.id
|
||||||
|
) {
|
||||||
|
setCalendarid(userPersonnalCalendars[0].id);
|
||||||
|
}
|
||||||
setAllDay(false);
|
setAllDay(false);
|
||||||
setRepetition({} as RepetitionObject);
|
setRepetition({} as RepetitionObject);
|
||||||
setAlarm("");
|
setAlarm("");
|
||||||
@@ -156,7 +164,7 @@ function EventPopover({
|
|||||||
setTimezone(calendarTimezone);
|
setTimezone(calendarTimezone);
|
||||||
setHasVideoConference(false);
|
setHasVideoConference(false);
|
||||||
setMeetingLink(null);
|
setMeetingLink(null);
|
||||||
}, [calendarTimezone]);
|
}, [calendarTimezone, userPersonnalCalendars]);
|
||||||
|
|
||||||
// Track if we should sync from selectedRange (only on initial selection, not on toggle)
|
// Track if we should sync from selectedRange (only on initial selection, not on toggle)
|
||||||
const shouldSyncFromRangeRef = useRef(true);
|
const shouldSyncFromRangeRef = useRef(true);
|
||||||
@@ -224,9 +232,38 @@ function EventPopover({
|
|||||||
const startValue = selectedRange.allDay
|
const startValue = selectedRange.allDay
|
||||||
? startStr.split("T")[0]
|
? startStr.split("T")[0]
|
||||||
: startStr.slice(0, 16); // YYYY-MM-DDTHH:mm
|
: startStr.slice(0, 16); // YYYY-MM-DDTHH:mm
|
||||||
const endValue = selectedRange.allDay
|
let endValue = selectedRange.allDay
|
||||||
? endStr.split("T")[0]
|
? endStr.split("T")[0]
|
||||||
: endStr.slice(0, 16);
|
: endStr.slice(0, 16);
|
||||||
|
|
||||||
|
// For all-day slots: detect single click vs drag multiple days
|
||||||
|
// FullCalendar uses exclusive end, so end date = actual end date + 1
|
||||||
|
// For UI display, we need to adjust:
|
||||||
|
// - Single click (end = start + 1): set end = start for UI
|
||||||
|
// - Drag multiple days (end = actual end + 1): set end = end - 1 for UI
|
||||||
|
if (selectedRange.allDay) {
|
||||||
|
const startDateOnly = startValue.slice(0, 10);
|
||||||
|
const endDateOnlyFromRange = endValue.slice(0, 10);
|
||||||
|
|
||||||
|
// Calculate days difference
|
||||||
|
const startDateObj = new Date(startDateOnly);
|
||||||
|
const endDateObj = new Date(endDateOnlyFromRange);
|
||||||
|
const daysDiff = Math.floor(
|
||||||
|
(endDateObj.getTime() - startDateObj.getTime()) /
|
||||||
|
(1000 * 60 * 60 * 24)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (daysDiff <= 1) {
|
||||||
|
// Single click: FullCalendar gives end = start + 1, set end = start for UI
|
||||||
|
endValue = startValue;
|
||||||
|
} else {
|
||||||
|
// Drag multiple days: FullCalendar gives end = actual end + 1, subtract 1 for UI
|
||||||
|
const adjustedEndDate = new Date(endDateObj);
|
||||||
|
adjustedEndDate.setDate(adjustedEndDate.getDate() - 1);
|
||||||
|
endValue = adjustedEndDate.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setStart(startValue);
|
setStart(startValue);
|
||||||
setEnd(endValue);
|
setEnd(endValue);
|
||||||
|
|
||||||
@@ -240,7 +277,42 @@ function EventPopover({
|
|||||||
// 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
|
||||||
const formattedStart = formatLocalDateTime(selectedRange.start);
|
const formattedStart = formatLocalDateTime(selectedRange.start);
|
||||||
const formattedEnd = formatLocalDateTime(selectedRange.end);
|
let formattedEnd = formatLocalDateTime(selectedRange.end);
|
||||||
|
|
||||||
|
// For all-day slots: detect single click vs drag multiple days
|
||||||
|
// FullCalendar uses exclusive end, so end date = actual end date + 1
|
||||||
|
// For UI display, we need to adjust:
|
||||||
|
// - Single click (end = start + 1): set end = start for UI
|
||||||
|
// - Drag multiple days (end = actual end + 1): set end = end - 1 for UI
|
||||||
|
if (selectedRange.allDay && formattedStart && formattedEnd) {
|
||||||
|
const startDateOnly = formattedStart.slice(0, 10);
|
||||||
|
const endDateOnly = formattedEnd.slice(0, 10);
|
||||||
|
|
||||||
|
// Calculate days difference
|
||||||
|
const startDateObj = new Date(startDateOnly);
|
||||||
|
const endDateObj = new Date(endDateOnly);
|
||||||
|
const daysDiff = Math.floor(
|
||||||
|
(endDateObj.getTime() - startDateObj.getTime()) /
|
||||||
|
(1000 * 60 * 60 * 24)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (daysDiff <= 1) {
|
||||||
|
// Single click: FullCalendar gives end = start + 1, set end = start for UI
|
||||||
|
formattedEnd = formattedStart;
|
||||||
|
} else {
|
||||||
|
// Drag multiple days: FullCalendar gives end = actual end + 1, subtract 1 for UI
|
||||||
|
const adjustedEndDate = new Date(endDateObj);
|
||||||
|
adjustedEndDate.setDate(adjustedEndDate.getDate() - 1);
|
||||||
|
const adjustedEndDateStr = adjustedEndDate
|
||||||
|
.toISOString()
|
||||||
|
.split("T")[0];
|
||||||
|
// Preserve time part if exists, otherwise use date only
|
||||||
|
formattedEnd = formattedEnd.includes("T")
|
||||||
|
? `${adjustedEndDateStr}T${formattedEnd.split("T")[1]}`
|
||||||
|
: adjustedEndDateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (formattedStart) setStart(formattedStart);
|
if (formattedStart) setStart(formattedStart);
|
||||||
if (formattedEnd) setEnd(formattedEnd);
|
if (formattedEnd) setEnd(formattedEnd);
|
||||||
if (formattedStart && formattedEnd) {
|
if (formattedStart && formattedEnd) {
|
||||||
@@ -323,7 +395,13 @@ function EventPopover({
|
|||||||
setEnd("");
|
setEnd("");
|
||||||
}
|
}
|
||||||
|
|
||||||
setCalendarid(userPersonnalCalendars[0].id);
|
if (
|
||||||
|
userPersonnalCalendars &&
|
||||||
|
userPersonnalCalendars.length > 0 &&
|
||||||
|
userPersonnalCalendars[0]?.id
|
||||||
|
) {
|
||||||
|
setCalendarid(userPersonnalCalendars[0].id);
|
||||||
|
}
|
||||||
setRepetition(event.repetition ?? ({} as RepetitionObject));
|
setRepetition(event.repetition ?? ({} as RepetitionObject));
|
||||||
setShowRepeat(event.repetition?.freq ? true : false);
|
setShowRepeat(event.repetition?.freq ? true : false);
|
||||||
setAttendees(
|
setAttendees(
|
||||||
@@ -378,7 +456,13 @@ function EventPopover({
|
|||||||
setDescription("");
|
setDescription("");
|
||||||
setAttendees([]);
|
setAttendees([]);
|
||||||
setLocation("");
|
setLocation("");
|
||||||
setCalendarid(userPersonnalCalendars[0].id);
|
if (
|
||||||
|
userPersonnalCalendars &&
|
||||||
|
userPersonnalCalendars.length > 0 &&
|
||||||
|
userPersonnalCalendars[0]?.id
|
||||||
|
) {
|
||||||
|
setCalendarid(userPersonnalCalendars[0].id);
|
||||||
|
}
|
||||||
setAllDay(false);
|
setAllDay(false);
|
||||||
setRepetition({} as RepetitionObject);
|
setRepetition({} as RepetitionObject);
|
||||||
setAlarm("");
|
setAlarm("");
|
||||||
@@ -407,7 +491,9 @@ function EventPopover({
|
|||||||
startStr: newStart,
|
startStr: newStart,
|
||||||
allDay: allday,
|
allDay: allday,
|
||||||
} as DateSelectArg;
|
} as DateSelectArg;
|
||||||
calendarRef.current?.select(newRange);
|
setTimeout(() => {
|
||||||
|
calendarRef.current?.select(newRange);
|
||||||
|
}, 0);
|
||||||
return newRange;
|
return newRange;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -430,7 +516,9 @@ function EventPopover({
|
|||||||
endStr: newEnd,
|
endStr: newEnd,
|
||||||
allDay: allday,
|
allDay: allday,
|
||||||
} as DateSelectArg;
|
} as DateSelectArg;
|
||||||
calendarRef.current?.select(newRange);
|
setTimeout(() => {
|
||||||
|
calendarRef.current?.select(newRange);
|
||||||
|
}, 0);
|
||||||
return newRange;
|
return newRange;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -462,7 +550,9 @@ function EventPopover({
|
|||||||
),
|
),
|
||||||
allDay: newAllDay,
|
allDay: newAllDay,
|
||||||
} as DateSelectArg;
|
} as DateSelectArg;
|
||||||
calendarRef.current?.select(newRange);
|
setTimeout(() => {
|
||||||
|
calendarRef.current?.select(newRange);
|
||||||
|
}, 0);
|
||||||
return newRange;
|
return newRange;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -491,10 +581,20 @@ function EventPopover({
|
|||||||
}
|
}
|
||||||
const newEventUID = crypto.randomUUID();
|
const newEventUID = crypto.randomUUID();
|
||||||
|
|
||||||
|
// Resolve target calendar safely
|
||||||
|
const targetCalendar: Calendars | undefined =
|
||||||
|
calList[calendarid] ||
|
||||||
|
userPersonnalCalendars[0] ||
|
||||||
|
(Object.values(calList)[0] as Calendars | undefined);
|
||||||
|
if (!targetCalendar || !targetCalendar.id) {
|
||||||
|
console.error("No target calendar available to save event");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newEvent: CalendarEvent = {
|
const newEvent: CalendarEvent = {
|
||||||
calId: calList[calendarid].id,
|
calId: targetCalendar.id,
|
||||||
title,
|
title,
|
||||||
URL: `/calendars/${calList[calendarid].id}/${newEventUID}.ics`,
|
URL: `/calendars/${targetCalendar.id}/${newEventUID}.ics`,
|
||||||
start: "",
|
start: "",
|
||||||
allday,
|
allday,
|
||||||
uid: newEventUID,
|
uid: newEventUID,
|
||||||
@@ -515,22 +615,35 @@ function EventPopover({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
transp: busy,
|
transp: busy,
|
||||||
color: calList[calendarid]?.color,
|
color: targetCalendar?.color,
|
||||||
alarm: { trigger: alarm, action: "EMAIL" },
|
alarm: { trigger: alarm, action: "EMAIL" },
|
||||||
x_openpass_videoconference: meetingLink || undefined,
|
x_openpass_videoconference: meetingLink || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (allday) {
|
if (allday) {
|
||||||
const startDateOnly = (start || "").split("T")[0];
|
const startDateOnly = (start || "").split("T")[0];
|
||||||
const endDateOnlyBase = (end || start || "").split("T")[0];
|
const endDateOnlyUI = (end || start || "").split("T")[0];
|
||||||
|
// For all-day events, API needs end date = UI end date + 1 day
|
||||||
|
const endDateOnlyAPI = addDays(endDateOnlyUI, 1);
|
||||||
const startDateObj = new Date(`${startDateOnly}T00:00:00`);
|
const startDateObj = new Date(`${startDateOnly}T00:00:00`);
|
||||||
const endDateObj = new Date(`${endDateOnlyBase}T00:00:00`);
|
const endDateObj = new Date(`${endDateOnlyAPI}T00:00:00`);
|
||||||
newEvent.start = startDateObj.toISOString();
|
newEvent.start = startDateObj.toISOString();
|
||||||
newEvent.end = endDateObj.toISOString();
|
newEvent.end = endDateObj.toISOString();
|
||||||
} else {
|
} else {
|
||||||
newEvent.start = new Date(start).toISOString();
|
newEvent.start = new Date(start).toISOString();
|
||||||
if (end) {
|
if (end) {
|
||||||
newEvent.end = new Date(end).toISOString();
|
// In normal mode, only override end date when the end date field is not shown
|
||||||
|
if (!showMore && !hasEndDateChanged) {
|
||||||
|
const startDateOnly = (start || "").split("T")[0];
|
||||||
|
const endTimeOnly = end.includes("T")
|
||||||
|
? end.split("T")[1]?.slice(0, 5) || "00:00"
|
||||||
|
: "00:00";
|
||||||
|
const endDateTime = `${startDateOnly}T${endTimeOnly}`;
|
||||||
|
newEvent.end = new Date(endDateTime).toISOString();
|
||||||
|
} else {
|
||||||
|
// Extended mode or end date explicitly shown in normal mode: use actual end datetime
|
||||||
|
newEvent.end = new Date(end).toISOString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,7 +663,7 @@ function EventPopover({
|
|||||||
// Save to API in background
|
// Save to API in background
|
||||||
await dispatch(
|
await dispatch(
|
||||||
putEventAsync({
|
putEventAsync({
|
||||||
cal: calList[calendarid],
|
cal: targetCalendar,
|
||||||
newEvent,
|
newEvent,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -633,6 +746,7 @@ function EventPopover({
|
|||||||
onAllDayChange={handleAllDayChange}
|
onAllDayChange={handleAllDayChange}
|
||||||
onValidationChange={setIsFormValid}
|
onValidationChange={setIsFormValid}
|
||||||
showValidationErrors={showValidationErrors}
|
showValidationErrors={showValidationErrors}
|
||||||
|
onHasEndDateChangedChange={setHasEndDateChanged}
|
||||||
/>
|
/>
|
||||||
</ResponsiveDialog>
|
</ResponsiveDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { TIMEZONES } from "../../utils/timezone-data";
|
|||||||
import { addVideoConferenceToDescription } from "../../utils/videoConferenceUtils";
|
import { addVideoConferenceToDescription } from "../../utils/videoConferenceUtils";
|
||||||
import EventFormFields from "../../components/Event/EventFormFields";
|
import EventFormFields from "../../components/Event/EventFormFields";
|
||||||
import { formatDateTimeInTimezone } from "../../components/Event/utils/dateTimeFormatters";
|
import { formatDateTimeInTimezone } from "../../components/Event/utils/dateTimeFormatters";
|
||||||
|
import { addDays } from "../../components/Event/utils/dateRules";
|
||||||
import { getEvent, deleteEvent, putEvent } from "./EventApi";
|
import { getEvent, deleteEvent, putEvent } from "./EventApi";
|
||||||
import { refreshCalendars } from "../../components/Event/utils/eventUtils";
|
import { refreshCalendars } from "../../components/Event/utils/eventUtils";
|
||||||
import { getCalendarRange } from "../../utils/dateUtils";
|
import { getCalendarRange } from "../../utils/dateUtils";
|
||||||
@@ -154,7 +155,7 @@ function EventUpdateModal({
|
|||||||
);
|
);
|
||||||
const [newCalId, setNewCalId] = useState(calId);
|
const [newCalId, setNewCalId] = useState(calId);
|
||||||
const [calendarid, setCalendarid] = useState(
|
const [calendarid, setCalendarid] = useState(
|
||||||
calId ?? userPersonnalCalendars[0]?.id
|
calId ?? userPersonnalCalendars[0]?.id ?? ""
|
||||||
);
|
);
|
||||||
|
|
||||||
const [attendees, setAttendees] = useState<userAttendee[]>([]);
|
const [attendees, setAttendees] = useState<userAttendee[]>([]);
|
||||||
@@ -162,6 +163,7 @@ function EventUpdateModal({
|
|||||||
const [meetingLink, setMeetingLink] = useState<string | null>(null);
|
const [meetingLink, setMeetingLink] = useState<string | null>(null);
|
||||||
const [isFormValid, setIsFormValid] = useState(false);
|
const [isFormValid, setIsFormValid] = useState(false);
|
||||||
const [showValidationErrors, setShowValidationErrors] = useState(false);
|
const [showValidationErrors, setShowValidationErrors] = useState(false);
|
||||||
|
const [hasEndDateChanged, setHasEndDateChanged] = useState(false);
|
||||||
|
|
||||||
const resetAllStateToDefault = useCallback(() => {
|
const resetAllStateToDefault = useCallback(() => {
|
||||||
setShowMore(false);
|
setShowMore(false);
|
||||||
@@ -379,12 +381,27 @@ function EventUpdateModal({
|
|||||||
// For single events or "solo" edits, use the edited dates from form
|
// For single events or "solo" edits, use the edited dates from form
|
||||||
if (allday) {
|
if (allday) {
|
||||||
// For all-day events, use date format (YYYY-MM-DD)
|
// For all-day events, use date format (YYYY-MM-DD)
|
||||||
startDate = new Date(start).toISOString().split("T")[0];
|
// API needs end date = UI end date + 1 day
|
||||||
endDate = new Date(end).toISOString().split("T")[0];
|
const startDateOnly = new Date(start).toISOString().split("T")[0];
|
||||||
|
const endDateOnlyUI = new Date(end).toISOString().split("T")[0];
|
||||||
|
const endDateOnlyAPI = addDays(endDateOnlyUI, 1);
|
||||||
|
startDate = startDateOnly;
|
||||||
|
endDate = endDateOnlyAPI;
|
||||||
} else {
|
} else {
|
||||||
// For timed events, use full datetime
|
// For timed events
|
||||||
startDate = new Date(start).toISOString();
|
startDate = new Date(start).toISOString();
|
||||||
endDate = new Date(end).toISOString();
|
// In normal mode, only override end date when the end date field is not shown
|
||||||
|
if (!showMore && !hasEndDateChanged) {
|
||||||
|
const startDateOnly = (start || "").split("T")[0];
|
||||||
|
const endTimeOnly = end.includes("T")
|
||||||
|
? end.split("T")[1]?.slice(0, 5) || "00:00"
|
||||||
|
: "00:00";
|
||||||
|
const endDateTime = `${startDateOnly}T${endTimeOnly}`;
|
||||||
|
endDate = new Date(endDateTime).toISOString();
|
||||||
|
} else {
|
||||||
|
// Extended mode: use actual end datetime
|
||||||
|
endDate = new Date(end).toISOString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,6 +770,7 @@ function EventUpdateModal({
|
|||||||
}}
|
}}
|
||||||
onValidationChange={setIsFormValid}
|
onValidationChange={setIsFormValid}
|
||||||
showValidationErrors={showValidationErrors}
|
showValidationErrors={showValidationErrors}
|
||||||
|
onHasEndDateChangedChange={setHasEndDateChanged}
|
||||||
/>
|
/>
|
||||||
</ResponsiveDialog>
|
</ResponsiveDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user