diff --git a/src/components/Event/EventFormFields.tsx b/src/components/Event/EventFormFields.tsx index bba2d7a..5e9d0d9 100644 --- a/src/components/Event/EventFormFields.tsx +++ b/src/components/Event/EventFormFields.tsx @@ -38,6 +38,7 @@ import { FieldWithLabel } from "./components/FieldWithLabel"; import { DateTimeFields } from "./components/DateTimeFields"; import { useAllDayToggle } from "./hooks/useAllDayToggle"; import { splitDateTime, combineDateTime } from "./utils/dateTimeHelpers"; +import { getEndDateForStartChange } from "./utils/dateRules"; import { formatLocalDateTime, formatDateTimeInTimezone, @@ -163,6 +164,9 @@ export default function EventFormFields({ const [endDate, setEndDate] = React.useState(""); const [endTime, setEndTime] = React.useState(""); + // UI state for showing/hiding end date field + const [showEndDate, setShowEndDate] = React.useState(false); + // Use all-day toggle hook const { originalTimeRef, handleAllDayToggle } = useAllDayToggle({ allday, @@ -268,13 +272,25 @@ export default function EventFormFields({ (newDate: string) => { setStartDate(newDate); const newStart = combineDateTime(newDate, startTime); + + // Update start if (onStartChange) { onStartChange(newStart); } else { setStart(newStart); } + + // Rule: update end date when start date changes + const nextEndDate = getEndDateForStartChange(newDate, allday); + setEndDate(nextEndDate); + const newEnd = combineDateTime(nextEndDate, endTime || startTime); + if (onEndChange) { + onEndChange(newEnd); + } else { + setEnd(newEnd); + } }, - [startTime, onStartChange, setStart] + [startTime, endTime, onStartChange, onEndChange, setStart, setEnd, allday] ); const handleStartTimeChange = React.useCallback( @@ -343,6 +359,35 @@ export default function EventFormFields({ onValidationChange?.(validation.isValid); }, [validateForm, onValidationChange]); + // Auto-calculate end date from start date if not already set + React.useEffect(() => { + if (startDate && !endDate) { + setEndDate(startDate); + const newEnd = combineDateTime(startDate, endTime || startTime); + if (onEndChange) { + onEndChange(newEnd); + } else { + setEnd(newEnd); + } + } + }, [startDate, endDate, startTime, endTime, onEndChange, setEnd]); + + // Auto-calculate end time from start time (+ 1 hour) if not already set + React.useEffect(() => { + if (startTime && !endTime && !allday) { + const [hours, minutes] = startTime.split(":"); + const endHour = (parseInt(hours) + 1) % 24; + const calculatedEndTime = `${endHour.toString().padStart(2, "0")}:${minutes}`; + setEndTime(calculatedEndTime); + const newEnd = combineDateTime(endDate || startDate, calculatedEndTime); + if (onEndChange) { + onEndChange(newEnd); + } else { + setEnd(newEnd); + } + } + }, [startTime, endTime, allday, endDate, startDate, onEndChange, setEnd]); + const validation = validateForm(); const handleAddVideoConference = () => { @@ -465,6 +510,8 @@ export default function EventFormFields({ onStartTimeChange={handleStartTimeChange} onEndDateChange={handleEndDateChange} onEndTimeChange={handleEndTimeChange} + showEndDate={showEndDate} + onToggleEndDate={() => setShowEndDate(!showEndDate)} /> diff --git a/src/components/Event/components/DateTimeFields.tsx b/src/components/Event/components/DateTimeFields.tsx index a306fe2..0e741d0 100644 --- a/src/components/Event/components/DateTimeFields.tsx +++ b/src/components/Event/components/DateTimeFields.tsx @@ -1,9 +1,10 @@ import React from "react"; -import { Box, Typography } from "@mui/material"; +import { Box, IconButton, Tooltip, Typography } from "@mui/material"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { TimePicker } from "@mui/x-date-pickers/TimePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; +import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import moment, { Moment } from "moment"; /** @@ -16,6 +17,8 @@ export interface DateTimeFieldsProps { endTime: string; allday: boolean; showMore: boolean; + showEndDate: boolean; + onToggleEndDate: () => void; validation: { errors: { dateTime: string; @@ -38,6 +41,8 @@ export const DateTimeFields: React.FC = ({ endTime, allday, showMore, + showEndDate, + onToggleEndDate, validation, onStartDateChange, onStartTimeChange, @@ -46,18 +51,18 @@ export const DateTimeFields: React.FC = ({ }) => { return ( - + {/* First row: 4 fields */} - + {/* Start Date - 30% */} - - {showMore && ( - - Start Date - - )} + { onStartDateChange(newValue?.format("YYYY-MM-DD") || ""); @@ -68,22 +73,17 @@ export const DateTimeFields: React.FC = ({ margin: "dense" as const, fullWidth: true, InputLabelProps: { shrink: true }, - "data-testid": "start-date-input", sx: { width: "100%" }, + inputProps: { "data-testid": "start-date-input" }, }, }} /> {/* Start Time - 20% */} - - {showMore && ( - - Start Time - - )} + { onStartTimeChange(newValue?.format("HH:mm") || ""); @@ -95,22 +95,17 @@ export const DateTimeFields: React.FC = ({ margin: "dense" as const, fullWidth: true, InputLabelProps: { shrink: true }, - "data-testid": "start-time-input", sx: { width: "100%" }, + inputProps: { "data-testid": "start-time-input" }, }, }} /> {/* End Time - 20% */} - - {showMore && ( - - End Time - - )} + { onEndTimeChange(newValue?.format("HH:mm") || ""); @@ -123,38 +118,53 @@ export const DateTimeFields: React.FC = ({ fullWidth: true, InputLabelProps: { shrink: true }, error: !!validation.errors.dateTime, - "data-testid": "end-time-input", sx: { width: "100%" }, + inputProps: { "data-testid": "end-time-input" }, }, }} /> - {/* End Date - 30% */} - - {showMore && ( - - End Date - + {/* End Date - Conditional rendering */} + + {!showEndDate ? ( + // Show "..." button to reveal end date + + + + + + + + ) : ( + // Show End Date picker (no hide button) + { + onEndDateChange(newValue?.format("YYYY-MM-DD") || ""); + }} + slotProps={{ + textField: { + size: "small", + margin: "dense" as const, + fullWidth: true, + InputLabelProps: { shrink: true }, + error: !!validation.errors.dateTime, + sx: { width: "100%" }, + inputProps: { "data-testid": "end-date-input" }, + }, + }} + /> )} - { - onEndDateChange(newValue?.format("YYYY-MM-DD") || ""); - }} - slotProps={{ - textField: { - size: "small", - margin: "dense" as const, - fullWidth: true, - InputLabelProps: { shrink: true }, - error: !!validation.errors.dateTime, - "data-testid": "end-date-input", - sx: { width: "100%" }, - }, - }} - /> diff --git a/src/components/Event/hooks/useAllDayToggle.ts b/src/components/Event/hooks/useAllDayToggle.ts index 5a2e953..3ddfa23 100644 --- a/src/components/Event/hooks/useAllDayToggle.ts +++ b/src/components/Event/hooks/useAllDayToggle.ts @@ -1,5 +1,6 @@ import React from "react"; import { combineDateTime } from "../utils/dateTimeHelpers"; +import { getEndDateForToggle } from "../utils/dateRules"; import { getRoundedCurrentTime } from "../utils/dateTimeFormatters"; /** @@ -86,15 +87,15 @@ export function useAllDayToggle( setStartTime(""); setEndTime(""); newStart = startDate; - newEnd = endDate; - - // 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 - } + const nextEndDate = getEndDateForToggle({ + nextAllDay: true, + fromAllDaySlot: !!originalTimeRef.current?.fromAllDaySlot, + startDate, + previousEndDate: endDate, + originalEndDate: originalTimeRef.current?.endDate, + }); + newEnd = nextEndDate; + setEndDate(nextEndDate); } else { // ON => OFF: Restore original time AND original end date if (originalTimeRef.current) { @@ -123,7 +124,12 @@ export function useAllDayToggle( setEndDate(startDate); // Set endDate = startDate } else { // Normal case: restore original time AND original end date - const restoredEndDate = originalTimeRef.current.endDate || endDate; + const restoredEndDate = getEndDateForToggle({ + nextAllDay: false, + startDate, + previousEndDate: endDate, + originalEndDate: originalTimeRef.current.endDate, + }); newStart = combineDateTime(startDate, originalTimeRef.current.start); newEnd = combineDateTime( diff --git a/src/components/Event/utils/dateRules.ts b/src/components/Event/utils/dateRules.ts new file mode 100644 index 0000000..4f821c8 --- /dev/null +++ b/src/components/Event/utils/dateRules.ts @@ -0,0 +1,62 @@ +// Date rules helpers to normalize end date/time behavior + +import { combineDateTime } from "./dateTimeHelpers"; + +/** Adds a number of days to a YYYY-MM-DD string and returns YYYY-MM-DD */ +export function addDays(dateStr: string, days: number): string { + const d = new Date(dateStr); + d.setDate(d.getDate() + days); + return d.toISOString().split("T")[0]; +} + +/** + * Compute endDate (YYYY-MM-DD) when startDate changes + * - If all-day: endDate = startDate + 1 day + * - Else: endDate = startDate + */ +export function getEndDateForStartChange( + startDate: string, + isAllDay: boolean +): string { + if (!startDate) return ""; + return isAllDay ? addDays(startDate, 1) : startDate; +} + +/** + * Compute new start/end (ISO-like strings) when toggling all-day + * - nextAllDay = true: + * - If fromAllDaySlot: endDate = startDate + * - Else: endDate = startDate + 1 day + * - Times are cleared (date-only) + * - nextAllDay = false: + * - end restored from originalEndDate if any, else previousEndDate, else start + */ +export function getEndDateForToggle(params: { + nextAllDay: boolean; + fromAllDaySlot?: boolean; + startDate: string; + previousEndDate: string; + originalEndDate?: string; +}): string { + const { + nextAllDay, + fromAllDaySlot, + startDate, + previousEndDate, + originalEndDate, + } = params; + if (nextAllDay) { + return fromAllDaySlot ? startDate : addDays(startDate, 1); + } + return originalEndDate || previousEndDate || startDate; +} + +/** Utility to combine date with a fallback time (HH:mm) safely */ +export function combineWithFallback( + dateStr: string, + timeHHmm: string | undefined, + fallbackTime: string +): string { + const time = timeHHmm && timeHHmm.trim() ? timeHHmm : fallbackTime; + return combineDateTime(dateStr, time); +}