diff --git a/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx b/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx index c6c117b124..2bfedc4ae3 100644 --- a/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx @@ -370,6 +370,60 @@ const Schedule = { const { t } = useTranslation(); const currentLanguage = getCurrentLanguageInformation(); + const getEndDateForSchedulingTime = () => { + const { + scheduleStartDate, + scheduleStartHour, + scheduleStartMinute, + scheduleEndDate, + scheduleEndHour, + scheduleEndMinute, + scheduleDurationHours, + sourceMode, + } = formik.values; + + const durationHours = Number(scheduleDurationHours) || 0; + + if (durationHours === 0) { + return undefined; + } + + const startDateTime = new Date(scheduleStartDate); + startDateTime.setHours( + parseInt(scheduleStartHour, 10), + parseInt(scheduleStartMinute, 10), + 0, + 0, + ); + + let endDateTime: Date; + + if (sourceMode === "SCHEDULE_MULTIPLE") { + endDateTime = new Date(startDateTime); + endDateTime.setHours(endDateTime.getHours() + durationHours); + } else { + if (!scheduleEndDate) { + return undefined; + } + endDateTime = new Date(scheduleEndDate); + endDateTime.setHours( + parseInt(scheduleEndHour, 10), + parseInt(scheduleEndMinute, 10), + 0, + 0, + ); + } + + if ( + endDateTime.getDate() !== startDateTime.getDate() || + endDateTime.getMonth() !== startDateTime.getMonth() || + endDateTime.getFullYear() !== startDateTime.getFullYear() + ) { + return "+1 day"; + } + return undefined; + }; + const renderInputDeviceOptions = () => { if (formik.values.location) { @@ -606,13 +660,7 @@ const Schedule = - {/* Displays given date. Can be used to signify which date the scheduling time belong to*/} - {date && + {date && ( - {new Date(date).toLocaleDateString( - currentLanguage ? currentLanguage.dateLocale.code : undefined, - )} + {typeof date === "string" + ? date // show the string as it is + : date instanceof Date && !isNaN(date.getDate()) + ? new Date(date).toLocaleDateString( + currentLanguage ? currentLanguage.dateLocale.code : undefined, + ) + : null + } - } + )} ); diff --git a/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json b/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json index 028706ce3c..4b2843ff91 100644 --- a/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json +++ b/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json @@ -211,6 +211,7 @@ "CONFLICT_ALREADY_ENDED": "Scheduling error: The event has already ended.", "CONFLICT_END_BEFORE_START": "Scheduling error: Schedule end has to be later than the start.", "CONFLICT_IN_THE_PAST": "The schedule could not be updated: You cannot schedule an event to be in the past.", + "CONFLICT_RANGE_DAYS":"At least one repeat day must be within the scheduled date range.", "INVALID_ACL_RULES": "Rules have to contain a valid role and read or/and write right(s).", "MISSING_ACL_RULES": "At least one role with Read and Write permissions is required!", "SAVED_ACL_RULES": "The access rules have been saved.", diff --git a/src/slices/eventSlice.ts b/src/slices/eventSlice.ts index 30a1a31d14..baa4a318bc 100644 --- a/src/slices/eventSlice.ts +++ b/src/slices/eventSlice.ts @@ -910,6 +910,20 @@ export const checkConflicts = (values: { values.sourceMode === "SCHEDULE_SINGLE" || values.sourceMode === "SCHEDULE_MULTIPLE" ) { + if (values.sourceMode === "SCHEDULE_MULTIPLE") { + const isRepeatOnValid = validateRepeatOnInRange(values); + if (!isRepeatOnValid) { + dispatch( + addNotification({ + type: "error", + key: "CONFLICT_RANGE_DAYS", + duration: -1, + context: NOTIFICATION_CONTEXT, + }), + ); + return false; // Exit early if repeatOn is invalid + } + } // Get timezone offset; Checks should be performed on UTC times // let offset = getTimezoneOffset(); @@ -924,7 +938,7 @@ export const checkConflicts = (values: { ); // If start date of event is smaller than today --> Event is in past - if (values.sourceMode === "SCHEDULE_SINGLE" && startDate < new Date()) { + if ((values.sourceMode === "SCHEDULE_SINGLE" && startDate < new Date()) || (values.sourceMode === "SCHEDULE_MULTIPLE" && startDate < new Date())) { dispatch( addNotification({ type: "error", @@ -1107,6 +1121,42 @@ export const checkForSchedulingConflicts = (events: EditedEvents[]) => (dispatch return data; }; +export const validateRepeatOnInRange = (values: { + repeatOn: string[], // e.g. ["MO", "TU"] + scheduleStartDate: string, // e.g. "2025-08-11" + scheduleEndDate: string // e.g. "2025-08-13" +}) => { + if (!values.repeatOn || values.repeatOn.length === 0) { + return true; + } + + const start = new Date(values.scheduleStartDate); + const end = new Date(values.scheduleEndDate); + + // Map day codes to numbers: Sunday=0, Monday=1, ..., Saturday=6 + const dayMap: Record = { + SU: 0, + MO: 1, + TU: 2, + WE: 3, + TH: 4, + FR: 5, + SA: 6, + }; + const repeatDays = values.repeatOn.map(day => dayMap[day]); + // Check if **at least one** date in the [start..end] range matches repeatOn days + const current = new Date(start); + while (current <= end) { + if (repeatDays.includes(current.getDay())) { + return true; // Valid because this repeat day is in range + } + current.setDate(current.getDate() + 1); // next day + } + + // If no repeat days fall within the range, return false + return false; +}; + const eventSlice = createSlice({ name: "events", initialState,