feat: Split datetime fields
- Replace single datetime-local field with 4 separate fields: - Add helper functions: * splitDateTime(): Split datetime string into date and time parts * combineDateTime(): Combine date and time into datetime string - Implement internal state management: * startDate, startTime, endDate, endTime internal states * Sync effects to update internal states from start/end props * Change handlers for each field that combine values and call parent handlers - Fix test cases: * Update EventModal.test.tsx, EventUpdateModal.test.tsx to use new field labels * Change from 'Start'/'End' to 'Start Date'/'Start Time'/'End Time'/'End Date'
This commit is contained in:
committed by
Benoit TELLIER
parent
4f5308fe08
commit
701f2f2b71
@@ -152,7 +152,8 @@ describe("EventPopover", () => {
|
|||||||
allDay: false,
|
allDay: false,
|
||||||
} as unknown as DateSelectArg);
|
} as unknown as DateSelectArg);
|
||||||
|
|
||||||
expect(screen.getByLabelText("Start")).toHaveValue("2026-07-20T10:00");
|
expect(screen.getByLabelText("Start Date")).toHaveValue("2026-07-20");
|
||||||
|
expect(screen.getByLabelText("Start Time")).toHaveValue("10:00");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates inputs on change", () => {
|
it("updates inputs on change", () => {
|
||||||
@@ -276,10 +277,10 @@ describe("EventPopover", () => {
|
|||||||
target: { value: newEvent.title },
|
target: { value: newEvent.title },
|
||||||
});
|
});
|
||||||
fireEvent.click(screen.getByLabelText("All day"));
|
fireEvent.click(screen.getByLabelText("All day"));
|
||||||
fireEvent.change(screen.getByLabelText("Start"), {
|
fireEvent.change(screen.getByLabelText("Start Date"), {
|
||||||
target: { value: newEvent.start.split("T")[0] },
|
target: { value: newEvent.start.split("T")[0] },
|
||||||
});
|
});
|
||||||
fireEvent.change(screen.getByLabelText("End"), {
|
fireEvent.change(screen.getByLabelText("End Date"), {
|
||||||
target: { value: newEvent.end.split("T")[0] },
|
target: { value: newEvent.end.split("T")[0] },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -92,16 +92,20 @@ describe("EventUpdateModal Timezone Handling", () => {
|
|||||||
const titleInput = screen.getByDisplayValue("Timezone Event");
|
const titleInput = screen.getByDisplayValue("Timezone Event");
|
||||||
expect(titleInput).toBeInTheDocument();
|
expect(titleInput).toBeInTheDocument();
|
||||||
|
|
||||||
// Verify the start time input exists
|
// Verify the start date and time inputs exist
|
||||||
// Since showMore is false by default, label should be "Start"
|
// Since showMore is false by default, labels should be "Start Date" and "Start Time"
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const startInput = screen.getByLabelText("Start");
|
const startDateInput = screen.getByLabelText("Start Date");
|
||||||
expect(startInput).toBeInTheDocument();
|
const startTimeInput = screen.getByLabelText("Start Time");
|
||||||
expect(startInput).toHaveAttribute("type", "datetime-local");
|
|
||||||
|
|
||||||
// The value should be formatted as 2025-01-15T14:00 (2PM in Bangkok time)
|
expect(startDateInput).toBeInTheDocument();
|
||||||
// EventUpdateModal uses formatDateTimeInTimezone which formats in event's original timezone
|
expect(startTimeInput).toBeInTheDocument();
|
||||||
expect(startInput).toHaveValue("2025-01-15T14:00");
|
expect(startDateInput).toHaveAttribute("type", "date");
|
||||||
|
expect(startTimeInput).toHaveAttribute("type", "time");
|
||||||
|
|
||||||
|
// The values should be split from 2025-01-15T14:00
|
||||||
|
expect(startDateInput).toHaveValue("2025-01-15");
|
||||||
|
expect(startTimeInput).toHaveValue("14:00");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function updateDarkColor(
|
|||||||
dispatch: ThunkDispatch<any, any, any>
|
dispatch: ThunkDispatch<any, any, any>
|
||||||
) {
|
) {
|
||||||
Object.values(calendars).forEach((cal) => {
|
Object.values(calendars).forEach((cal) => {
|
||||||
if (!cal?.color?.light) return;
|
if (!cal?.color?.light || typeof cal.color.light !== "string") return;
|
||||||
const baseColor = cal.color.light;
|
const baseColor = cal.color.light;
|
||||||
|
|
||||||
const isDefault = Object.values(defaultColors).find(
|
const isDefault = Object.values(defaultColors).find(
|
||||||
@@ -29,6 +29,10 @@ export function updateDarkColor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getAccessiblePair(baseColor: string, theme: Theme): string {
|
export function getAccessiblePair(baseColor: string, theme: Theme): string {
|
||||||
|
if (typeof baseColor !== "string") {
|
||||||
|
return theme.palette.getContrastText("#000");
|
||||||
|
}
|
||||||
|
|
||||||
const contrastToBlack = getContrastRatio(baseColor, "#000");
|
const contrastToBlack = getContrastRatio(baseColor, "#000");
|
||||||
const contrastToWhite = getContrastRatio(baseColor, "#fff");
|
const contrastToWhite = getContrastRatio(baseColor, "#fff");
|
||||||
const isLight = contrastToBlack > contrastToWhite;
|
const isLight = contrastToBlack > contrastToWhite;
|
||||||
|
|||||||
@@ -35,6 +35,23 @@ import {
|
|||||||
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
|
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
|
||||||
import { CalendarItemList } from "../Calendar/CalendarItemList";
|
import { CalendarItemList } from "../Calendar/CalendarItemList";
|
||||||
|
|
||||||
|
// Helper to split datetime string (YYYY-MM-DDTHH:mm) to date and time
|
||||||
|
function splitDateTime(datetime: string): { date: string; time: string } {
|
||||||
|
if (!datetime) return { date: "", time: "" };
|
||||||
|
const parts = datetime.split("T");
|
||||||
|
return {
|
||||||
|
date: parts[0] || "",
|
||||||
|
time: parts[1]?.slice(0, 5) || "", // HH:mm only
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to combine date and time to datetime string
|
||||||
|
function combineDateTime(date: string, time: string): string {
|
||||||
|
if (!date) return "";
|
||||||
|
if (!time) return date; // Date only for all-day
|
||||||
|
return `${date}T${time}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper component for field with label
|
// Helper component for field with label
|
||||||
export const FieldWithLabel = React.memo(
|
export const FieldWithLabel = React.memo(
|
||||||
({
|
({
|
||||||
@@ -200,9 +217,18 @@ export default function EventFormFields({
|
|||||||
showValidationErrors = false,
|
showValidationErrors = false,
|
||||||
}: EventFormFieldsProps) {
|
}: EventFormFieldsProps) {
|
||||||
// Store original time before toggling to all-day
|
// Store original time before toggling to all-day
|
||||||
const originalTimeRef = React.useRef<{ start: string; end: string } | null>(
|
const originalTimeRef = React.useRef<{
|
||||||
null
|
start: string;
|
||||||
);
|
end: string;
|
||||||
|
endDate?: string; // Add this field
|
||||||
|
fromAllDaySlot?: boolean; // Track if came from all-day slot click
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
// Internal state for 4 separate fields
|
||||||
|
const [startDate, setStartDate] = React.useState("");
|
||||||
|
const [startTime, setStartTime] = React.useState("");
|
||||||
|
const [endDate, setEndDate] = React.useState("");
|
||||||
|
const [endTime, setEndTime] = React.useState("");
|
||||||
|
|
||||||
// Ref for title input field to enable auto-focus
|
// Ref for title input field to enable auto-focus
|
||||||
const titleInputRef = React.useRef<HTMLInputElement>(null);
|
const titleInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
@@ -251,44 +277,135 @@ export default function EventFormFields({
|
|||||||
prevShowMoreRef.current = showMore;
|
prevShowMoreRef.current = showMore;
|
||||||
}, [showMore, isOpen]);
|
}, [showMore, isOpen]);
|
||||||
|
|
||||||
|
// Sync start prop to startDate/startTime
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (start) {
|
||||||
|
const { date, time } = splitDateTime(start);
|
||||||
|
setStartDate(date);
|
||||||
|
setStartTime(time);
|
||||||
|
}
|
||||||
|
}, [start]);
|
||||||
|
|
||||||
|
// Sync end prop to endDate/endTime
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (end) {
|
||||||
|
const { date, time } = splitDateTime(end);
|
||||||
|
setEndDate(date);
|
||||||
|
setEndTime(time);
|
||||||
|
}
|
||||||
|
}, [end]);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const handleStartDateChange = React.useCallback(
|
||||||
|
(newDate: string) => {
|
||||||
|
setStartDate(newDate);
|
||||||
|
const newStart = combineDateTime(newDate, startTime);
|
||||||
|
if (onStartChange) {
|
||||||
|
onStartChange(newStart);
|
||||||
|
} else {
|
||||||
|
setStart(newStart);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[startTime, onStartChange, setStart]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleStartTimeChange = React.useCallback(
|
||||||
|
(newTime: string) => {
|
||||||
|
setStartTime(newTime);
|
||||||
|
const newStart = combineDateTime(startDate, newTime);
|
||||||
|
if (onStartChange) {
|
||||||
|
onStartChange(newStart);
|
||||||
|
} else {
|
||||||
|
setStart(newStart);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[startDate, onStartChange, setStart]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEndDateChange = React.useCallback(
|
||||||
|
(newDate: string) => {
|
||||||
|
setEndDate(newDate);
|
||||||
|
const newEnd = combineDateTime(newDate, endTime);
|
||||||
|
if (onEndChange) {
|
||||||
|
onEndChange(newEnd);
|
||||||
|
} else {
|
||||||
|
setEnd(newEnd);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[endTime, onEndChange, setEnd]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEndTimeChange = React.useCallback(
|
||||||
|
(newTime: string) => {
|
||||||
|
setEndTime(newTime);
|
||||||
|
const newEnd = combineDateTime(endDate, newTime);
|
||||||
|
if (onEndChange) {
|
||||||
|
onEndChange(newEnd);
|
||||||
|
} else {
|
||||||
|
setEnd(newEnd);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[endDate, onEndChange, setEnd]
|
||||||
|
);
|
||||||
|
|
||||||
// Validation logic
|
// Validation logic
|
||||||
const validateForm = React.useCallback(() => {
|
const validateForm = React.useCallback(() => {
|
||||||
// Title validation
|
|
||||||
const isTitleValid = title.trim().length > 0;
|
const isTitleValid = title.trim().length > 0;
|
||||||
const shouldShowTitleError = showValidationErrors && !isTitleValid;
|
const shouldShowTitleError = showValidationErrors && !isTitleValid;
|
||||||
|
|
||||||
// Date/time validation
|
|
||||||
let isDateTimeValid = true;
|
let isDateTimeValid = true;
|
||||||
let dateTimeError = "";
|
let dateTimeError = "";
|
||||||
let startError = "";
|
|
||||||
|
|
||||||
// Convert to string if needed
|
// Validate start date
|
||||||
const startStr = typeof start === "string" ? start : String(start || "");
|
if (!startDate || startDate.trim() === "") {
|
||||||
const endStr = typeof end === "string" ? end : String(end || "");
|
|
||||||
|
|
||||||
// Check if start date is provided and valid
|
|
||||||
if (!start || startStr.trim() === "") {
|
|
||||||
isDateTimeValid = false;
|
isDateTimeValid = false;
|
||||||
dateTimeError = "Start date/time is required";
|
dateTimeError = "Start date 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
|
// Validate start time (if not all-day)
|
||||||
else if (!end || endStr.trim() === "") {
|
else if (!allday && (!startTime || startTime.trim() === "")) {
|
||||||
isDateTimeValid = false;
|
isDateTimeValid = false;
|
||||||
dateTimeError = "End date/time is required";
|
dateTimeError = "Start time is required";
|
||||||
} else if (isNaN(new Date(end).getTime())) {
|
|
||||||
isDateTimeValid = false;
|
|
||||||
dateTimeError = "Invalid end date/time";
|
|
||||||
}
|
}
|
||||||
// Check if end is after start
|
// Validate end date
|
||||||
|
else if (!endDate || endDate.trim() === "") {
|
||||||
|
isDateTimeValid = false;
|
||||||
|
dateTimeError = "End date is required";
|
||||||
|
}
|
||||||
|
// Validate end time (if not all-day)
|
||||||
|
else if (!allday && (!endTime || endTime.trim() === "")) {
|
||||||
|
isDateTimeValid = false;
|
||||||
|
dateTimeError = "End time is required";
|
||||||
|
}
|
||||||
|
// Validate end > start
|
||||||
else {
|
else {
|
||||||
const startDate = new Date(start);
|
const startDateTime = allday
|
||||||
const endDate = new Date(end);
|
? new Date(startDate + "T00:00:00")
|
||||||
if (endDate <= startDate) {
|
: new Date(combineDateTime(startDate, startTime));
|
||||||
|
const endDateTime = allday
|
||||||
|
? new Date(endDate + "T00:00:00")
|
||||||
|
: new Date(combineDateTime(endDate, endTime));
|
||||||
|
|
||||||
|
if (isNaN(startDateTime.getTime()) || isNaN(endDateTime.getTime())) {
|
||||||
|
isDateTimeValid = false;
|
||||||
|
dateTimeError = "Invalid date/time";
|
||||||
|
} else if (endDateTime <= startDateTime) {
|
||||||
isDateTimeValid = false;
|
isDateTimeValid = false;
|
||||||
dateTimeError = "End time must be after start time";
|
dateTimeError = "End time must be after start time";
|
||||||
}
|
}
|
||||||
@@ -300,11 +417,18 @@ export default function EventFormFields({
|
|||||||
isValid,
|
isValid,
|
||||||
errors: {
|
errors: {
|
||||||
title: shouldShowTitleError ? "Title is required" : "",
|
title: shouldShowTitleError ? "Title is required" : "",
|
||||||
start: showValidationErrors ? startError : "",
|
|
||||||
dateTime: showValidationErrors ? dateTimeError : "",
|
dateTime: showValidationErrors ? dateTimeError : "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [title, start, end, showValidationErrors]);
|
}, [
|
||||||
|
title,
|
||||||
|
startDate,
|
||||||
|
startTime,
|
||||||
|
endDate,
|
||||||
|
endTime,
|
||||||
|
allday,
|
||||||
|
showValidationErrors,
|
||||||
|
]);
|
||||||
|
|
||||||
// Notify parent about validation changes
|
// Notify parent about validation changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -346,22 +470,6 @@ export default function EventFormFields({
|
|||||||
setMeetingLink(null);
|
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 = (
|
const handleAllDayChange = (
|
||||||
newAllDay: boolean,
|
newAllDay: boolean,
|
||||||
newStart: string,
|
newStart: string,
|
||||||
@@ -457,47 +565,109 @@ export default function EventFormFields({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<FieldWithLabel label="Date & Time" isExpanded={showMore}>
|
<FieldWithLabel label="Date & Time" isExpanded={showMore}>
|
||||||
<Box display="flex" gap={2} flexDirection="column">
|
<Box display="flex" gap={1} flexDirection="column">
|
||||||
<Box display="flex" gap={2}>
|
{/* First row: 4 fields */}
|
||||||
<Box flexGrow={1}>
|
<Box
|
||||||
|
display="flex"
|
||||||
|
gap={1}
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
|
{/* Start Date - 30% */}
|
||||||
|
<Box sx={{ flexGrow: 0.3, flexBasis: "30%" }}>
|
||||||
{showMore && (
|
{showMore && (
|
||||||
<Typography variant="caption" display="block" mb={0.5}>
|
<Typography variant="caption" display="block" mb={0.5}>
|
||||||
Start
|
Start Date
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label={!showMore ? "Start" : ""}
|
label={!showMore ? "Start Date" : ""}
|
||||||
type={allday ? "date" : "datetime-local"}
|
type="date"
|
||||||
value={allday ? start.split("T")[0] : start}
|
value={startDate}
|
||||||
onChange={(e) => handleStartChange(e.target.value)}
|
onChange={(e) => handleStartDateChange(e.target.value)}
|
||||||
error={!!validation.errors.start}
|
|
||||||
helperText={validation.errors.start}
|
|
||||||
size="small"
|
size="small"
|
||||||
margin="dense"
|
margin="dense"
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexGrow={1}>
|
|
||||||
|
{/* Start Time - 20% */}
|
||||||
|
<Box sx={{ flexGrow: 0.2, flexBasis: "20%" }}>
|
||||||
{showMore && (
|
{showMore && (
|
||||||
<Typography variant="caption" display="block" mb={0.5}>
|
<Typography variant="caption" display="block" mb={0.5}>
|
||||||
End
|
Start Time
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label={!showMore ? "End" : ""}
|
label={!showMore ? "Start Time" : ""}
|
||||||
type={allday ? "date" : "datetime-local"}
|
type="time"
|
||||||
value={allday ? end.split("T")[0] : end}
|
value={startTime}
|
||||||
onChange={(e) => handleEndChange(e.target.value)}
|
onChange={(e) => handleStartTimeChange(e.target.value)}
|
||||||
|
disabled={allday}
|
||||||
|
size="small"
|
||||||
|
margin="dense"
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* End Time - 20% */}
|
||||||
|
<Box sx={{ flexGrow: 0.2, flexBasis: "20%" }}>
|
||||||
|
{showMore && (
|
||||||
|
<Typography variant="caption" display="block" mb={0.5}>
|
||||||
|
End Time
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={!showMore ? "End Time" : ""}
|
||||||
|
type="time"
|
||||||
|
value={endTime}
|
||||||
|
onChange={(e) => handleEndTimeChange(e.target.value)}
|
||||||
|
disabled={allday}
|
||||||
|
error={!!validation.errors.dateTime}
|
||||||
|
size="small"
|
||||||
|
margin="dense"
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* End Date - 30% */}
|
||||||
|
<Box sx={{ flexGrow: 0.3, flexBasis: "30%" }}>
|
||||||
|
{showMore && (
|
||||||
|
<Typography variant="caption" display="block" mb={0.5}>
|
||||||
|
End Date
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={!showMore ? "End Date" : ""}
|
||||||
|
type="date"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => handleEndDateChange(e.target.value)}
|
||||||
error={!!validation.errors.dateTime}
|
error={!!validation.errors.dateTime}
|
||||||
helperText={validation.errors.dateTime}
|
|
||||||
size="small"
|
size="small"
|
||||||
margin="dense"
|
margin="dense"
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Second row: Error message - 2 columns 50% each */}
|
||||||
|
{validation.errors.dateTime && (
|
||||||
|
<Box display="flex" gap={1} flexDirection="row">
|
||||||
|
{/* Empty left column - 50% */}
|
||||||
|
<Box sx={{ flexGrow: 0.5, flexBasis: "50%" }} />
|
||||||
|
|
||||||
|
{/* Error message right column - 50% */}
|
||||||
|
<Box sx={{ flexGrow: 0.5, flexBasis: "50%" }}>
|
||||||
|
<Typography variant="caption" color="error">
|
||||||
|
{validation.errors.dateTime}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</FieldWithLabel>
|
</FieldWithLabel>
|
||||||
|
|
||||||
@@ -513,90 +683,124 @@ export default function EventFormFields({
|
|||||||
let newEnd = end;
|
let newEnd = end;
|
||||||
|
|
||||||
if (newAllDay) {
|
if (newAllDay) {
|
||||||
// OFF => ON: Save original time before converting to all-day
|
// OFF => ON: Save original time AND original end date
|
||||||
if (start.includes("T")) {
|
if (start.includes("T")) {
|
||||||
originalTimeRef.current = { start, end };
|
originalTimeRef.current = {
|
||||||
|
start: startTime,
|
||||||
|
end: endTime,
|
||||||
|
endDate: endDate, // Save original end date
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to date-only format only if both dates are valid
|
// Reset time to empty, keep only date part
|
||||||
if (start && end) {
|
setStartTime("");
|
||||||
const startDate = start.includes("T")
|
setEndTime("");
|
||||||
? new Date(start)
|
newStart = startDate;
|
||||||
: new Date(start + "T00:00:00");
|
newEnd = endDate;
|
||||||
const endDate = end.includes("T")
|
|
||||||
? new Date(end)
|
|
||||||
: new Date(end + "T00:00:00");
|
|
||||||
|
|
||||||
// Check if dates are valid before proceeding
|
// If same day, extend end to next day
|
||||||
if (
|
if (startDate === endDate) {
|
||||||
!isNaN(startDate.getTime()) &&
|
const nextDay = new Date(endDate);
|
||||||
!isNaN(endDate.getTime())
|
nextDay.setDate(nextDay.getDate() + 1);
|
||||||
) {
|
newEnd = nextDay.toISOString().split("T")[0];
|
||||||
// If same day, extend end to next day
|
setEndDate(newEnd); // Update internal endDate state
|
||||||
if (endDate.getDate() === startDate.getDate()) {
|
|
||||||
endDate.setDate(startDate.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedEnd = formatLocalDateTime(endDate);
|
|
||||||
if (formattedEnd) {
|
|
||||||
newEnd = formattedEnd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// ON => OFF: Restore original time if available
|
// ON => OFF: Restore original time AND original end date
|
||||||
if (originalTimeRef.current) {
|
if (originalTimeRef.current) {
|
||||||
// Extract hours/minutes from original time strings
|
// Check if this came from all-day slot click
|
||||||
const originalStartMatch =
|
if (originalTimeRef.current.fromAllDaySlot) {
|
||||||
originalTimeRef.current.start.match(/T(\d{2}):(\d{2})/);
|
// From all-day slot: set endDate = startDate, use default time
|
||||||
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();
|
const currentTime = getRoundedCurrentTime();
|
||||||
|
const hours = String(currentTime.getHours()).padStart(
|
||||||
|
2,
|
||||||
|
"0"
|
||||||
|
);
|
||||||
|
const minutes = String(
|
||||||
|
currentTime.getMinutes()
|
||||||
|
).padStart(2, "0");
|
||||||
|
const timeStr = `${hours}:${minutes}`;
|
||||||
|
|
||||||
startDate.setHours(currentTime.getHours());
|
newStart = combineDateTime(startDate, timeStr);
|
||||||
startDate.setMinutes(currentTime.getMinutes());
|
|
||||||
const formattedStart = formatLocalDateTime(startDate);
|
|
||||||
if (formattedStart) {
|
|
||||||
newStart = formattedStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
const endDate = new Date(startDate);
|
// End time = start time + 1 hour
|
||||||
endDate.setMinutes(endDate.getMinutes() + 30);
|
const endTimeDate = new Date(currentTime);
|
||||||
const formattedEnd = formatLocalDateTime(endDate);
|
endTimeDate.setHours(endTimeDate.getHours() + 1);
|
||||||
if (formattedEnd) {
|
const endHours = String(
|
||||||
newEnd = formattedEnd;
|
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 =
|
||||||
|
originalTimeRef.current.endDate || 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); // Changed from setMinutes +30
|
||||||
|
const endHours = String(endTimeDate.getHours()).padStart(
|
||||||
|
2,
|
||||||
|
"0"
|
||||||
|
);
|
||||||
|
const endMinutes = String(
|
||||||
|
endTimeDate.getMinutes()
|
||||||
|
).padStart(2, "0");
|
||||||
|
const endTimeStr = `${endHours}:${endMinutes}`;
|
||||||
|
|
||||||
|
newEnd = combineDateTime(endDate, endTimeStr);
|
||||||
|
|
||||||
|
// Update internal states
|
||||||
|
setStartTime(timeStr);
|
||||||
|
setEndTime(endTimeStr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update local state if no callback (for backwards compatibility)
|
|
||||||
if (!onAllDayChange) {
|
if (!onAllDayChange) {
|
||||||
setStart(newStart);
|
setStart(newStart);
|
||||||
setEnd(newEnd);
|
setEnd(newEnd);
|
||||||
setAllDay(newAllDay);
|
setAllDay(newAllDay);
|
||||||
} else {
|
} else {
|
||||||
// Let callback handle all state updates to avoid duplicate renders
|
|
||||||
handleAllDayChange(newAllDay, newStart, newEnd);
|
handleAllDayChange(newAllDay, newStart, newEnd);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -201,6 +201,11 @@ function EventPopover({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedRange && selectedRange.start && selectedRange.end) {
|
if (selectedRange && selectedRange.start && selectedRange.end) {
|
||||||
|
// Auto-check allday if selectedRange is all-day
|
||||||
|
if (selectedRange.allDay) {
|
||||||
|
setAllDay(true);
|
||||||
|
}
|
||||||
|
|
||||||
// selectedRange gives us the visual time displayed on calendar
|
// selectedRange gives us the visual time displayed on calendar
|
||||||
// Use selectedRange.startStr and endStr if available (from FullCalendar)
|
// Use selectedRange.startStr and endStr if available (from FullCalendar)
|
||||||
if (selectedRange.startStr && selectedRange.endStr) {
|
if (selectedRange.startStr && selectedRange.endStr) {
|
||||||
@@ -249,6 +254,9 @@ function EventPopover({
|
|||||||
|
|
||||||
if (formattedStart) setStart(formattedStart);
|
if (formattedStart) setStart(formattedStart);
|
||||||
if (formattedEnd) setEnd(formattedEnd);
|
if (formattedEnd) setEnd(formattedEnd);
|
||||||
|
|
||||||
|
// Default to non-all-day when no selectedRange
|
||||||
|
setAllDay(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldSyncFromRangeRef.current = false;
|
shouldSyncFromRangeRef.current = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user