diff --git a/zmsadmin/js/page/availabilityDay/form/datepicker.js b/zmsadmin/js/page/availabilityDay/form/datepicker.js index f81bfe4d0..22b13c683 100644 --- a/zmsadmin/js/page/availabilityDay/form/datepicker.js +++ b/zmsadmin/js/page/availabilityDay/form/datepicker.js @@ -174,10 +174,6 @@ class AvailabilityDatePicker extends Component } handleTimeChange(name, date) { - if (!date) { - this.closeDatePicker(); - return; - } if ('startDate' == name) { if (this.state.availability.startTime != moment(date).format('HH:mm')) { this.props.onChange("startTime", moment(date).format('HH:mm')); diff --git a/zmsadmin/js/page/availabilityDay/form/errors.js b/zmsadmin/js/page/availabilityDay/form/errors.js index 67f225527..7dfa3dfd4 100644 --- a/zmsadmin/js/page/availabilityDay/form/errors.js +++ b/zmsadmin/js/page/availabilityDay/form/errors.js @@ -1,15 +1,20 @@ -import React from 'react' -import PropTypes from 'prop-types' +import React from 'react'; +import PropTypes from 'prop-types'; -const renderErrors = errors => Object.keys(errors).map(key => { - return ( +const renderErrors = (errors) => + Object.keys(errors).map(key => (
{errors[key].itemList.map((item, index) => { - return
{item[0].message}
- })} + if (Array.isArray(item)) { + return item.map((nestedItem, nestedIndex) => ( +
{nestedItem.message}
+ )); + } else { + return
{item.message}
; + } + })}
- ) -}) + )); const Errors = (props) => { return ( @@ -18,15 +23,15 @@ const Errors = (props) => {

Folgende Fehler sind bei der Prüfung Ihrer Eingaben aufgetreten:

{renderErrors(props.errorList)} : null - ) -} + ); +}; Errors.defaultProps = { - errorList: [] -} + errorList: {} +}; Errors.propTypes = { errorList: PropTypes.object -} +}; -export default Errors +export default Errors; diff --git a/zmsadmin/js/page/availabilityDay/form/validate.js b/zmsadmin/js/page/availabilityDay/form/validate.js index 48c35e13c..daa4025e3 100644 --- a/zmsadmin/js/page/availabilityDay/form/validate.js +++ b/zmsadmin/js/page/availabilityDay/form/validate.js @@ -16,10 +16,8 @@ const validate = (data, props) => { itemList: [] }; - // Add new validation functions for null and format checks errorList.itemList.push(validateNullValues(data)); errorList.itemList.push(validateTimestampAndTimeFormats(data)); - errorList.itemList.push(validateStartTime(today, tomorrow, selectedDate, data)); errorList.itemList.push(validateEndTime(today, yesterday, selectedDate, data)); errorList.itemList.push(validateOriginEndTime(today, yesterday, selectedDate, data)); @@ -35,7 +33,6 @@ const validate = (data, props) => { }; }; -// Validate if startDate, endDate, startTime, and endTime are not null function validateNullValues(data) { let errorList = []; @@ -70,50 +67,71 @@ function validateNullValues(data) { return errorList; } -// Validate timestamps and time formats function validateTimestampAndTimeFormats(data) { let errorList = []; - const timeRegex = /^\d{2}:\d{2}(:\d{2})?$/; // HH:mm:ss or HH:mm + const timeRegex = /^\d{2}:\d{2}(:\d{2})?$/; + + let isStartDateValid = isValidTimestamp(data.startDate); + let isEndDateValid = isValidTimestamp(data.endDate); - // Validate startDate and endDate as valid timestamps - if (!isValidTimestamp(data.startDate)) { + if (!isStartDateValid) { errorList.push({ type: 'startDateInvalid', message: 'Das Startdatum ist kein gültiger Zeitstempel.' }); } - if (!isValidTimestamp(data.endDate)) { + if (!isEndDateValid) { errorList.push({ type: 'endDateInvalid', message: 'Das Enddatum ist kein gültiger Zeitstempel.' }); } - // Validate startTime and endTime format - if (data.startTime && !timeRegex.test(data.startTime)) { + if (data.startTime) { + if (!timeRegex.test(data.startTime)) { + errorList.push({ + type: 'startTimeFormat', + message: 'Die Startzeit muss im Format "HH:mm:ss" oder "HH:mm" vorliegen.' + }); + } + } else { errorList.push({ - type: 'startTimeFormat', - message: 'Die Startzeit muss im Format "HH:mm:ss" oder "HH:mm" vorliegen.' + type: 'startTimeMissing', + message: 'Die Startzeit darf nicht leer sein.' }); } - if (data.endTime && !timeRegex.test(data.endTime)) { + if (data.endTime) { + if (!timeRegex.test(data.endTime)) { + errorList.push({ + type: 'endTimeFormat', + message: 'Die Endzeit muss im Format "HH:mm:ss" oder "HH:mm" vorliegen.' + }); + } + } else { errorList.push({ - type: 'endTimeFormat', - message: 'Die Endzeit muss im Format "HH:mm:ss" oder "HH:mm" vorliegen.' + type: 'endTimeMissing', + message: 'Die Endzeit darf nicht leer sein.' }); } + if (isStartDateValid && isEndDateValid) { + if (new Date(data.startDate) > new Date(data.endDate)) { + errorList.push({ + type: 'dateOrderInvalid', + message: 'Das Startdatum darf nicht nach dem Enddatum liegen.' + }); + } + } + return errorList; } -// Helper function to check if a timestamp is valid function isValidTimestamp(timestamp) { return !isNaN(timestamp) && moment.unix(timestamp).isValid(); } -// Parse timestamp and time into moment objects function parseTimestampAndTime(dateTimestamp, timeStr) { const date = moment.unix(dateTimestamp); if (!date.isValid()) return null; @@ -122,48 +140,6 @@ function parseTimestampAndTime(dateTimestamp, timeStr) { return date.set({ hour: hours, minute: minutes, second: seconds }); } -// Example usage in validateStartTime and validateEndTime -function validateStartTime(today, tomorrow, selectedDate, data) { - let errorList = []; - const startTime = parseTimestampAndTime(data.startDate, data.startTime); - const isFuture = data.kind && data.kind === 'future'; - - if (!startTime) { - errorList.push({ - type: 'startTimeInvalid', - message: 'Ungültige Startzeit oder Startdatum.' - }); - } else if (!isFuture && startTime.isAfter(today, 'minute')) { - errorList.push({ - type: 'startTimeFuture', - message: `Das Startdatum der Öffnungszeit muss vor dem ${tomorrow.format('DD.MM.YYYY')} liegen.` - }); - } - - return errorList; -} - -function validateEndTime(today, yesterday, selectedDate, data) { - let errorList = []; - const endTime = parseTimestampAndTime(data.endDate, data.endTime); - const startTime = parseTimestampAndTime(data.startDate, data.startTime); - - if (!endTime) { - errorList.push({ - type: 'endTimeInvalid', - message: 'Ungültige Endzeit oder Enddatum.' - }); - } else if (startTime && endTime.isBefore(startTime)) { - errorList.push({ - type: 'endTime', - message: 'Das Enddatum darf nicht vor dem Startdatum liegen.' - }); - } - - return errorList; -} - - function validateStartTime(today, tomorrow, selectedDate, data) { let errorList = [] const startTime = moment(data.startDate, 'X').startOf('day'); @@ -181,15 +157,15 @@ function validateStartTime(today, tomorrow, selectedDate, data) { message: `Das Startdatum der Öffnungszeit muss vor dem ${tomorrow.format('DD.MM.YYYY')} liegen.` }) } -/* - if (isOrigin && startTime.isBefore(today.startOf('day'), 'day') && data.__modified) { - errorList.push({ - type: 'startTimeOrigin', - message: 'Öffnungszeiten in der Vergangenheit lassen sich nicht bearbeiten ' - + '(Der Terminanfang am "'+startDateTime.format('DD.MM.YYYY')+' liegt vor dem heutigen Tag").' - }) - } -*/ + /* + if (isOrigin && startTime.isBefore(today.startOf('day'), 'day') && data.__modified) { + errorList.push({ + type: 'startTimeOrigin', + message: 'Öffnungszeiten in der Vergangenheit lassen sich nicht bearbeiten ' + + '(Der Terminanfang am "'+startDateTime.format('DD.MM.YYYY')+' liegt vor dem heutigen Tag").' + }) + } + */ if ((startHour == "00" && startMinute == "00") || (endHour == "00" && endMinute == "00")) { errorList.push({ @@ -218,12 +194,15 @@ function validateEndTime(today, yesterday, selectedDate, data) { type: 'endTime', message: 'Die Endzeit darf nicht vor der Startzeit liegen.' }) - } else if (startTimestamp >= endTimestamp) { + } + + if (startTimestamp >= endTimestamp) { errorList.push({ type: 'endTime', message: 'Das Enddatum darf nicht vor dem Startdatum liegen.' }) } + return errorList; } diff --git a/zmsadmin/js/page/availabilityDay/index.js b/zmsadmin/js/page/availabilityDay/index.js index b5d26c62e..2b610d878 100644 --- a/zmsadmin/js/page/availabilityDay/index.js +++ b/zmsadmin/js/page/availabilityDay/index.js @@ -179,7 +179,6 @@ class AvailabilityPage extends Component { } } - onRevertUpdates() { this.isCreatingExclusion = false this.setState(Object.assign({}, getInitialState(this.props), { @@ -196,18 +195,14 @@ class AvailabilityPage extends Component { const id = availability.id; if (ok) { - // Format the selected date const selectedDate = formatTimestampDate(this.props.timestamp); - // Prepare the data to send const sendAvailability = Object.assign({}, availability); - // Clean up temporary fields if (sendAvailability.tempId) { delete sendAvailability.tempId; } - // Include 'kind' and wrap it in 'availabilityList' const payload = { availabilityList: [ { @@ -220,7 +215,6 @@ class AvailabilityPage extends Component { console.log('Updating single availability', payload); - // Make the AJAX request $.ajax(`${this.props.links.includeurl}/availability/save/${id}/`, { method: 'POST', data: JSON.stringify(payload), @@ -495,72 +489,110 @@ class AvailabilityPage extends Component { return hasError || hasConflict; } - getValidationList(list = []) { - const validateData = data => { - let validationResult = validate(data, this.props) - if (!validationResult.valid) { - return validationResult.errorList - - } - return []; - } - - this.state.availabilitylist.map(availability => { - list.push(validateData(availability)) - }) - list = list.filter(el => el.id) - - this.setState({ - errorList: list.length ? Object.assign({}, list) : {} - }, () => { - if (list.length) { - this.errorElement.scrollIntoView() - } - }) - } - - getConflictList() { - const requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(Object.assign({}, { - availabilityList: this.state.availabilitylist, - selectedDate: formatTimestampDate(this.props.timestamp), - selectedAvailability: this.state.selectedAvailability - })) - }; - const url = `${this.props.links.includeurl}/availability/conflicts/`; - fetch(url, requestOptions) - .then(res => res.json()) - .then( - (data) => { - this.setState({ - conflictList: Object.assign({}, - { - itemList: Object.assign({}, data.conflictList), - conflictIdList: data.conflictIdList - } - ) - }) - if (data.conflictIdList.length > 0) { - this.errorElement.scrollIntoView() - } + getValidationList() { + return new Promise((resolve, reject) => { + const validateData = (data) => { + const validationResult = validate(data, this.props); + if (!validationResult.valid) { + return validationResult.errorList; + } + return []; + }; + + const list = this.state.availabilitylist + .map(validateData) + .flat(); + + console.log("Validation list:", list); + + this.setState( + { + errorList: list.length ? list : [], }, - (err) => { - let isException = err.responseText.toLowerCase().includes('exception'); - if (err.status >= 500 && isException) { - new ExceptionHandler($('.opened'), { - code: err.status, - message: err.responseText - }); + () => { + if (list.length > 0) { + console.warn("Validation failed with errors:", list); + this.errorElement?.scrollIntoView(); + //reject(new Error("Validation failed")); // Reject with an error object } else { - console.log('conflict error', err); + console.log("Validation passed."); + resolve(); } - hideSpinner(); } - ) + ); + }); + } + + validateAvailabilityList(availabilitylist) { + const timeRegex = /^\d{2}:\d{2}(:\d{2})?$/; + + const isValidTimestamp = (timestamp) => !isNaN(timestamp) && moment.unix(timestamp).isValid(); + + const invalidAvailabilities = availabilitylist.filter((availability) => { + const hasInvalidDates = + !isValidTimestamp(availability.startDate) || !isValidTimestamp(availability.endDate); + const hasInvalidTimes = + !timeRegex.test(availability.startTime) || !timeRegex.test(availability.endTime); + + if (hasInvalidDates || hasInvalidTimes) { + console.warn("Invalid availability detected:", availability); + } + + return hasInvalidDates || hasInvalidTimes; + }); + + return invalidAvailabilities; } + getConflictList() { + this.getValidationList() + .then(() => { + const { availabilitylist, selectedAvailability } = this.state; + const { timestamp } = this.props; + + console.log("Validation passed. Proceeding with /availability/conflicts/."); + + selectedAvailability.startTime = moment(selectedAvailability.startTime, ['HH:mm:ss', 'HH:mm']).format('HH:mm'); + selectedAvailability.endTime = moment(selectedAvailability.endTime, ['HH:mm:ss', 'HH:mm']).format('HH:mm'); + + const requestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + availabilityList: availabilitylist, + selectedDate: formatTimestampDate(timestamp), + selectedAvailability, + }), + }; + + const url = `${this.props.links.includeurl}/availability/conflicts/`; + + fetch(url, requestOptions) + .then((res) => res.json()) + .then( + (data) => { + console.log("Conflicts fetched successfully:", data); + this.setState({ + conflictList: { + itemList: { ...data.conflictList }, + conflictIdList: data.conflictIdList, + }, + }); + if (data.conflictIdList.length > 0) { + this.errorElement?.scrollIntoView(); + } + }, + (err) => { + console.error("Conflict fetch error:", err); + hideSpinner(); + } + ); + }) + .catch((error) => { + console.warn("Validation failed. Conflict fetch aborted.", error); + }); + } + renderTimeTable() { const onSelect = data => { this.onSelectAvailability(data) @@ -586,41 +618,55 @@ class AvailabilityPage extends Component { } readCalculatedAvailabilityList() { - $.ajax(`${this.props.links.includeurl}/availability/slots/`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - data: JSON.stringify({ - 'availabilityList': this.state.availabilitylist, - 'busySlots': this.state.busyslots - }) - }).done((responseData) => { - let availabilityList = writeSlotCalculationIntoAvailability( - this.state.availabilitylist, - responseData['maxSlots'], - responseData['busySlots'] - ); - this.setState({ - availabilitylistslices: availabilityList, - maxWorkstationCount: parseInt(responseData['maxWorkstationCount']), - }) - }).fail((err) => { - if (err.status === 404) { - console.log('404 error, ignored') - } else { - let isException = err.responseText.toLowerCase().includes('exception'); - if (err.status >= 500 && isException) { - new ExceptionHandler($('.opened'), { - code: err.status, - message: err.responseText + this.getValidationList() + .then(() => { + const { availabilitylist, busyslots } = this.state; + + console.log("Validation passed. Proceeding with /availability/slots/."); + + $.ajax(`${this.props.links.includeurl}/availability/slots/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: JSON.stringify({ + availabilityList: availabilitylist, + busySlots: busyslots, + }), + }) + .done((responseData) => { + console.log("Slots fetched successfully:", responseData); + const availabilityList = writeSlotCalculationIntoAvailability( + this.state.availabilitylist, + responseData['maxSlots'], + responseData['busySlots'] + ); + this.setState({ + availabilitylistslices: availabilityList, + maxWorkstationCount: parseInt(responseData['maxWorkstationCount']), }); - } else { - console.log('reading calculated availability list error', err); - } - hideSpinner(); - } - }) - } - + }) + .fail((err) => { + console.error("Error during /availability/slots/ fetch:", err); + if (err.status === 404) { + console.log("404 error ignored."); + } else { + const isException = err.responseText.toLowerCase().includes("exception"); + if (err.status >= 500 && isException) { + new ExceptionHandler($(".opened"), { + code: err.status, + message: err.responseText, + }); + } + } + hideSpinner(); + }); + }) + .catch((error) => { + console.warn("Validation failed. Slot calculation fetch aborted.", error); + this.setState({ errorList: error }); + this.errorElement?.scrollIntoView(); + }); + } + handleChange(data) { if (data.__modified) { clearTimeout(this.timer) diff --git a/zmsentities/src/Zmsentities/Availability.php b/zmsentities/src/Zmsentities/Availability.php index 5561db327..2dd33f895 100644 --- a/zmsentities/src/Zmsentities/Availability.php +++ b/zmsentities/src/Zmsentities/Availability.php @@ -450,8 +450,6 @@ public function hasDateBetween(\DateTimeInterface $startTime, \DateTimeInterface } while ($startTime->getTimestamp() <= $stopTime->getTimestamp()); return false; } - - public function validateStartTime(\DateTimeInterface $today, \DateTimeInterface $tomorrow, \DateTimeInterface $startDate, \DateTimeInterface $endDate, \DateTimeInterface $selectedDate, String $kind) { $errorList = []; @@ -482,9 +480,7 @@ public function validateStartTime(\DateTimeInterface $today, \DateTimeInterface } return $errorList; - } - - + } public function validateEndTime(\DateTimeInterface $today, \DateTimeInterface $yesterday, \DateTimeInterface $startDate, \DateTimeInterface $endDate, \DateTimeInterface $selectedDate) { $errorList = []; @@ -497,8 +493,7 @@ public function validateEndTime(\DateTimeInterface $today, \DateTimeInterface $y $dayMinutesEnd = ($endHour * 60) + $endMinute; $startTimestamp = $startDate->getTimestamp(); $endTimestamp = $endDate->getTimestamp(); - - // Check if end time is before start time + if ($dayMinutesEnd <= $dayMinutesStart) { $errorList[] = [ 'type' => 'endTime', @@ -514,7 +509,6 @@ public function validateEndTime(\DateTimeInterface $today, \DateTimeInterface $y return $errorList; } - public function validateOriginEndTime(\DateTimeInterface $today, \DateTimeInterface $yesterday, \DateTimeInterface $startDate, \DateTimeInterface $endDate, \DateTimeInterface $selectedDate, String $kind) { $errorList = [];