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,
|
||||
} 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", () => {
|
||||
@@ -276,10 +277,10 @@ describe("EventPopover", () => {
|
||||
target: { value: newEvent.title },
|
||||
});
|
||||
fireEvent.click(screen.getByLabelText("All day"));
|
||||
fireEvent.change(screen.getByLabelText("Start"), {
|
||||
fireEvent.change(screen.getByLabelText("Start Date"), {
|
||||
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] },
|
||||
});
|
||||
|
||||
|
||||
@@ -92,16 +92,20 @@ describe("EventUpdateModal Timezone Handling", () => {
|
||||
const titleInput = screen.getByDisplayValue("Timezone Event");
|
||||
expect(titleInput).toBeInTheDocument();
|
||||
|
||||
// Verify the start time input exists
|
||||
// Since showMore is false by default, label should be "Start"
|
||||
// Verify the start date and time inputs exist
|
||||
// Since showMore is false by default, labels should be "Start Date" and "Start Time"
|
||||
await waitFor(() => {
|
||||
const startInput = screen.getByLabelText("Start");
|
||||
expect(startInput).toBeInTheDocument();
|
||||
expect(startInput).toHaveAttribute("type", "datetime-local");
|
||||
const startDateInput = screen.getByLabelText("Start Date");
|
||||
const startTimeInput = screen.getByLabelText("Start Time");
|
||||
|
||||
// The value should be formatted as 2025-01-15T14:00 (2PM in Bangkok time)
|
||||
// EventUpdateModal uses formatDateTimeInTimezone which formats in event's original timezone
|
||||
expect(startInput).toHaveValue("2025-01-15T14:00");
|
||||
expect(startDateInput).toBeInTheDocument();
|
||||
expect(startTimeInput).toBeInTheDocument();
|
||||
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>
|
||||
) {
|
||||
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 isDefault = Object.values(defaultColors).find(
|
||||
@@ -29,6 +29,10 @@ export function updateDarkColor(
|
||||
}
|
||||
|
||||
export function getAccessiblePair(baseColor: string, theme: Theme): string {
|
||||
if (typeof baseColor !== "string") {
|
||||
return theme.palette.getContrastText("#000");
|
||||
}
|
||||
|
||||
const contrastToBlack = getContrastRatio(baseColor, "#000");
|
||||
const contrastToWhite = getContrastRatio(baseColor, "#fff");
|
||||
const isLight = contrastToBlack > contrastToWhite;
|
||||
|
||||
@@ -35,6 +35,23 @@ import {
|
||||
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
|
||||
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
|
||||
export const FieldWithLabel = React.memo(
|
||||
({
|
||||
@@ -200,9 +217,18 @@ export default function EventFormFields({
|
||||
showValidationErrors = false,
|
||||
}: EventFormFieldsProps) {
|
||||
// Store original time before toggling to all-day
|
||||
const originalTimeRef = React.useRef<{ start: string; end: string } | null>(
|
||||
null
|
||||
);
|
||||
const originalTimeRef = React.useRef<{
|
||||
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
|
||||
const titleInputRef = React.useRef<HTMLInputElement>(null);
|
||||
@@ -251,44 +277,135 @@ export default function EventFormFields({
|
||||
prevShowMoreRef.current = showMore;
|
||||
}, [showMore, isOpen]);
|
||||
|
||||
// Sync start prop to startDate/startTime
|
||||
React.useEffect(() => {
|
||||
if (start) {
|
||||
const { date, time } = splitDateTime(start);
|
||||
setStartDate(date);
|
||||
setStartTime(time);
|
||||
}
|
||||
}, [start]);
|
||||
|
||||
// Sync end prop to endDate/endTime
|
||||
React.useEffect(() => {
|
||||
if (end) {
|
||||
const { date, time } = splitDateTime(end);
|
||||
setEndDate(date);
|
||||
setEndTime(time);
|
||||
}
|
||||
}, [end]);
|
||||
|
||||
// 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
|
||||
const validateForm = React.useCallback(() => {
|
||||
// Title validation
|
||||
const isTitleValid = title.trim().length > 0;
|
||||
const shouldShowTitleError = showValidationErrors && !isTitleValid;
|
||||
|
||||
// Date/time validation
|
||||
let isDateTimeValid = true;
|
||||
let dateTimeError = "";
|
||||
let startError = "";
|
||||
|
||||
// Convert to string if needed
|
||||
const startStr = typeof start === "string" ? start : String(start || "");
|
||||
const endStr = typeof end === "string" ? end : String(end || "");
|
||||
|
||||
// Check if start date is provided and valid
|
||||
if (!start || startStr.trim() === "") {
|
||||
// Validate start date
|
||||
if (!startDate || startDate.trim() === "") {
|
||||
isDateTimeValid = false;
|
||||
dateTimeError = "Start date/time is required";
|
||||
startError = "Start date/time is required";
|
||||
} else if (isNaN(new Date(start).getTime())) {
|
||||
isDateTimeValid = false;
|
||||
dateTimeError = "Invalid start date/time";
|
||||
startError = "Invalid start date/time";
|
||||
dateTimeError = "Start date is required";
|
||||
}
|
||||
// Check if end date is provided and valid
|
||||
else if (!end || endStr.trim() === "") {
|
||||
// Validate start time (if not all-day)
|
||||
else if (!allday && (!startTime || startTime.trim() === "")) {
|
||||
isDateTimeValid = false;
|
||||
dateTimeError = "End date/time is required";
|
||||
} else if (isNaN(new Date(end).getTime())) {
|
||||
isDateTimeValid = false;
|
||||
dateTimeError = "Invalid end date/time";
|
||||
dateTimeError = "Start time is required";
|
||||
}
|
||||
// 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 {
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
if (endDate <= startDate) {
|
||||
const startDateTime = allday
|
||||
? new Date(startDate + "T00:00:00")
|
||||
: 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;
|
||||
dateTimeError = "End time must be after start time";
|
||||
}
|
||||
@@ -300,11 +417,18 @@ export default function EventFormFields({
|
||||
isValid,
|
||||
errors: {
|
||||
title: shouldShowTitleError ? "Title is required" : "",
|
||||
start: showValidationErrors ? startError : "",
|
||||
dateTime: showValidationErrors ? dateTimeError : "",
|
||||
},
|
||||
};
|
||||
}, [title, start, end, showValidationErrors]);
|
||||
}, [
|
||||
title,
|
||||
startDate,
|
||||
startTime,
|
||||
endDate,
|
||||
endTime,
|
||||
allday,
|
||||
showValidationErrors,
|
||||
]);
|
||||
|
||||
// Notify parent about validation changes
|
||||
React.useEffect(() => {
|
||||
@@ -346,22 +470,6 @@ export default function EventFormFields({
|
||||
setMeetingLink(null);
|
||||
};
|
||||
|
||||
const handleStartChange = (newStart: string) => {
|
||||
if (onStartChange) {
|
||||
onStartChange(newStart);
|
||||
} else {
|
||||
setStart(newStart);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEndChange = (newEnd: string) => {
|
||||
if (onEndChange) {
|
||||
onEndChange(newEnd);
|
||||
} else {
|
||||
setEnd(newEnd);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAllDayChange = (
|
||||
newAllDay: boolean,
|
||||
newStart: string,
|
||||
@@ -457,47 +565,109 @@ export default function EventFormFields({
|
||||
)}
|
||||
|
||||
<FieldWithLabel label="Date & Time" isExpanded={showMore}>
|
||||
<Box display="flex" gap={2} flexDirection="column">
|
||||
<Box display="flex" gap={2}>
|
||||
<Box flexGrow={1}>
|
||||
<Box display="flex" gap={1} flexDirection="column">
|
||||
{/* First row: 4 fields */}
|
||||
<Box
|
||||
display="flex"
|
||||
gap={1}
|
||||
flexDirection="row"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{/* Start Date - 30% */}
|
||||
<Box sx={{ flexGrow: 0.3, flexBasis: "30%" }}>
|
||||
{showMore && (
|
||||
<Typography variant="caption" display="block" mb={0.5}>
|
||||
Start
|
||||
Start Date
|
||||
</Typography>
|
||||
)}
|
||||
<TextField
|
||||
fullWidth
|
||||
label={!showMore ? "Start" : ""}
|
||||
type={allday ? "date" : "datetime-local"}
|
||||
value={allday ? start.split("T")[0] : start}
|
||||
onChange={(e) => handleStartChange(e.target.value)}
|
||||
error={!!validation.errors.start}
|
||||
helperText={validation.errors.start}
|
||||
label={!showMore ? "Start Date" : ""}
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => handleStartDateChange(e.target.value)}
|
||||
size="small"
|
||||
margin="dense"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
|
||||
{/* Start Time - 20% */}
|
||||
<Box sx={{ flexGrow: 0.2, flexBasis: "20%" }}>
|
||||
{showMore && (
|
||||
<Typography variant="caption" display="block" mb={0.5}>
|
||||
End
|
||||
Start Time
|
||||
</Typography>
|
||||
)}
|
||||
<TextField
|
||||
fullWidth
|
||||
label={!showMore ? "End" : ""}
|
||||
type={allday ? "date" : "datetime-local"}
|
||||
value={allday ? end.split("T")[0] : end}
|
||||
onChange={(e) => handleEndChange(e.target.value)}
|
||||
label={!showMore ? "Start Time" : ""}
|
||||
type="time"
|
||||
value={startTime}
|
||||
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}
|
||||
helperText={validation.errors.dateTime}
|
||||
size="small"
|
||||
margin="dense"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</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>
|
||||
</FieldWithLabel>
|
||||
|
||||
@@ -513,90 +683,124 @@ export default function EventFormFields({
|
||||
let newEnd = end;
|
||||
|
||||
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")) {
|
||||
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
|
||||
if (start && end) {
|
||||
const startDate = start.includes("T")
|
||||
? new Date(start)
|
||||
: new Date(start + "T00:00:00");
|
||||
const endDate = end.includes("T")
|
||||
? new Date(end)
|
||||
: new Date(end + "T00:00:00");
|
||||
// Reset time to empty, keep only date part
|
||||
setStartTime("");
|
||||
setEndTime("");
|
||||
newStart = startDate;
|
||||
newEnd = endDate;
|
||||
|
||||
// Check if dates are valid before proceeding
|
||||
if (
|
||||
!isNaN(startDate.getTime()) &&
|
||||
!isNaN(endDate.getTime())
|
||||
) {
|
||||
// If same day, extend end to next day
|
||||
if (endDate.getDate() === startDate.getDate()) {
|
||||
endDate.setDate(startDate.getDate() + 1);
|
||||
}
|
||||
|
||||
const formattedEnd = formatLocalDateTime(endDate);
|
||||
if (formattedEnd) {
|
||||
newEnd = formattedEnd;
|
||||
}
|
||||
}
|
||||
// If same day, extend end to next day
|
||||
if (startDate === endDate) {
|
||||
const nextDay = new Date(endDate);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
newEnd = nextDay.toISOString().split("T")[0];
|
||||
setEndDate(newEnd); // Update internal endDate state
|
||||
}
|
||||
} else {
|
||||
// ON => OFF: Restore original time if available
|
||||
// ON => OFF: Restore original time AND original end date
|
||||
if (originalTimeRef.current) {
|
||||
// Extract hours/minutes from original time strings
|
||||
const originalStartMatch =
|
||||
originalTimeRef.current.start.match(/T(\d{2}):(\d{2})/);
|
||||
const originalEndMatch =
|
||||
originalTimeRef.current.end.match(/T(\d{2}):(\d{2})/);
|
||||
|
||||
if (originalStartMatch && originalEndMatch) {
|
||||
// Parse current date (YYYY-MM-DD)
|
||||
const currentDate = start.split("T")[0];
|
||||
|
||||
// Reconstruct datetime with original time
|
||||
newStart = `${currentDate}T${originalStartMatch[1]}:${originalStartMatch[2]}`;
|
||||
newEnd = `${currentDate}T${originalEndMatch[1]}:${originalEndMatch[2]}`;
|
||||
}
|
||||
|
||||
// Clear stored time after use
|
||||
originalTimeRef.current = null;
|
||||
} else if (start) {
|
||||
// No original time, use rounded current time
|
||||
const startDate = start.includes("T")
|
||||
? new Date(start)
|
||||
: new Date(start + "T00:00:00");
|
||||
|
||||
// Check if start date is valid
|
||||
if (!isNaN(startDate.getTime())) {
|
||||
// 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}`;
|
||||
|
||||
startDate.setHours(currentTime.getHours());
|
||||
startDate.setMinutes(currentTime.getMinutes());
|
||||
const formattedStart = formatLocalDateTime(startDate);
|
||||
if (formattedStart) {
|
||||
newStart = formattedStart;
|
||||
}
|
||||
newStart = combineDateTime(startDate, timeStr);
|
||||
|
||||
const endDate = new Date(startDate);
|
||||
endDate.setMinutes(endDate.getMinutes() + 30);
|
||||
const formattedEnd = formatLocalDateTime(endDate);
|
||||
if (formattedEnd) {
|
||||
newEnd = formattedEnd;
|
||||
}
|
||||
// End time = start time + 1 hour
|
||||
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}`;
|
||||
|
||||
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) {
|
||||
setStart(newStart);
|
||||
setEnd(newEnd);
|
||||
setAllDay(newAllDay);
|
||||
} else {
|
||||
// Let callback handle all state updates to avoid duplicate renders
|
||||
handleAllDayChange(newAllDay, newStart, newEnd);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -201,6 +201,11 @@ function EventPopover({
|
||||
}
|
||||
|
||||
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
|
||||
// Use selectedRange.startStr and endStr if available (from FullCalendar)
|
||||
if (selectedRange.startStr && selectedRange.endStr) {
|
||||
@@ -249,6 +254,9 @@ function EventPopover({
|
||||
|
||||
if (formattedStart) setStart(formattedStart);
|
||||
if (formattedEnd) setEnd(formattedEnd);
|
||||
|
||||
// Default to non-all-day when no selectedRange
|
||||
setAllDay(false);
|
||||
}
|
||||
|
||||
shouldSyncFromRangeRef.current = false;
|
||||
|
||||
Reference in New Issue
Block a user