Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ZMS-3253 ZMS-3466 ZMS-3415 ZMS-1891): Replicate frontend validation in the backend for the availability opening hours to improve data integrity and frontend validation messaging #812

Conversation

ThomasAFink
Copy link
Member

@ThomasAFink ThomasAFink commented Jan 31, 2025

Pull Request Checklist (Feature Branch to next):

  • Ich habe die neuesten Änderungen aus dem next Branch in meinen Feature-Branch gemergt.
  • Das Code-Review wurde abgeschlossen.
  • Fachliche Tests wurden durchgeführt und sind abgeschlossen.

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced availability management with more robust conflict detection and validation.
    • Improved error handling for availability creation and updates.
    • Added detailed validation for start times, end times, and availability types.
    • Introduced a new state variable for visibility in the SaveBar component.
    • New Twig templates for user-friendly error messages when availability actions fail.
  • Bug Fixes

    • Corrected time slot calculations and conflict resolution logic.
    • Fixed issues with overlapping availability entries.
    • Improved handling of edge cases in time and date selection.
    • Resolved rendering issues for conflict messages.
  • User Experience

    • More informative error messages for availability conflicts.
    • Enhanced form validation with clearer feedback.
    • Improved time selection constraints.
    • Updated rendering logic for error messages to improve clarity.
  • Technical Enhancements

    • Updated type hinting and method signatures.
    • Added comprehensive validation methods for availability entities.
    • Improved error handling across frontend and backend components.
    • Introduced new props for enhanced button state management in forms.

Tom Fink added 30 commits November 12, 2024 12:25
@ThomasAFink
Copy link
Member Author

ThomasAFink commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink The issue is in the filterPassedTime function. Here's the fix:

const filterPassedTime = (time) => {
    // For future dates, only check maintenance window
    if (!moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
        const hour = time.getHours();
        const minutes = time.getMinutes();
        // Block maintenance window (22:00-01:00)
        return !(hour >= 22 || hour < 1 || (hour === 22 && minutes > 0));
    }

    // For today, check both maintenance window and buffer
    const currentTime = moment();
    const timeToCheck = moment(time);
    const minutesDiff = timeToCheck.diff(currentTime, 'minutes');
    const bufferMinutes = this.state.availability.slotTimeInMinutes;
    
    return minutesDiff >= bufferMinutes;
};

Key changes:

  1. For future dates, only check maintenance window
  2. For today, keep the buffer check
  3. Simplified maintenance window check

This will enable all valid time slots in the future while still blocking:

  • Past times on current day
  • Maintenance window (22:00-01:00)
  • Times that conflict with existing availabilities (via excludeTimes)

✏️ Learnings added
🧠 Learnings used

Now the intervals are 90 minutes again and why is 19:30 disabled and 21:00 enabled? @coderabbitai

                            <DatePicker
                                name={this.props.name + "_time"}
                                locale="de"
                                className="form-control form-input"
                                ariaDescribedBy={"help_" + this.props.attributes.id + "_time"}
                                id={this.props.attributes.id + "_time"}
                                selected={this.state.selectedDate}
                                onChange={date => { this.handleTimeChange(this.props.name, date) }}
                                showTimeSelect
                                showTimeSelectOnly
                                dateFormat="HH:mm"
                                timeFormat="HH:mm"
                                timeIntervals={this.state.availability.slotTimeInMinutes > 30 ?
                                    this.state.availability.slotTimeInMinutes : 30} // Use 30 min intervals for better UX
                                timeCaption="Uhrzeit"
                                minTime={this.state.minTime}
                                maxTime={this.state.maxTime}
                                excludeTimes={this.state.excludeTimeList}
                                filterTime={(time) => {
                                    // Only check buffer for current day
                                    if (moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
                                        const currentTime = moment();
                                        const timeToCheck = moment(time);
                                        const minutesDiff = timeToCheck.diff(currentTime, 'minutes');
                                        return minutesDiff >= this.state.availability.slotTimeInMinutes;
                                    }
                                    return true;
                                }}
                                disabled={this.props.attributes.disabled}
                                onInputClick={this.openTimePicker}
                                onKeyDown={this.tpKeyDownHandler}
                                onClickOutside={this.closeTimePicker}
                                strictParsing={true}
                                open={this.state.timePickerIsOpen}
                                ref={(timepicker) => { this.timepicker = timepicker }}
                            />

@it-at-m it-at-m deleted a comment from coderabbitai bot Feb 4, 2025
Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Oops, something went wrong! Please try again later. 🐰 💔

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix for the DatePicker component:

<DatePicker
    name={this.props.name + "_time"}
    locale="de"
    className="form-control form-input"
    ariaDescribedBy={"help_" + this.props.attributes.id + "_time"}
    id={this.props.attributes.id + "_time"}
    selected={this.state.selectedDate}
    onChange={date => { this.handleTimeChange(this.props.name, date) }}
    showTimeSelect
    showTimeSelectOnly
    dateFormat="HH:mm"
    timeFormat="HH:mm"
    timeIntervals={30}  // Always use 30-minute intervals for better UX
    timeCaption="Uhrzeit"
    excludeTimes={this.state.excludeTimeList}
    filterTime={(time) => {
        const hour = time.getHours();
        const minutes = time.getMinutes();
        
        // Block maintenance window (22:00-01:00)
        if (hour >= 22 || hour < 1 || (hour === 22 && minutes > 0)) {
            return false;
        }
        
        // Check buffer only for current day
        if (moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
            const currentTime = moment();
            const timeToCheck = moment(time);
            const minutesDiff = timeToCheck.diff(currentTime, 'minutes');
            if (minutesDiff < this.state.availability.slotTimeInMinutes) {
                return false;
            }
        }
        
        // Ensure time aligns with slot size
        const totalMinutes = hour * 60 + minutes;
        return totalMinutes % this.state.availability.slotTimeInMinutes === 0;
    }}
    disabled={this.props.attributes.disabled}
    onInputClick={this.openTimePicker}
    onKeyDown={this.tpKeyDownHandler}
    onClickOutside={this.closeTimePicker}
    strictParsing={true}
    open={this.state.timePickerIsOpen}
    ref={(timepicker) => { this.timepicker = timepicker }}
/>

Key changes:

  1. Fixed timeIntervals to 30 minutes for consistent UI
  2. Removed minTime/maxTime props
  3. Added maintenance window check in filterTime
  4. Added slot alignment validation
  5. Improved buffer check logic

This will:

  • Show 30-minute intervals for better UX
  • Enable all valid time slots
  • Block only maintenance window, past times, and conflicting slots
  • Ensure selected times align with slot size

✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/812

Timestamp: 2025-02-04T17:41:49.126Z
Learning: In DatePicker for availability selection, use fixed 30-minute timeIntervals for UI consistency but validate against slotTimeInMinutes in filterTime to ensure selected times align with actual slot boundaries. Remove minTime/maxTime props and handle all restrictions in filterTime.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-04T17:24:39.091Z
Learning: When filtering available times in DatePicker for availability selection, use slotTimeInMinutes as the buffer instead of a fixed duration to ensure all valid time slots are enabled, especially for larger slot sizes like 90 minutes.

@ThomasAFink
Copy link
Member Author

ThomasAFink commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix for the DatePicker component:

<DatePicker
    name={this.props.name + "_time"}
    locale="de"
    className="form-control form-input"
    ariaDescribedBy={"help_" + this.props.attributes.id + "_time"}
    id={this.props.attributes.id + "_time"}
    selected={this.state.selectedDate}
    onChange={date => { this.handleTimeChange(this.props.name, date) }}
    showTimeSelect
    showTimeSelectOnly
    dateFormat="HH:mm"
    timeFormat="HH:mm"
    timeIntervals={30}  // Always use 30-minute intervals for better UX
    timeCaption="Uhrzeit"
    excludeTimes={this.state.excludeTimeList}
    filterTime={(time) => {
        const hour = time.getHours();
        const minutes = time.getMinutes();
        
        // Block maintenance window (22:00-01:00)
        if (hour >= 22 || hour < 1 || (hour === 22 && minutes > 0)) {
            return false;
        }
        
        // Check buffer only for current day
        if (moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
            const currentTime = moment();
            const timeToCheck = moment(time);
            const minutesDiff = timeToCheck.diff(currentTime, 'minutes');
            if (minutesDiff < this.state.availability.slotTimeInMinutes) {
                return false;
            }
        }
        
        // Ensure time aligns with slot size
        const totalMinutes = hour * 60 + minutes;
        return totalMinutes % this.state.availability.slotTimeInMinutes === 0;
    }}
    disabled={this.props.attributes.disabled}
    onInputClick={this.openTimePicker}
    onKeyDown={this.tpKeyDownHandler}
    onClickOutside={this.closeTimePicker}
    strictParsing={true}
    open={this.state.timePickerIsOpen}
    ref={(timepicker) => { this.timepicker = timepicker }}
/>

Key changes:

  1. Fixed timeIntervals to 30 minutes for consistent UI
  2. Removed minTime/maxTime props
  3. Added maintenance window check in filterTime
  4. Added slot alignment validation
  5. Improved buffer check logic

This will:

  • Show 30-minute intervals for better UX
  • Enable all valid time slots
  • Block only maintenance window, past times, and conflicting slots
  • Ensure selected times align with slot size

✏️ Learnings added
🧠 Learnings used

@coderabbitai the 30 minute timeslots are there but I can't select 18:30 19:00 19:30 20:30 21:30 and 22:00. They'Re still disabled but shouldn't be. fix it

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Oops, something went wrong! Please try again later. 🐰 💔

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix for the DatePicker component:

<DatePicker
    name={this.props.name + "_time"}
    locale="de"
    className="form-control form-input"
    ariaDescribedBy={"help_" + this.props.attributes.id + "_time"}
    id={this.props.attributes.id + "_time"}
    selected={this.state.selectedDate}
    onChange={date => { this.handleTimeChange(this.props.name, date) }}
    showTimeSelect
    showTimeSelectOnly
    dateFormat="HH:mm"
    timeFormat="HH:mm"
    timeIntervals={30}  // Always use 30-minute intervals for better UX
    timeCaption="Uhrzeit"
    excludeTimes={this.state.excludeTimeList}
    filterTime={(time) => {
        const hour = time.getHours();
        const minutes = time.getMinutes();
        
        // Block maintenance window (22:00-01:00)
        if (hour >= 22 || hour < 1 || (hour === 22 && minutes > 0)) {
            return false;
        }
        
        // Check buffer only for current day
        if (moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
            const currentTime = moment();
            const timeToCheck = moment(time);
            const minutesDiff = timeToCheck.diff(currentTime, 'minutes');
            if (minutesDiff < this.state.availability.slotTimeInMinutes) {
                return false;
            }
        }
        
        return true;  // Remove slot alignment check to enable all valid slots
    }}
    disabled={this.props.attributes.disabled}
    onInputClick={this.openTimePicker}
    onKeyDown={this.tpKeyDownHandler}
    onClickOutside={this.closeTimePicker}
    strictParsing={true}
    open={this.state.timePickerIsOpen}
    ref={(timepicker) => { this.timepicker = timepicker }}
/>

Key changes:

  1. Removed the slot alignment check from filterTime
  2. Kept only the maintenance window check and current day buffer check
  3. Let excludeTimes handle conflicts with existing availabilities

This will enable all 30-minute slots except those in:

  • Maintenance window (22:00-01:00)
  • Past times on current day
  • Existing availabilities
✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/812

Timestamp: 2025-02-04T17:55:17.381Z
Learning: In DatePicker for availability selection, avoid using slot alignment validation in filterTime as it can incorrectly disable valid time slots. Instead, use filterTime only for maintenance window and buffer checks, and let excludeTimes handle conflicts with existing availabilities.

@ThomasAFink
Copy link
Member Author

ThomasAFink commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix for the DatePicker component:

<DatePicker
    name={this.props.name + "_time"}
    locale="de"
    className="form-control form-input"
    ariaDescribedBy={"help_" + this.props.attributes.id + "_time"}
    id={this.props.attributes.id + "_time"}
    selected={this.state.selectedDate}
    onChange={date => { this.handleTimeChange(this.props.name, date) }}
    showTimeSelect
    showTimeSelectOnly
    dateFormat="HH:mm"
    timeFormat="HH:mm"
    timeIntervals={30}  // Always use 30-minute intervals for better UX
    timeCaption="Uhrzeit"
    excludeTimes={this.state.excludeTimeList}
    filterTime={(time) => {
        const hour = time.getHours();
        const minutes = time.getMinutes();
        
        // Block maintenance window (22:00-01:00)
        if (hour >= 22 || hour < 1 || (hour === 22 && minutes > 0)) {
            return false;
        }
        
        // Check buffer only for current day
        if (moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
            const currentTime = moment();
            const timeToCheck = moment(time);
            const minutesDiff = timeToCheck.diff(currentTime, 'minutes');
            if (minutesDiff < this.state.availability.slotTimeInMinutes) {
                return false;
            }
        }
        
        return true;  // Remove slot alignment check to enable all valid slots
    }}
    disabled={this.props.attributes.disabled}
    onInputClick={this.openTimePicker}
    onKeyDown={this.tpKeyDownHandler}
    onClickOutside={this.closeTimePicker}
    strictParsing={true}
    open={this.state.timePickerIsOpen}
    ref={(timepicker) => { this.timepicker = timepicker }}
/>

Key changes:

  1. Removed the slot alignment check from filterTime
  2. Kept only the maintenance window check and current day buffer check
  3. Let excludeTimes handle conflicts with existing availabilities

This will enable all 30-minute slots except those in:

  • Maintenance window (22:00-01:00)
  • Past times on current day
  • Existing availabilities

✏️ Learnings added

@coderabbitai now only 20:30 21:00 and 21:30 are enabled 19:00 19:30 are still disabled. current time is 18:50

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Let's check how the excludeTimes list is being populated. Can you add a console.log to print out the excludeTimeList in setExcludeTimesForDay? Add this line just before setting the state:

console.log('Excluded times:', times.map(t => moment(t).format('HH:mm')));

This will help us understand why those specific time slots are being disabled. The issue is likely in how we're calculating the excluded times rather than in the filterTime function.

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Let's fix the setExcludeTimesForDay method. Here's the corrected version:

setExcludeTimesForDay() {
    if (this.state.kind === 'exclusion') {
        return;
    }
    var times = [];
    
    // Add maintenance window times (22:00-01:00)
    const selectedDate = moment(this.state.selectedDate);
    for (let minute = 1; minute < 60; minute++) {
        times.push(selectedDate.clone().hour(22).minute(minute).toDate());
        times.push(selectedDate.clone().hour(0).minute(minute).toDate());
    }
    times.push(selectedDate.clone().hour(23).minute(0).toDate());
    times.push(selectedDate.clone().hour(23).minute(30).toDate());

    // Filter and sort availabilities
    const availabilities = [...this.state.availabilityList]
        .filter(availability => 
            availability.id !== this.state.availability.id &&
            availability.type == this.state.availability.type &&
            this.isWeekDaySelected(this.state.selectedDate, availability)
        )
        .sort((a, b) => {
            const timeA = moment(a.startTime, 'HH:mm');
            const timeB = moment(b.startTime, 'HH:mm');
            return timeA.diff(timeB);
        });

    // Add times from existing availabilities
    availabilities.forEach(availability => {
        const startTime = moment(availability.startTime, 'HH:mm');
        const endTime = moment(availability.endTime, 'HH:mm');
        
        let currentTime = startTime.clone();
        while (currentTime.isBefore(endTime)) {
            times.push(selectedDate.clone()
                .hour(currentTime.hour())
                .minute(currentTime.minute())
                .toDate()
            );
            currentTime.add(30, 'minutes');
        }
    });

    console.log('Excluded times:', times.map(t => moment(t).format('HH:mm')));
    this.setState({excludeTimeList: times});
}

Key changes:

  1. Simplified maintenance window time generation
  2. Fixed time slot generation for existing availabilities
  3. Added console.log to debug excluded times
  4. Removed unnecessary buffer calculations
  5. Use 30-minute increments consistently

This should properly enable all valid time slots while only excluding:

  • Maintenance window (22:00-01:00)
  • Times that overlap with existing availabilities
✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/812

Timestamp: 2025-02-04T18:05:17.381Z
Learning: When generating excluded times for DatePicker in availability selection, use consistent 30-minute increments and only exclude maintenance window (22:00-01:00) and times that overlap with existing availabilities.

@ThomasAFink
Copy link
Member Author

ThomasAFink commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Let's fix the setExcludeTimesForDay method. Here's the corrected version:

setExcludeTimesForDay() {
    if (this.state.kind === 'exclusion') {
        return;
    }
    var times = [];
    
    // Add maintenance window times (22:00-01:00)
    const selectedDate = moment(this.state.selectedDate);
    for (let minute = 1; minute < 60; minute++) {
        times.push(selectedDate.clone().hour(22).minute(minute).toDate());
        times.push(selectedDate.clone().hour(0).minute(minute).toDate());
    }
    times.push(selectedDate.clone().hour(23).minute(0).toDate());
    times.push(selectedDate.clone().hour(23).minute(30).toDate());

    // Filter and sort availabilities
    const availabilities = [...this.state.availabilityList]
        .filter(availability => 
            availability.id !== this.state.availability.id &&
            availability.type == this.state.availability.type &&
            this.isWeekDaySelected(this.state.selectedDate, availability)
        )
        .sort((a, b) => {
            const timeA = moment(a.startTime, 'HH:mm');
            const timeB = moment(b.startTime, 'HH:mm');
            return timeA.diff(timeB);
        });

    // Add times from existing availabilities
    availabilities.forEach(availability => {
        const startTime = moment(availability.startTime, 'HH:mm');
        const endTime = moment(availability.endTime, 'HH:mm');
        
        let currentTime = startTime.clone();
        while (currentTime.isBefore(endTime)) {
            times.push(selectedDate.clone()
                .hour(currentTime.hour())
                .minute(currentTime.minute())
                .toDate()
            );
            currentTime.add(30, 'minutes');
        }
    });

    console.log('Excluded times:', times.map(t => moment(t).format('HH:mm')));
    this.setState({excludeTimeList: times});
}

Key changes:

  1. Simplified maintenance window time generation
  2. Fixed time slot generation for existing availabilities
  3. Added console.log to debug excluded times
  4. Removed unnecessary buffer calculations
  5. Use 30-minute increments consistently

This should properly enable all valid time slots while only excluding:

  • Maintenance window (22:00-01:00)
  • Times that overlap with existing availabilities

✏️ Learnings added

@coderabbitai didn'T work 19:00 19:30 and 20:00 are still disabled

/* eslint-disable react/prop-types */
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
import setHours from "date-fns/setHours"
import setMinutes from "date-fns/setMinutes";
import DatePicker, { registerLocale } from 'react-datepicker'
import { weekDayList, repeat } from '../helpers'
import * as Inputs from '../../../lib/inputs'
const { Label, Description } = Inputs
import de from 'date-fns/locale/de';
//import {formatTimestampDate} from "../helpers"
registerLocale('de', de)

class AvailabilityDatePicker extends Component {
    constructor(props) {
        super(props);
        this.state = {
            kind: this.props.attributes.kind,
            availability: this.props.attributes.availability,
            availabilityList: this.props.attributes.availabilitylist,
            minDate: moment.unix(this.props.attributes.availability.startDate).toDate(),
            minTime: setHours(setMinutes(new Date(), 1), 0),
            maxTime: setHours(setMinutes(new Date(), 59), 22),
            datePickerIsOpen: false,
            timPickerIsOpen: false
        }
        this.escHandler = this.escHandler.bind(this);
        //datepicker
        this.openDatePicker = this.openDatePicker.bind(this);
        this.closeDatePicker = this.closeDatePicker.bind(this);
        this.dpKeyDownHandler = this.dpKeyDownHandler.bind(this);
        this.handleCalendarIcon = this.handleCalendarIcon.bind(this);
        //timepicker
        this.openTimePicker = this.openTimePicker.bind(this);
        this.closeTimePicker = this.closeTimePicker.bind(this);
        this.tpKeyDownHandler = this.tpKeyDownHandler.bind(this);
        this.handleClockIcon = this.handleClockIcon.bind(this);



    }

    componentDidMount() {
        document.addEventListener("keydown", this.escHandler, false);
        this.datepicker.input.ariaLive = "polite";
        this.timepicker.input.ariaLive = "polite";

        this.updateState()
    }

    componentDidUpdate(prevProps) {
        if (this.props.attributes.availability !== prevProps.attributes.availability) {
            this.updateState()
        }
    }

    updateState(name, date) {
        let startTime = moment(this.props.attributes.availability.startTime, 'HH:mm');
        let startDate = moment.unix(this.props.attributes.availability.startDate)
            .set({ "h": startTime.hours(), "m": startTime.minutes() })
            .toDate()
        let endTime = moment(this.props.attributes.availability.endTime, 'HH:mm');
        let endDate = moment.unix(this.props.attributes.availability.endDate)
            .set({ "h": endTime.hours(), "m": endTime.minutes() })
            .toDate()

        let selectedDate = ("startDate" == this.props.name) ? startDate : endDate

        if (name && date) {
            startDate = ("startDate" == name) ? date.startOf('day') : startDate;
            endDate = ("endDate" == name) ? date.startOf('day') : endDate;
            selectedDate = date
        }
        this.setState({
            availability: this.props.attributes.availability,
            availabilityList: this.props.attributes.availabilitylist,
            selectedDate: selectedDate
        }, () => {
            this.setExcludeTimesForDay()
        })
    }

    setClassNameForSelectedWeekDay(className, date) {
        if (this.isWeekDaySelected(date) &&
            this.isDateInAvailabilityRange(date)
        ) {
            className = `${className} day__selected__weekday__${this.state.availability.type}`;
        }
        return className
    }

    isDateInAvailabilityRange(date) {
        return (
            date <= moment.unix(this.state.availability.endDate).toDate() &&
            date >= moment.unix(this.state.availability.startDate).toDate()
        )
    }

    isDateEqual(date1, date2) {
        return (
            date1.getDate() === date2.getDate() &&
            date1.getMonth() === date2.getMonth() &&
            date1.getFullYear() === date2.getFullYear()
        )
    }

    isWeekDaySelected(date, availability) {
        const selectedAvailability = availability ? availability : this.state.availability
        let isSelected = false;
        for (const [key, value] of Object.entries(selectedAvailability.weekday)) {
            weekDayList.map((weekday, index) => {
                if ((index + 1) == date.getDay() && weekday.value == key && value > 0) {
                    isSelected = true;
                }
            })
        }
        return isSelected;
    }

    setExcludeTimesForDay() {
        if (this.state.kind === 'exclusion') {
            return;
        }
        var times = [];

        // Add maintenance window times (22:00-01:00)
        const selectedDate = moment(this.state.selectedDate);
        for (let minute = 1; minute < 60; minute++) {
            times.push(selectedDate.clone().hour(22).minute(minute).toDate());
        }
        for (let minute = 0; minute < 59; minute++) {
            times.push(selectedDate.clone().hour(0).minute(minute).toDate());
        }

        // Filter and sort availabilities
        const availabilities = [...this.state.availabilityList]
            .filter(availability =>
                availability.id !== this.state.availability.id &&
                availability.type == this.state.availability.type &&
                this.isWeekDaySelected(this.state.selectedDate, availability)
            )
            .sort((a, b) => {
                const timeA = moment(a.startTime, 'HH:mm');
                const timeB = moment(b.startTime, 'HH:mm');
                return timeA.diff(timeB);
            });

        // Add regular excluded times
        availabilities.forEach(availability => {
            const startTime = moment(availability.startTime, 'hh:mm')
                .add(this.state.availability.slotTimeInMinutes, "m");
            const startOnDay = moment(this.state.selectedDate)
                .set({ "h": startTime.hours(), "m": startTime.minutes() })
                .toDate();

            const endTime = moment(availability.endTime, 'hh:mm')
                .subtract(this.state.availability.slotTimeInMinutes, "m");
            const endOnDay = moment(this.state.selectedDate)
                .set({ "h": endTime.hours(), "m": endTime.minutes() })
                .toDate();

            var currentTime = new Date(startOnDay);
            while (currentTime < endOnDay) {
                times.push(new Date(currentTime));
                currentTime = moment(currentTime)
                    .add(this.state.availability.slotTimeInMinutes, "m")
                    .toDate();
            }
            times.push(endOnDay);
        });

        // Add boundary timestamps between adjacent availabilities
        for (let i = 0; i < availabilities.length - 1; i++) {
            const current = availabilities[i];
            const next = availabilities[i + 1];

            const currentEnd = moment(current.endTime, 'HH:mm');
            const nextStart = moment(next.startTime, 'HH:mm');

            // If they're adjacent (end time of one equals start time of next)
            if (currentEnd.format('HH:mm') === nextStart.format('HH:mm')) {
                // Add the boundary timestamp to excluded times
                const boundaryTime = moment(this.state.selectedDate)
                    .set({ "h": currentEnd.hours(), "m": currentEnd.minutes() })
                    .toDate();
                times.push(boundaryTime);
            }
        }

        this.setState({ excludeTimeList: times });
    }

    handleChange(name, date) {
        if (!date) {
            this.closeDatePicker();
            return;
        }
        if ('startDate' == name) {
            if (this.state.availability.startDate != moment(date).startOf('day').unix()) {
                this.props.onChange("startDate", moment(date).unix());
            }
        }
        if ('endDate' == name) {
            if (this.state.availability.endDate != moment(date).startOf('day').unix()) {
                this.props.onChange("endDate", moment(date).unix());
            }
        }
        this.closeDatePicker();
    }

    handleTimeChange(name, date) {
        if ('startDate' == name) {
            if (this.state.availability.startTime != moment(date).format('HH:mm')) {
                this.props.onChange("startTime", moment(date).format('HH:mm'));
            }
        }
        if ('endDate' == name) {
            if (this.state.availability.endTime != moment(date).format('HH:mm')) {
                this.props.onChange("endTime", moment(date).format('HH:mm'));
            }
        }
        this.closeTimePicker();
    }

    escHandler(event) {
        if (event.key === "Escape") {
            this.closeDatePicker();
            this.closeTimePicker();
        }
    }

    dpKeyDownHandler(event) {
        if (event.key === 'Enter') {
            event.preventDefault()
            this.openDatePicker()
        }
    }

    tpKeyDownHandler(event) {
        if (event.key === 'Enter') {
            event.preventDefault()
            this.openTimePicker()
        }
    }

    handleCalendarIcon(event) {
        event.preventDefault();
        this.openDatePicker()
        this.datepicker.input.focus();
    }

    handleClockIcon(event) {
        event.preventDefault();
        this.openTimePicker()
        this.timepicker.input.focus();
    }

    openDatePicker() {
        if (!this.props.attributes.disabled) {
            this.setState({
                datePickerIsOpen: true,
            });
        }
    }

    openTimePicker() {
        if (!this.props.attributes.disabled) {
            this.setState({
                timePickerIsOpen: true,
            });
        }

    }

    closeDatePicker() {
        this.setState({
            datePickerIsOpen: false,
        });
    }

    closeTimePicker() {
        this.setState({
            timePickerIsOpen: false,
        });
    }

    render() {
        const dayClassName = (date) => {
            let className = "";
            className = this.setClassNameForSelectedWeekDay(className, date);
            return className;
        }

        const filterPassedTime = (time) => {
            // For future dates, only check maintenance window
            if (!moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
                const hour = time.getHours();
                const minutes = time.getMinutes();
                // Block maintenance window (22:00-01:00)
                return !(hour >= 22 || hour < 1 || (hour === 22 && minutes > 0));
            }

            // For today, check both maintenance window and buffer
            const currentTime = moment();
            const timeToCheck = moment(time);
            const minutesDiff = timeToCheck.diff(currentTime, 'minutes');
            const bufferMinutes = this.state.availability.slotTimeInMinutes;

            return minutesDiff >= bufferMinutes;
        };

        /*
        const isWeekday = date => {
            const day = date.getDay();
            return day !== 0 && day !== 6;
        };
        */

        return (
            <div className="grid">
                <div className="grid__item one-half">
                    <div className="form-group">
                        <Label
                            attributes={{ "htmlFor": this.props.attributes.id, "className": "light" }}
                            value={"startDate" == this.props.name ? "Datum von" : "Datum bis"}>
                        </Label>
                        <div className="controls add-date-picker">
                            <DatePicker
                                todayButton="Heute"
                                locale="de"
                                className="form-control form-input"
                                id={this.props.attributes.id}
                                ariaDescribedBy={"help_" + this.props.attributes.id}
                                name={this.props.name}
                                dateFormat="dd.MM.yyyy"
                                selected={this.state.selectedDate}
                                onChange={date => { this.handleChange(this.props.name, date) }}
                                minDate={this.state.minDate}
                                //maxDate={repeat(this.state.availability.repeat) == 0 ? this.state.selectedDate : null}
                                //filterDate={isWeekday}
                                //excludeDates={this.state.excludeDateList}                   
                                dayClassName={dayClassName}
                                disabled={this.props.attributes.disabled}
                                onInputClick={this.openDatePicker}
                                onKeyDown={this.dpKeyDownHandler}
                                onClickOutside={this.closeDatePicker}
                                strictParsing={true}
                                open={this.state.datePickerIsOpen}
                                ref={(datepicker) => { this.datepicker = datepicker }}
                                chooseDayAriaLabelPrefix="Datumsauswahl"
                                disabledDayAriaLabelPrefix="Nicht auswählbar"
                                previousMonthAriaLabel="vorheriger Monat"
                                nextMonthAriaLabel="nächster Monat"
                                monthAriaLabelPrefix="Monat"
                            />
                            <a aria-describedby={"help_" + this.props.attributes.id} href="#" aria-label={"startDate" == this.props.name ? "Kalender Datum von öffnen" : "Kalender Datum bis öffnen"} className="calendar-placement icon" title={"startDate" == this.props.name ? "Kalender Datum von öffnen" : "Kalender Datum bis öffnen"} onClick={this.handleCalendarIcon} onKeyDown={this.dpKeyDownHandler}>
                                <i className="far fa-calendar-alt" aria-hidden="true" />
                            </a>
                        </div>
                        <Description attributes={{ "id": "help_" + this.props.attributes.id }} value="Eingabe des Datums im Format TT.MM.YYYY"></Description>
                    </div>
                </div>
                <div className="grid__item one-half">
                    <div className="form-group">
                        <Label
                            attributes={{ "htmlFor": this.props.attributes.id + "_time", "className": "light" }}
                            value={"startDate" == this.props.name ? "Uhrzeit von" : "Uhrzeit bis"}>
                        </Label>
                        <div className="controls add-date-picker">
                        <DatePicker
    name={this.props.name + "_time"}
    locale="de"
    className="form-control form-input"
    ariaDescribedBy={"help_" + this.props.attributes.id + "_time"}
    id={this.props.attributes.id + "_time"}
    selected={this.state.selectedDate}
    onChange={date => { this.handleTimeChange(this.props.name, date) }}
    showTimeSelect
    showTimeSelectOnly
    dateFormat="HH:mm"
    timeFormat="HH:mm"
    timeIntervals={30}  // Always use 30-minute intervals for better UX
    timeCaption="Uhrzeit"
    excludeTimes={this.state.excludeTimeList}
    filterTime={(time) => {
        const hour = time.getHours();
        const minutes = time.getMinutes();
        
        // Block maintenance window (22:00-01:00)
        if (hour >= 22 || hour < 1 || (hour === 22 && minutes > 0)) {
            return false;
        }
        
        // Check buffer only for current day
        if (moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
            const currentTime = moment();
            const timeToCheck = moment(time);
            const minutesDiff = timeToCheck.diff(currentTime, 'minutes');
            if (minutesDiff < this.state.availability.slotTimeInMinutes) {
                return false;
            }
        }
        
        return true;  // Remove slot alignment check to enable all valid slots
    }}
    disabled={this.props.attributes.disabled}
    onInputClick={this.openTimePicker}
    onKeyDown={this.tpKeyDownHandler}
    onClickOutside={this.closeTimePicker}
    strictParsing={true}
    open={this.state.timePickerIsOpen}
    ref={(timepicker) => { this.timepicker = timepicker }}
/>
                            <a href="#" aria-describedby={"help_" + this.props.attributes.id + "_time"} aria-label="Uhrzeitauswahl öffnen" className="calendar-placement icon" title={"startDate" == this.props.name ? "Uhrzeit von wählen" : "Uhrzeit bis wählen"} onClick={this.handleClockIcon} onKeyDown={this.tpKeyDownHandler}>
                                <i className="far fa-clock" aria-hidden="true" />
                            </a>
                        </div>
                        <Description attributes={{ "id": "help_" + this.props.attributes.id + "_time" }} value="Eingabe der Uhrzeit im Format HH:MM"></Description>
                    </div>
                </div>
            </div>
        )
    }
}

AvailabilityDatePicker.propTypes = {
    onChange: PropTypes.func,
    attributes: PropTypes.object
}

export default AvailabilityDatePicker

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Oops, something went wrong! Please try again later. 🐰 💔

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix for setExcludeTimesForDay:

setExcludeTimesForDay() {
    if (this.state.kind === 'exclusion') {
        return;
    }
    var times = [];
    
    // Add maintenance window times (22:00-01:00)
    const selectedDate = moment(this.state.selectedDate);
    for (let minute = 1; minute < 60; minute++) {
        times.push(selectedDate.clone().hour(22).minute(minute).toDate());
        times.push(selectedDate.clone().hour(0).minute(minute).toDate());
    }
    times.push(selectedDate.clone().hour(23).minute(0).toDate());
    times.push(selectedDate.clone().hour(23).minute(30).toDate());

    // Filter and sort availabilities
    const availabilities = [...this.state.availabilityList]
        .filter(availability => 
            availability.id !== this.state.availability.id &&
            availability.type == this.state.availability.type &&
            this.isWeekDaySelected(this.state.selectedDate, availability)
        )
        .sort((a, b) => {
            const timeA = moment(a.startTime, 'HH:mm');
            const timeB = moment(b.startTime, 'HH:mm');
            return timeA.diff(timeB);
        });

    // Add times from existing availabilities
    availabilities.forEach(availability => {
        const startTime = moment(availability.startTime, 'HH:mm');
        const endTime = moment(availability.endTime, 'HH:mm');
        
        // Only exclude times that are actually within the availability
        let currentTime = startTime.clone();
        while (currentTime.isBefore(endTime) || currentTime.isSame(endTime)) {
            times.push(selectedDate.clone()
                .hour(currentTime.hour())
                .minute(currentTime.minute())
                .toDate()
            );
            currentTime.add(30, 'minutes');
        }
    });

    console.log('Excluded times:', times.map(t => moment(t).format('HH:mm')).sort());
    this.setState({excludeTimeList: times});
}

Key changes:

  1. Removed incorrect buffer calculations with slotTimeInMinutes
  2. Use exact start and end times from availabilities
  3. Use consistent 30-minute increments
  4. Added debug logging to help verify excluded times
  5. Fixed maintenance window time generation

This should now correctly enable all valid time slots while only excluding:

  • Maintenance window (22:00-01:00)
  • Times that are actually occupied by existing availabilities

✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/812

Timestamp: 2025-02-04T17:54:09.877Z
Learning: When generating excluded times for DatePicker in availability selection, use exact start and end times from existing availabilities and consistent 30-minute increments. Avoid using slotTimeInMinutes for buffer calculations as it incorrectly excludes valid time slots.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@ThomasAFink
Copy link
Member Author

ThomasAFink commented Feb 4, 2025

setExcludeTimesForDay

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix for setExcludeTimesForDay:

setExcludeTimesForDay() {
    if (this.state.kind === 'exclusion') {
        return;
    }
    var times = [];
    
    // Add maintenance window times (22:00-01:00)
    const selectedDate = moment(this.state.selectedDate);
    for (let minute = 1; minute < 60; minute++) {
        times.push(selectedDate.clone().hour(22).minute(minute).toDate());
        times.push(selectedDate.clone().hour(0).minute(minute).toDate());
    }
    times.push(selectedDate.clone().hour(23).minute(0).toDate());
    times.push(selectedDate.clone().hour(23).minute(30).toDate());

    // Filter and sort availabilities
    const availabilities = [...this.state.availabilityList]
        .filter(availability => 
            availability.id !== this.state.availability.id &&
            availability.type == this.state.availability.type &&
            this.isWeekDaySelected(this.state.selectedDate, availability)
        )
        .sort((a, b) => {
            const timeA = moment(a.startTime, 'HH:mm');
            const timeB = moment(b.startTime, 'HH:mm');
            return timeA.diff(timeB);
        });

    // Add times from existing availabilities
    availabilities.forEach(availability => {
        const startTime = moment(availability.startTime, 'HH:mm');
        const endTime = moment(availability.endTime, 'HH:mm');
        
        // Only exclude times that are actually within the availability
        let currentTime = startTime.clone();
        while (currentTime.isBefore(endTime) || currentTime.isSame(endTime)) {
            times.push(selectedDate.clone()
                .hour(currentTime.hour())
                .minute(currentTime.minute())
                .toDate()
            );
            currentTime.add(30, 'minutes');
        }
    });

    console.log('Excluded times:', times.map(t => moment(t).format('HH:mm')).sort());
    this.setState({excludeTimeList: times});
}

Key changes:

  1. Removed incorrect buffer calculations with slotTimeInMinutes
  2. Use exact start and end times from availabilities
  3. Use consistent 30-minute increments
  4. Added debug logging to help verify excluded times
  5. Fixed maintenance window time generation

This should now correctly enable all valid time slots while only excluding:

  • Maintenance window (22:00-01:00)
  • Times that are actually occupied by existing availabilities

✏️ Learnings added

@coderabbitai setExcludeTimesForDay is not the problem here. Removing it doesn't change anything for example.

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink You're right - I found the issue. The problem is with the minTime/maxTime props that are still being passed to the DatePicker. Remove these props as they're redundant and causing additional time slots to be disabled:

<DatePicker
    name={this.props.name + "_time"}
    locale="de"
    className="form-control form-input"
    ariaDescribedBy={"help_" + this.props.attributes.id + "_time"}
    id={this.props.attributes.id + "_time"}
    selected={this.state.selectedDate}
    onChange={date => { this.handleTimeChange(this.props.name, date) }}
    showTimeSelect
    showTimeSelectOnly
    dateFormat="HH:mm"
    timeFormat="HH:mm"
    timeIntervals={30}
    timeCaption="Uhrzeit"
    // Remove these props
    // minTime={this.state.minTime}
    // maxTime={this.state.maxTime}
    excludeTimes={this.state.excludeTimeList}
    filterTime={(time) => {
        const hour = time.getHours();
        const minutes = time.getMinutes();
        
        // Block maintenance window (22:00-01:00)
        if (hour >= 22 || hour < 1 || (hour === 22 && minutes > 0)) {
            return false;
        }
        
        // Check buffer only for current day
        if (moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
            const currentTime = moment();
            const timeToCheck = moment(time);
            const minutesDiff = timeToCheck.diff(currentTime, 'minutes');
            if (minutesDiff < this.state.availability.slotTimeInMinutes) {
                return false;
            }
        }
        
        return true;
    }}
    disabled={this.props.attributes.disabled}
    onInputClick={this.openTimePicker}
    onKeyDown={this.tpKeyDownHandler}
    onClickOutside={this.closeTimePicker}
    strictParsing={true}
    open={this.state.timePickerIsOpen}
    ref={(timepicker) => { this.timepicker = timepicker }}
/>

This should now correctly enable all time slots except:

  • Past times on current day
  • Maintenance window (22:00-01:00)
  • Times that conflict with existing availabilities
✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/812

Timestamp: 2025-02-04T18:05:17.381Z
Learning: In DatePicker for availability selection, avoid using minTime/maxTime props as they can incorrectly disable valid time slots. Instead, use filterTime for maintenance window and buffer checks, and excludeTimes for conflicts with existing availabilities.

🧠 Learnings used
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-04T17:54:10.003Z
Learning: When generating excluded times for DatePicker in availability selection, use exact start and end times from existing availabilities and consistent 30-minute increments. Avoid using slotTimeInMinutes for buffer calculations as it incorrectly excludes valid time slots.

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink I found the issue! The problem is with the minTime/maxTime props that are still being passed to the DatePicker component. Remove these lines from the constructor:

minTime: setHours(setMinutes(new Date(), 1), 0),
maxTime: setHours(setMinutes(new Date(), 59), 22),

And remove these props from the DatePicker component:

minTime={this.state.minTime}
maxTime={this.state.maxTime}

These props are overriding both excludeTimes and filterTime, causing time slots to be incorrectly disabled. The maintenance window and buffer checks in filterTime will handle all the necessary time restrictions.

✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/812

Timestamp: 2025-02-04T18:05:17.381Z
Learning: In DatePicker for availability selection, avoid using minTime/maxTime props as they override excludeTimes and filterTime. Instead, use filterTime for maintenance window and buffer checks, and excludeTimes for overlapping availabilities.

@ThomasAFink
Copy link
Member Author

@coderabbitai I think the problem are the buffer minutes

        const bufferMinutes = this.state.availability.slotTimeInMinutes;

        return minutesDiff >= bufferMinutes;
        
        git rid of them
/* eslint-disable react/prop-types */
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
import setHours from "date-fns/setHours"
import setMinutes from "date-fns/setMinutes";
import DatePicker, { registerLocale } from 'react-datepicker'
import {weekDayList, repeat} from '../helpers'
import * as Inputs from '../../../lib/inputs'
const { Label, Description } = Inputs
import de from 'date-fns/locale/de';
//import {formatTimestampDate} from "../helpers"
registerLocale('de', de)

class AvailabilityDatePicker extends Component 
{
    constructor(props) {
        super(props);
        this.state = {
            kind: this.props.attributes.kind,            
            availability: this.props.attributes.availability,
            availabilityList: this.props.attributes.availabilitylist,
            minDate: moment.unix(this.props.attributes.availability.startDate).toDate(),
            minTime: setHours(setMinutes(new Date(), 1), 0),
            maxTime: setHours(setMinutes(new Date(), 59), 22),
            datePickerIsOpen: false,
            timPickerIsOpen: false
        }
        this.escHandler = this.escHandler.bind(this);
        //datepicker
        this.openDatePicker = this.openDatePicker.bind(this);
        this.closeDatePicker = this.closeDatePicker.bind(this);
        this.dpKeyDownHandler = this.dpKeyDownHandler.bind(this);
        this.handleCalendarIcon = this.handleCalendarIcon.bind(this);
        //timepicker
        this.openTimePicker = this.openTimePicker.bind(this);
        this.closeTimePicker = this.closeTimePicker.bind(this);
        this.tpKeyDownHandler = this.tpKeyDownHandler.bind(this);
        this.handleClockIcon = this.handleClockIcon.bind(this);



    }

    componentDidMount() {
        document.addEventListener("keydown", this.escHandler, false);
        this.datepicker.input.ariaLive = "polite";
        this.timepicker.input.ariaLive = "polite";

        this.updateState()
    }

    componentDidUpdate(prevProps) {
        if (this.props.attributes.availability !== prevProps.attributes.availability) {
            this.updateState()
        }
    }

    updateState(name, date) {
        let startTime = moment(this.props.attributes.availability.startTime, 'HH:mm');
        let startDate = moment.unix(this.props.attributes.availability.startDate)
            .set({"h": startTime.hours(), "m": startTime.minutes()})
            .toDate()
        let endTime = moment(this.props.attributes.availability.endTime, 'HH:mm');
        let endDate = moment.unix(this.props.attributes.availability.endDate)
            .set({"h": endTime.hours(), "m": endTime.minutes()})
            .toDate()

        let selectedDate = ("startDate" == this.props.name) ? startDate : endDate

        if (name && date) {
            startDate = ("startDate" == name) ? date.startOf('day') : startDate;
            endDate = ("endDate" == name) ? date.startOf('day') : endDate;
            selectedDate = date
        }
        this.setState({
            availability: this.props.attributes.availability,
            availabilityList: this.props.attributes.availabilitylist,
            selectedDate: selectedDate
        }, () => {
            this.setExcludeTimesForDay()
        })
    }

    setClassNameForSelectedWeekDay(className, date) {
        if (this.isWeekDaySelected(date) &&
            this.isDateInAvailabilityRange(date)
        ) {
            className = `${className} day__selected__weekday__${this.state.availability.type}`; 
        }
        return className
    }

    isDateInAvailabilityRange(date) {
        return (
            date <= moment.unix(this.state.availability.endDate).toDate() && 
            date >= moment.unix(this.state.availability.startDate).toDate()
        )
    }

    isDateEqual(date1, date2) {
        return (
            date1.getDate() === date2.getDate() &&
            date1.getMonth() === date2.getMonth() &&
            date1.getFullYear() === date2.getFullYear()
        )
    }

    isWeekDaySelected(date, availability)
    {
        const selectedAvailability = availability ? availability : this.state.availability
        let isSelected = false;
        for (const [key, value] of Object.entries(selectedAvailability.weekday)) {
            weekDayList.map((weekday, index) => {
                if ((index+1) == date.getDay() && weekday.value == key && value > 0) {
                    isSelected = true; 
                }   
            }) 
        } 
        return isSelected;
    }

    setExcludeTimesForDay() {
        if (this.state.kind === 'exclusion') {
            return;
        }
        var times = [];
        
        // Add maintenance window times (22:00-01:00)
        const selectedDate = moment(this.state.selectedDate);
        for (let minute = 1; minute < 60; minute++) {
            times.push(selectedDate.clone().hour(22).minute(minute).toDate());
        }
        for (let minute = 0; minute < 59; minute++) {
            times.push(selectedDate.clone().hour(0).minute(minute).toDate());
        }
    
        // Filter and sort availabilities
        const availabilities = [...this.state.availabilityList]
            .filter(availability => 
                availability.id !== this.state.availability.id &&
                availability.type == this.state.availability.type &&
                this.isWeekDaySelected(this.state.selectedDate, availability)
            )
            .sort((a, b) => {
                const timeA = moment(a.startTime, 'HH:mm');
                const timeB = moment(b.startTime, 'HH:mm');
                return timeA.diff(timeB);
            });
    
        // Add regular excluded times
        availabilities.forEach(availability => {
            const startTime = moment(availability.startTime, 'hh:mm')
                .add(this.state.availability.slotTimeInMinutes, "m");
            const startOnDay = moment(this.state.selectedDate)
                .set({"h": startTime.hours(), "m": startTime.minutes()})
                .toDate();
            
            const endTime = moment(availability.endTime, 'hh:mm')
                .subtract(this.state.availability.slotTimeInMinutes, "m");
            const endOnDay = moment(this.state.selectedDate)
                .set({"h": endTime.hours(), "m": endTime.minutes()})
                .toDate();
            
            var currentTime = new Date(startOnDay);
            while (currentTime < endOnDay) {
                times.push(new Date(currentTime));
                currentTime = moment(currentTime)
                    .add(this.state.availability.slotTimeInMinutes, "m")
                    .toDate();
            }
            times.push(endOnDay);
        });
    
        // Add boundary timestamps between adjacent availabilities
        for (let i = 0; i < availabilities.length - 1; i++) {
            const current = availabilities[i];
            const next = availabilities[i + 1];
            
            const currentEnd = moment(current.endTime, 'HH:mm');
            const nextStart = moment(next.startTime, 'HH:mm');
            
            // If they're adjacent (end time of one equals start time of next)
            if (currentEnd.format('HH:mm') === nextStart.format('HH:mm')) {
                // Add the boundary timestamp to excluded times
                const boundaryTime = moment(this.state.selectedDate)
                    .set({"h": currentEnd.hours(), "m": currentEnd.minutes()})
                    .toDate();
                times.push(boundaryTime);
            }
        }
    
        this.setState({excludeTimeList: times});
    }

    handleChange(name, date) {
        if (!date) {
            this.closeDatePicker();
            return;
        }
        if ('startDate' == name) {
            if (this.state.availability.startDate != moment(date).startOf('day').unix()) {
                this.props.onChange("startDate", moment(date).unix());
            }
        }
        if ('endDate' == name) {
            if (this.state.availability.endDate != moment(date).startOf('day').unix()) {
                this.props.onChange("endDate", moment(date).unix());
            }
        }
        this.closeDatePicker();
    }

    handleTimeChange(name, date) {
        if ('startDate' == name) {
            if (this.state.availability.startTime != moment(date).format('HH:mm')) {
                this.props.onChange("startTime", moment(date).format('HH:mm'));
            }
        }
        if ('endDate' == name) {
            if (this.state.availability.endTime != moment(date).format('HH:mm')) {
                this.props.onChange("endTime", moment(date).format('HH:mm'));
            }
        }
        this.closeTimePicker();
    }

    escHandler(event) {
        if (event.key === "Escape") {
            this.closeDatePicker();
            this.closeTimePicker();
        }
    }

    dpKeyDownHandler(event) {
        if (event.key === 'Enter') {
            event.preventDefault()
            this.openDatePicker() 
        }
    }

    tpKeyDownHandler(event) {
        if (event.key === 'Enter') {
            event.preventDefault()
            this.openTimePicker() 
        }
    }

    handleCalendarIcon(event) {
        event.preventDefault();
        this.openDatePicker()
        this.datepicker.input.focus();
    }

    handleClockIcon(event) {
        event.preventDefault();
        this.openTimePicker()
        this.timepicker.input.focus();
    }

    openDatePicker() {
        if (!this.props.attributes.disabled) {
            this.setState({
                datePickerIsOpen: true,
            });
        }
    }

    openTimePicker() {
        if (!this.props.attributes.disabled) {
            this.setState({
                timePickerIsOpen: true,
            });
        }
        
    }

    closeDatePicker() {
        this.setState({
            datePickerIsOpen: false,
        });
    }

    closeTimePicker() {
        this.setState({
            timePickerIsOpen: false,
        });
    }

    render() {
        const dayClassName = (date) => {
            let className = "";
            className = this.setClassNameForSelectedWeekDay(className, date);
            return className;
        }

        const filterPassedTime = (time) => {
            // For future dates, only check maintenance window
            if (!moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
                const hour = time.getHours();
                const minutes = time.getMinutes();
                // Block maintenance window (22:00-01:00)
                return !(hour >= 22 || hour < 1 || (hour === 22 && minutes > 0));
            }
        
            // For today, check both maintenance window and buffer
            const currentTime = moment();
            const timeToCheck = moment(time);
            const minutesDiff = timeToCheck.diff(currentTime, 'minutes');
            const bufferMinutes = this.state.availability.slotTimeInMinutes;
            
            return minutesDiff >= bufferMinutes;
        };
        
        /*
        const isWeekday = date => {
            const day = date.getDay();
            return day !== 0 && day !== 6;
        };
        */

        return (
            <div className="grid">
                <div className="grid__item one-half">
                    <div className="form-group">
                        <Label 
                            attributes={{"htmlFor": this.props.attributes.id, "className": "light"}} 
                            value={"startDate" == this.props.name ? "Datum von" : "Datum bis" }>
                        </Label>    
                        <div className="controls add-date-picker">
                            <DatePicker 
                                todayButton="Heute"
                                locale="de" 
                                className="form-control form-input" 
                                id={this.props.attributes.id}
                                ariaDescribedBy={"help_" + this.props.attributes.id}
                                name={this.props.name}
                                dateFormat="dd.MM.yyyy" 
                                selected={this.state.selectedDate}
                                onChange={date => {this.handleChange(this.props.name, date)}}
                                minDate={this.state.minDate}
                                //maxDate={repeat(this.state.availability.repeat) == 0 ? this.state.selectedDate : null}
                                //filterDate={isWeekday}
                                //excludeDates={this.state.excludeDateList}                   
                                dayClassName={dayClassName}
                                disabled={this.props.attributes.disabled}
                                onInputClick={this.openDatePicker}
                                onKeyDown={this.dpKeyDownHandler}
                                onClickOutside={this.closeDatePicker}
                                strictParsing={true}
                                open={this.state.datePickerIsOpen}
                                ref={(datepicker) => { this.datepicker = datepicker }} 
                                chooseDayAriaLabelPrefix="Datumsauswahl"
                                disabledDayAriaLabelPrefix="Nicht auswählbar"
                                previousMonthAriaLabel="vorheriger Monat"
                                nextMonthAriaLabel="nächster Monat"
                                monthAriaLabelPrefix="Monat"
                            />
                            <a aria-describedby={"help_" + this.props.attributes.id} href="#" aria-label={"startDate" == this.props.name ? "Kalender Datum von öffnen" : "Kalender Datum bis öffnen"} className="calendar-placement icon" title={"startDate" == this.props.name ? "Kalender Datum von öffnen" : "Kalender Datum bis öffnen"} onClick={this.handleCalendarIcon} onKeyDown={this.dpKeyDownHandler}>
                                <i className="far fa-calendar-alt" aria-hidden="true" />
                            </a>
                        </div>
                        <Description attributes={{ "id": "help_" + this.props.attributes.id }} value="Eingabe des Datums im Format TT.MM.YYYY"></Description>
                    </div>
                </div>
                <div className="grid__item one-half">
                    <div className="form-group">
                        <Label 
                            attributes={{"htmlFor": this.props.attributes.id + "_time", "className": "light"}} 
                            value={"startDate" == this.props.name ? "Uhrzeit von" : "Uhrzeit bis" }>
                        </Label> 
                        <div className="controls add-date-picker">
                        <DatePicker
    name={this.props.name + "_time"}
    locale="de"
    className="form-control form-input" 
    ariaDescribedBy={"help_" + this.props.attributes.id + "_time"}
    id={this.props.attributes.id + "_time"}
    selected={this.state.selectedDate}
    onChange={date => {this.handleTimeChange(this.props.name, date)}}
    showTimeSelect
    showTimeSelectOnly
    dateFormat="HH:mm"
    timeFormat="HH:mm"
    timeIntervals={this.state.availability.slotTimeInMinutes > 30 ? 
        this.state.availability.slotTimeInMinutes : 30} // Use 30 min intervals for better UX
    timeCaption="Uhrzeit"
    minTime={this.state.minTime}
    maxTime={this.state.maxTime}
    excludeTimes={this.state.excludeTimeList}
    filterTime={(time) => {
        // First check buffer for current day
        if (!filterPassedTime(time)) {
            return false;
        }
        
        // Get minutes since midnight
        const minutes = time.getHours() * 60 + time.getMinutes();
        const slotTime = this.state.availability.slotTimeInMinutes;
        
        // Check if time is a valid slot boundary
        // Allow times that could be either start or end of a slot
        return minutes % 30 === 0 && minutes <= (24 * 60 - slotTime);
    }}
    disabled={this.props.attributes.disabled}
    onInputClick={this.openTimePicker}
    onKeyDown={this.tpKeyDownHandler}
    onClickOutside={this.closeTimePicker}
    strictParsing={true}
    open={this.state.timePickerIsOpen}
    ref={(timepicker) => { this.timepicker = timepicker }}
/>
                            <a href="#" aria-describedby={"help_" + this.props.attributes.id + "_time"} aria-label="Uhrzeitauswahl öffnen" className="calendar-placement icon" title={"startDate" == this.props.name ? "Uhrzeit von wählen" : "Uhrzeit bis wählen"} onClick={this.handleClockIcon} onKeyDown={this.tpKeyDownHandler}>
                                <i className="far fa-clock" aria-hidden="true" />
                            </a>
                        </div>
                        <Description attributes={{ "id": "help_" + this.props.attributes.id + "_time" }} value="Eingabe der Uhrzeit im Format HH:MM"></Description>
                    </div>
                </div>
            </div>
        )
    }
}

AvailabilityDatePicker.propTypes = {
    onChange: PropTypes.func,
    attributes: PropTypes.object
}

export default AvailabilityDatePicker

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix. Replace the filterPassedTime function with:

const filterPassedTime = (time) => {
    // For future dates, only check maintenance window
    if (!moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
        const hour = time.getHours();
        const minutes = time.getMinutes();
        // Block maintenance window (22:00-01:00)
        return !(hour >= 22 || hour < 1 || (hour === 22 && minutes > 0));
    }

    // For today, check both maintenance window and use fixed 30-minute buffer
    const currentTime = moment();
    const timeToCheck = moment(time);
    const minutesDiff = timeToCheck.diff(currentTime, 'minutes');
    
    return minutesDiff >= 30;  // Fixed 30-minute buffer
};

This will:

  1. Use a fixed 30-minute buffer instead of slotTimeInMinutes
  2. Enable all valid time slots after the buffer
  3. Keep maintenance window check (22:00-01:00)
  4. Keep excludeTimes for overlapping availabilities

✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/812

Timestamp: 2025-02-04T17:58:30.158Z
Learning: When filtering time slots in DatePicker for availability selection, use a fixed 30-minute buffer for the current day instead of using slotTimeInMinutes as buffer, to avoid incorrectly disabling valid time slots, especially with larger slot sizes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@ThomasAFink
Copy link
Member Author

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix. Replace the filterPassedTime function with:

const filterPassedTime = (time) => {
    // For future dates, only check maintenance window
    if (!moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
        const hour = time.getHours();
        const minutes = time.getMinutes();
        // Block maintenance window (22:00-01:00)
        return !(hour >= 22 || hour < 1 || (hour === 22 && minutes > 0));
    }

    // For today, check both maintenance window and use fixed 30-minute buffer
    const currentTime = moment();
    const timeToCheck = moment(time);
    const minutesDiff = timeToCheck.diff(currentTime, 'minutes');
    
    return minutesDiff >= 30;  // Fixed 30-minute buffer
};

This will:

  1. Use a fixed 30-minute buffer instead of slotTimeInMinutes
  2. Enable all valid time slots after the buffer
  3. Keep maintenance window check (22:00-01:00)
  4. Keep excludeTimes for overlapping availabilities

✏️ Learnings added

@coderabbitai no buffer

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix. Replace the filterPassedTime function with:

const filterPassedTime = (time) => {
    const hour = time.getHours();
    const minutes = time.getMinutes();
    // Block maintenance window (22:00-01:00)
    return !(hour >= 22 || hour < 1 || (hour === 22 && minutes > 0));
};

This will:

  1. Remove all buffer checks
  2. Only block maintenance window (22:00-01:00)
  3. Let excludeTimes handle overlapping availabilities

(`・ω・)ゞ Thoughtfully removed buffer to enable all valid time slots.


✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/812

Timestamp: 2025-02-04T17:59:49.925Z
Learning: When filtering time slots in DatePicker for availability selection, only block maintenance window (22:00-01:00) and let excludeTimes handle overlapping availabilities. Do not add any buffer time to avoid incorrectly disabling valid time slots.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-04T17:58:30.319Z
Learning: When filtering time slots in DatePicker for availability selection, use a fixed 30-minute buffer for the current day instead of using slotTimeInMinutes as buffer, to avoid incorrectly disabling valid time slots, especially with larger slot sizes.

@it-at-m it-at-m deleted a comment from coderabbitai bot Feb 4, 2025
@it-at-m it-at-m deleted a comment from coderabbitai bot Feb 4, 2025
@it-at-m it-at-m deleted a comment from coderabbitai bot Feb 4, 2025
@ThomasAFink
Copy link
Member Author

now for over 60 minutes, the 30 minute time slots are not showing in the selection @coderabbitai add them. for example 90 minutes I'm still getting 90 interval selections

/* eslint-disable react/prop-types */
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
import setHours from "date-fns/setHours"
import setMinutes from "date-fns/setMinutes";
import DatePicker, { registerLocale } from 'react-datepicker'
import { weekDayList, repeat } from '../helpers'
import * as Inputs from '../../../lib/inputs'
const { Label, Description } = Inputs
import de from 'date-fns/locale/de';
//import {formatTimestampDate} from "../helpers"
registerLocale('de', de)

class AvailabilityDatePicker extends Component {
    constructor(props) {
        super(props);
        this.state = {
            kind: this.props.attributes.kind,
            availability: this.props.attributes.availability,
            availabilityList: this.props.attributes.availabilitylist,
            minDate: moment.unix(this.props.attributes.availability.startDate).toDate(),
            minTime: setHours(setMinutes(new Date(), 1), 0),
            maxTime: setHours(setMinutes(new Date(), 59), 22),
            datePickerIsOpen: false,
            timPickerIsOpen: false
        }
        this.escHandler = this.escHandler.bind(this);
        //datepicker
        this.openDatePicker = this.openDatePicker.bind(this);
        this.closeDatePicker = this.closeDatePicker.bind(this);
        this.dpKeyDownHandler = this.dpKeyDownHandler.bind(this);
        this.handleCalendarIcon = this.handleCalendarIcon.bind(this);
        //timepicker
        this.openTimePicker = this.openTimePicker.bind(this);
        this.closeTimePicker = this.closeTimePicker.bind(this);
        this.tpKeyDownHandler = this.tpKeyDownHandler.bind(this);
        this.handleClockIcon = this.handleClockIcon.bind(this);



    }

    componentDidMount() {
        document.addEventListener("keydown", this.escHandler, false);
        this.datepicker.input.ariaLive = "polite";
        this.timepicker.input.ariaLive = "polite";

        this.updateState()
    }

    componentDidUpdate(prevProps) {
        if (this.props.attributes.availability !== prevProps.attributes.availability) {
            this.updateState()
        }
    }

    updateState(name, date) {
        let startTime = moment(this.props.attributes.availability.startTime, 'HH:mm');
        let startDate = moment.unix(this.props.attributes.availability.startDate)
            .set({ "h": startTime.hours(), "m": startTime.minutes() })
            .toDate()
        let endTime = moment(this.props.attributes.availability.endTime, 'HH:mm');
        let endDate = moment.unix(this.props.attributes.availability.endDate)
            .set({ "h": endTime.hours(), "m": endTime.minutes() })
            .toDate()

        let selectedDate = ("startDate" == this.props.name) ? startDate : endDate

        if (name && date) {
            startDate = ("startDate" == name) ? date.startOf('day') : startDate;
            endDate = ("endDate" == name) ? date.startOf('day') : endDate;
            selectedDate = date
        }
        this.setState({
            availability: this.props.attributes.availability,
            availabilityList: this.props.attributes.availabilitylist,
            selectedDate: selectedDate
        }, () => {
            this.setExcludeTimesForDay()
        })
    }

    setClassNameForSelectedWeekDay(className, date) {
        if (this.isWeekDaySelected(date) &&
            this.isDateInAvailabilityRange(date)
        ) {
            className = `${className} day__selected__weekday__${this.state.availability.type}`;
        }
        return className
    }

    isDateInAvailabilityRange(date) {
        return (
            date <= moment.unix(this.state.availability.endDate).toDate() &&
            date >= moment.unix(this.state.availability.startDate).toDate()
        )
    }

    isDateEqual(date1, date2) {
        return (
            date1.getDate() === date2.getDate() &&
            date1.getMonth() === date2.getMonth() &&
            date1.getFullYear() === date2.getFullYear()
        )
    }

    isWeekDaySelected(date, availability) {
        const selectedAvailability = availability ? availability : this.state.availability
        let isSelected = false;
        for (const [key, value] of Object.entries(selectedAvailability.weekday)) {
            weekDayList.map((weekday, index) => {
                if ((index + 1) == date.getDay() && weekday.value == key && value > 0) {
                    isSelected = true;
                }
            })
        }
        return isSelected;
    }

    setExcludeTimesForDay() {
        if (this.state.kind === 'exclusion') {
            return;
        }
        var times = [];

        // Add maintenance window times (22:00-01:00)
        const selectedDate = moment(this.state.selectedDate);
        for (let minute = 1; minute < 60; minute++) {
            times.push(selectedDate.clone().hour(22).minute(minute).toDate());
        }
        for (let minute = 0; minute < 59; minute++) {
            times.push(selectedDate.clone().hour(0).minute(minute).toDate());
        }

        // Filter and sort availabilities
        const availabilities = [...this.state.availabilityList]
            .filter(availability =>
                availability.id !== this.state.availability.id &&
                availability.type == this.state.availability.type &&
                this.isWeekDaySelected(this.state.selectedDate, availability)
            )
            .sort((a, b) => {
                const timeA = moment(a.startTime, 'HH:mm');
                const timeB = moment(b.startTime, 'HH:mm');
                return timeA.diff(timeB);
            });

        // Add regular excluded times
        availabilities.forEach(availability => {
            const startTime = moment(availability.startTime, 'hh:mm')
                .add(this.state.availability.slotTimeInMinutes, "m");
            const startOnDay = moment(this.state.selectedDate)
                .set({ "h": startTime.hours(), "m": startTime.minutes() })
                .toDate();

            const endTime = moment(availability.endTime, 'hh:mm')
                .subtract(this.state.availability.slotTimeInMinutes, "m");
            const endOnDay = moment(this.state.selectedDate)
                .set({ "h": endTime.hours(), "m": endTime.minutes() })
                .toDate();

            var currentTime = new Date(startOnDay);
            while (currentTime < endOnDay) {
                times.push(new Date(currentTime));
                currentTime = moment(currentTime)
                    .add(this.state.availability.slotTimeInMinutes, "m")
                    .toDate();
            }
            times.push(endOnDay);
        });

        // Add boundary timestamps between adjacent availabilities
        for (let i = 0; i < availabilities.length - 1; i++) {
            const current = availabilities[i];
            const next = availabilities[i + 1];

            const currentEnd = moment(current.endTime, 'HH:mm');
            const nextStart = moment(next.startTime, 'HH:mm');

            // If they're adjacent (end time of one equals start time of next)
            if (currentEnd.format('HH:mm') === nextStart.format('HH:mm')) {
                // Add the boundary timestamp to excluded times
                const boundaryTime = moment(this.state.selectedDate)
                    .set({ "h": currentEnd.hours(), "m": currentEnd.minutes() })
                    .toDate();
                times.push(boundaryTime);
            }
        }

        this.setState({ excludeTimeList: times });
    }

    handleChange(name, date) {
        if (!date) {
            this.closeDatePicker();
            return;
        }
        if ('startDate' == name) {
            if (this.state.availability.startDate != moment(date).startOf('day').unix()) {
                this.props.onChange("startDate", moment(date).unix());
            }
        }
        if ('endDate' == name) {
            if (this.state.availability.endDate != moment(date).startOf('day').unix()) {
                this.props.onChange("endDate", moment(date).unix());
            }
        }
        this.closeDatePicker();
    }

    handleTimeChange(name, date) {
        if ('startDate' == name) {
            if (this.state.availability.startTime != moment(date).format('HH:mm')) {
                this.props.onChange("startTime", moment(date).format('HH:mm'));
            }
        }
        if ('endDate' == name) {
            if (this.state.availability.endTime != moment(date).format('HH:mm')) {
                this.props.onChange("endTime", moment(date).format('HH:mm'));
            }
        }
        this.closeTimePicker();
    }

    escHandler(event) {
        if (event.key === "Escape") {
            this.closeDatePicker();
            this.closeTimePicker();
        }
    }

    dpKeyDownHandler(event) {
        if (event.key === 'Enter') {
            event.preventDefault()
            this.openDatePicker()
        }
    }

    tpKeyDownHandler(event) {
        if (event.key === 'Enter') {
            event.preventDefault()
            this.openTimePicker()
        }
    }

    handleCalendarIcon(event) {
        event.preventDefault();
        this.openDatePicker()
        this.datepicker.input.focus();
    }

    handleClockIcon(event) {
        event.preventDefault();
        this.openTimePicker()
        this.timepicker.input.focus();
    }

    openDatePicker() {
        if (!this.props.attributes.disabled) {
            this.setState({
                datePickerIsOpen: true,
            });
        }
    }

    openTimePicker() {
        if (!this.props.attributes.disabled) {
            this.setState({
                timePickerIsOpen: true,
            });
        }

    }

    closeDatePicker() {
        this.setState({
            datePickerIsOpen: false,
        });
    }

    closeTimePicker() {
        this.setState({
            timePickerIsOpen: false,
        });
    }

    render() {
        const dayClassName = (date) => {
            let className = "";
            className = this.setClassNameForSelectedWeekDay(className, date);
            return className;
        }

        const filterPassedTime = (time) => {
            if (!moment(this.state.selectedDate).isSame(moment.unix(this.props.attributes.today), 'day')) {
                const hour = time.getHours();
                const minutes = time.getMinutes();
                // Block maintenance window (22:00-01:00)
                return !(hour >= 22 || hour < 1 || (hour === 22 && minutes > 0));
            }

            const currentTime = moment();
            const timeToCheck = moment(time);
            const minutesDiff = timeToCheck.diff(currentTime, 'minutes');

            return minutesDiff;
        };

        /*
        const isWeekday = date => {
            const day = date.getDay();
            return day !== 0 && day !== 6;
        };
        */

        return (
            <div className="grid">
                <div className="grid__item one-half">
                    <div className="form-group">
                        <Label
                            attributes={{ "htmlFor": this.props.attributes.id, "className": "light" }}
                            value={"startDate" == this.props.name ? "Datum von" : "Datum bis"}>
                        </Label>
                        <div className="controls add-date-picker">
                            <DatePicker
                                todayButton="Heute"
                                locale="de"
                                className="form-control form-input"
                                id={this.props.attributes.id}
                                ariaDescribedBy={"help_" + this.props.attributes.id}
                                name={this.props.name}
                                dateFormat="dd.MM.yyyy"
                                selected={this.state.selectedDate}
                                onChange={date => { this.handleChange(this.props.name, date) }}
                                minDate={this.state.minDate}
                                //maxDate={repeat(this.state.availability.repeat) == 0 ? this.state.selectedDate : null}
                                //filterDate={isWeekday}
                                //excludeDates={this.state.excludeDateList}                   
                                dayClassName={dayClassName}
                                disabled={this.props.attributes.disabled}
                                onInputClick={this.openDatePicker}
                                onKeyDown={this.dpKeyDownHandler}
                                onClickOutside={this.closeDatePicker}
                                strictParsing={true}
                                open={this.state.datePickerIsOpen}
                                ref={(datepicker) => { this.datepicker = datepicker }}
                                chooseDayAriaLabelPrefix="Datumsauswahl"
                                disabledDayAriaLabelPrefix="Nicht auswählbar"
                                previousMonthAriaLabel="vorheriger Monat"
                                nextMonthAriaLabel="nächster Monat"
                                monthAriaLabelPrefix="Monat"
                            />
                            <a aria-describedby={"help_" + this.props.attributes.id} href="#" aria-label={"startDate" == this.props.name ? "Kalender Datum von öffnen" : "Kalender Datum bis öffnen"} className="calendar-placement icon" title={"startDate" == this.props.name ? "Kalender Datum von öffnen" : "Kalender Datum bis öffnen"} onClick={this.handleCalendarIcon} onKeyDown={this.dpKeyDownHandler}>
                                <i className="far fa-calendar-alt" aria-hidden="true" />
                            </a>
                        </div>
                        <Description attributes={{ "id": "help_" + this.props.attributes.id }} value="Eingabe des Datums im Format TT.MM.YYYY"></Description>
                    </div>
                </div>
                <div className="grid__item one-half">
                    <div className="form-group">
                        <Label
                            attributes={{ "htmlFor": this.props.attributes.id + "_time", "className": "light" }}
                            value={"startDate" == this.props.name ? "Uhrzeit von" : "Uhrzeit bis"}>
                        </Label>
                        <div className="controls add-date-picker">
                            <DatePicker
                                name={this.props.name + "_time"}
                                locale="de"
                                className="form-control form-input"
                                ariaDescribedBy={"help_" + this.props.attributes.id + "_time"}
                                id={this.props.attributes.id + "_time"}
                                selected={this.state.selectedDate}
                                onChange={date => { this.handleTimeChange(this.props.name, date) }}
                                showTimeSelect
                                showTimeSelectOnly
                                dateFormat="HH:mm"
                                timeFormat="HH:mm"
                                timeIntervals={this.state.availability.slotTimeInMinutes > 30 ?
                                    this.state.availability.slotTimeInMinutes : 30} // Use 30 min intervals for better UX
                                timeCaption="Uhrzeit"
                                minTime={this.state.minTime}
                                maxTime={this.state.maxTime}
                                excludeTimes={this.state.excludeTimeList}
                                filterTime={(time) => {
                                    // First check buffer for current day
                                    if (!filterPassedTime(time)) {
                                        return false;
                                    }

                                    // Get minutes since midnight
                                    const minutes = time.getHours() * 60 + time.getMinutes();
                                    const slotTime = this.state.availability.slotTimeInMinutes;

                                    // Check if time is a valid slot boundary
                                    // Allow times that could be either start or end of a slot
                                    return minutes % 30 === 0 && minutes <= (24 * 60 - slotTime);
                                }}
                                disabled={this.props.attributes.disabled}
                                onInputClick={this.openTimePicker}
                                onKeyDown={this.tpKeyDownHandler}
                                onClickOutside={this.closeTimePicker}
                                strictParsing={true}
                                open={this.state.timePickerIsOpen}
                                ref={(timepicker) => { this.timepicker = timepicker }}
                            />
                            <a href="#" aria-describedby={"help_" + this.props.attributes.id + "_time"} aria-label="Uhrzeitauswahl öffnen" className="calendar-placement icon" title={"startDate" == this.props.name ? "Uhrzeit von wählen" : "Uhrzeit bis wählen"} onClick={this.handleClockIcon} onKeyDown={this.tpKeyDownHandler}>
                                <i className="far fa-clock" aria-hidden="true" />
                            </a>
                        </div>
                        <Description attributes={{ "id": "help_" + this.props.attributes.id + "_time" }} value="Eingabe der Uhrzeit im Format HH:MM"></Description>
                    </div>
                </div>
            </div>
        )
    }
}

AvailabilityDatePicker.propTypes = {
    onChange: PropTypes.func,
    attributes: PropTypes.object
}

export default AvailabilityDatePicker

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Oops, something went wrong! Please try again later. 🐰 💔

@ThomasAFink
Copy link
Member Author

ThomasAFink commented Feb 5, 2025

@coderabbitai for past opening hours the checkboxes are not disabled but should be

Wochentage and Die Dienstleistungen dürfen mehr als einen Zeitschlitz beanspruchen

import React from 'react'
import PropTypes from 'prop-types'
import * as Inputs from '../../../lib/inputs'
import AvailabilityDatePicker from './datepicker'
const { Label, FormGroup, Controls, Description } = Inputs
import { range } from '../../../lib/utils'
import { weekDayList, availabilitySeries, availabilityTypes, getDataValuesFromForm } from '../helpers'
import ErrorBar from '../errorBar'

const FormContent = (props) => {
    const {
        availabilityList,
        data,
        errorList,
        conflictList,
        onChange,
        today,
        setErrorRef
    } = props;

    const hasEndTimePastError = Object.values(errorList)
        .some(error => error.itemList?.[0]?.[0]?.type === 'endTimePast');
    const calenderDisabled = data.type && data.slotTimeInMinutes ? false : true;
    const inputDisabled = hasEndTimePastError || calenderDisabled;

    const isUnsafedSpontaneous = data.id == 0;

    const filteredErrorList = Object.values(errorList)
        .filter(error => error.itemList?.[0]?.[0]?.type !== 'endTimePast')
        .reduce((acc, error) => {
            acc[error.id] = error;
            return acc;
        }, {});

    return (
        <div>
            <form className="form--base">
                <ErrorBar errorList={filteredErrorList} conflictList={conflictList} setErrorRef={setErrorRef} />
                <fieldset>
                    {isUnsafedSpontaneous ?
                        <section
                            style={{
                                position: 'relative',
                                borderColor: '#cccccc',
                                margin: '20px auto'
                            }}
                            className="dialog message"
                            role="alert"
                        >
                            <div style={{
                                position: 'absolute',
                                top: '-15px',
                                left: '7px',
                                backgroundColor: '#fcaa67',
                                width: '30px',
                                height: '30px',
                                display: 'flex',
                                justifyContent: 'center',
                                alignItems: 'center'
                            }}>
                                <i className="fas fa-exclamation-circle" title="Wichtiger Hinweis" aria-hidden="true" style={{ color: 'white' }} />
                            </div>
                            <h2 className="message__heading">Hinweis zur Termin-Öffnungszeit</h2>
                            <div className="message__body">
                                Diese Öffnungszeit ist eine mit einer Termin-Öffnungszeit verbundenen Spontankunden-Öffnungszeit und hat noch keine eigene ID. Sie können diese Öffnungszeit erst einzeln aktualisieren, wenn sie die dazugehörige Termin-Öffnungszeit mit einmal initial aktualisiert haben.
                            </div>
                        </section> : null
                    }
                    {hasEndTimePastError && Object.values(errorList).map(error => {
                        const endTimePastError = error.itemList?.[0]?.[0];
                        if (endTimePastError?.type === 'endTimePast') {
                            return (
                                <section
                                    key={error.id}
                                    style={{
                                        position: 'relative',
                                        borderColor: '#cccccc',
                                        margin: '20px auto'
                                    }}
                                    className="dialog message"
                                    role="alert"
                                >
                                    <div style={{
                                        position: 'absolute',
                                        top: '-15px',
                                        left: '7px',
                                        backgroundColor: '#fcaa67',
                                        width: '30px',
                                        height: '30px',
                                        display: 'flex',
                                        justifyContent: 'center',
                                        alignItems: 'center'
                                    }}>
                                        <i className="fas fa-exclamation-circle" title="Wichtiger Hinweis" aria-hidden="true" style={{ color: 'white' }} />
                                    </div>
                                    <h2 className="message__heading">Öffnungszeit liegt in der Vergangenheit</h2>
                                    <div className="message__body">
                                        {endTimePastError.message}
                                    </div>
                                </section>
                            );
                        }
                        return null;
                    })}
                    <div className="panel--heavy">
                        <FormGroup>
                            <Label attributes={{ "htmlFor": "AvDayDescription" }}>Anmerkung</Label>
                            <Controls>
                                <Inputs.Text
                                    attributes={{ "id": "AvDayDescription", "aria-describedby": "help_AvDayDescription", "disabled": inputDisabled }}
                                    name="description"
                                    value={data.description}
                                    {...{ onChange }}
                                />
                                <Description attributes={{ "id": "help_AvDayDescription" }}>Optionale Angabe zur Kennzeichnung des Termins. {data.id > 0 ? "Die ID der Öffnungszeit ist " + data.id : " Die Öffnungszeit hat noch keine ID"}.
                                </Description>
                            </Controls>
                        </FormGroup>
                        <FormGroup>
                            <Label attributes={{ "htmlFor": "AvDayType" }}>Typ</Label>
                            <Controls>
                                <Inputs.Select name="type"
                                    attributes={{ disabled: data.id ? 'disabled' : null, "id": "AvDayType" }}
                                    value={data.type ? data.type : "appointment"} {...{ onChange }}
                                    options={availabilityTypes} />
                                <Description attributes={{ "id": "help_AvDayTypDescription" }}>Typ der Öffnungszeit.
                                </Description>
                            </Controls>
                        </FormGroup>
                    </div>
                </fieldset>
                <fieldset>
                    <div className="panel--heavy">
                        <legend className="label">Serie und Wochentage</legend>
                        <FormGroup>
                            <Label attributes={{ "htmlFor": "AvDaySeries", "className": "light" }}>Serie</Label>
                            <Controls>
                                <Inputs.Select
                                    name="repeat"
                                    attributes={{ "id": "AvDaySeries", "disabled": inputDisabled }}
                                    value={data.repeat} {...{ onChange }}
                                    options={availabilitySeries} />
                            </Controls>
                        </FormGroup>
                        <FormGroup>
                            <Label attributes={{ "className": "light" }}>Wochentage</Label>
                            <Controls>
                                <Inputs.CheckboxGroup name="weekday"
                                    value={data.weekday}
                                    inline={true}
                                    {...{ onChange }}
                                    boxes={weekDayList}
                                    attributes={{ "disabled": inputDisabled }}
                                    disabled={!data.repeat ? true : false}
                                />
                            </Controls>
                        </FormGroup>
                    </div>
                </fieldset>
                <fieldset>
                    <div className="panel--heavy">
                        <legend className="label">Terminabstand</legend>
                        <FormGroup inline={true}>
                            <Controls>
                                <Inputs.Text name="slotTimeInMinutes"
                                    value={data.slotTimeInMinutes}
                                    width="2"
                                    attributes={{ disabled: 'disabled', maxLength: 3, "id": "AvDaySlottime" }}
                                    {...{ onChange }} />
                                <Label attributes={{ "htmlFor": "AvDaySlottime", "className": "light" }}>&nbsp;Minuten Abstand zweier aufeinander folgender Termine</Label>
                            </Controls>
                        </FormGroup>
                        <FormGroup inline={true}>
                            <Controls>
                                <Inputs.Checkbox name="multipleSlotsAllowed"
                                    checked={"1" == data.multipleSlotsAllowed} {...{ onChange }}
                                    value="1"
                                    attributes={{ "id": "AvDayMultipleSlots", "disabled": inputDisabled }}
                                />
                                <Label attributes={{ "htmlFor": "AvDayMultipleSlots", "className": "light" }}>Die Dienstleistungen dürfen mehr als einen Zeitschlitz beanspruchen</Label>
                            </Controls>
                        </FormGroup>
                    </div>
                </fieldset>

                <fieldset>
                    <div className="panel--heavy">
                        <legend className="label">Öffnungszeit</legend>
                        <AvailabilityDatePicker attributes={{
                            "id": "AvDatesStart",
                            "availabilitylist": availabilityList,
                            "availability": getDataValuesFromForm(data, data.scope),
                            "today": today,
                            "kind": data.kind,
                            "disabled": inputDisabled
                        }} name="startDate" {...{ onChange }} />
                        <AvailabilityDatePicker attributes={{
                            "id": "AvDatesEnd",
                            "availabilitylist": availabilityList,
                            "availability": getDataValuesFromForm(data, data.scope),
                            "today": today,
                            "kind": data.kind,
                            "disabled": inputDisabled
                        }} name="endDate" {...{ onChange }} />
                    </div>
                </fieldset>

                <fieldset>
                    <div className="panel--heavy">
                        <legend className="label">Buchbar</legend>
                        <FormGroup inline={true}>
                            <Controls>
                                <Label attributes={{ "htmlFor": "AvDayOpenfrom", "className": "light" }}>von</Label>
                                <Inputs.Text name="open_from"
                                    width="3"
                                    value={data.open_from}
                                    attributes={{ placeholder: data.scope.preferences.appointment.startInDaysDefault, "id": "AvDayOpenfrom", "aria-describedby": "help_AvDayOpenfromto", "disabled": inputDisabled }}
                                    {...{ onChange }}
                                />
                            </Controls>
                            <Controls>
                                <Label attributes={{ "htmlFor": "AvDayOpento", "className": "light" }}>bis</Label>
                                <Inputs.Text name="open_to"
                                    width="3"
                                    value={data.open_to}
                                    attributes={{ placeholder: data.scope.preferences.appointment.endInDaysDefault, "id": "AvDayOpento", "aria-describedby": "help_AvDayOpenfromto", "disabled": inputDisabled }}
                                    {...{ onChange }}
                                />
                                <span aria-hidden="true"> Tage im voraus</span>
                            </Controls>
                            <Description attributes={{ "id": "help_AvDayOpenfromto" }}>Tage im voraus (Keine Eingabe bedeutet die Einstellungen vom Standort zu übernehmen).</Description>
                        </FormGroup>
                    </div>
                </fieldset>
                {data.type !== 'openinghours' ?
                    <fieldset>
                        <div className="panel--heavy">
                            <legend>Terminarbeitsplätze</legend>
                            <div>
                                <FormGroup>
                                    <Label attributes={{ "htmlFor": "WsCountIntern" }}>Insgesamt</Label>
                                    <Controls>
                                        <Inputs.Select name="workstationCount_intern"
                                            value={data.workstationCount_intern}
                                            attributes={{ "id": "WsCountIntern", "disabled": inputDisabled }}
                                            {...{ onChange }}
                                            options={range(0, 50).map(n => {
                                                let workstation = (n == 1) ? "Arbeitsplatz" : "Arbeitsplätze";
                                                return {
                                                    title: `${n} ${workstation}`,
                                                    value: `${n}`,
                                                    name: `${n} ${workstation}`
                                                }
                                            })} />
                                    </Controls>
                                </FormGroup>

                                <FormGroup>
                                    <Label attributes={{ "htmlFor": "WsCountCallcenter" }}>Callcenter</Label>
                                    <Controls>
                                        <Inputs.Select name="workstationCount_callcenter"
                                            value={data.workstationCount_callcenter}
                                            attributes={{ "id": "WsCountCallcenter", "aria-describedby": "help_WsCountCallcenter", "disabled": inputDisabled }}
                                            {...{ onChange }}
                                            options={range(0, data.workstationCount_intern).map(n => {
                                                let workstation = (n == 1) ? "Arbeitsplatz" : "Arbeitsplätze";
                                                return {
                                                    title: `${n} ${workstation}`,
                                                    value: `${n}`,
                                                    name: `${n} ${workstation}`
                                                }
                                            })} />
                                        <Description attributes={{ "id": "help_WsCountCallcenter" }}>Wieviele der insgesamt verfügbaren Terminarbeitsplätze sollen für das Callcenter zur Verfügung gestellt werden.</Description>
                                    </Controls>
                                </FormGroup>

                                <FormGroup>
                                    <Label attributes={{ "htmlFor": "WsCountPublic" }}>Internet</Label>
                                    <Controls>
                                        <Inputs.Select name="workstationCount_public"
                                            value={data.workstationCount_public}
                                            attributes={{ "id": "WsCountPublic", "aria-describedby": "help_WsCountPublic", "disabled": inputDisabled }}
                                            {...{ onChange }}
                                            options={range(0, data.workstationCount_intern).map(n => {
                                                let workstation = (n == 1) ? "Arbeitsplatz" : "Arbeitsplätze";
                                                return {
                                                    title: `${n} ${workstation}`,
                                                    value: `${n}`,
                                                    name: `${n} ${workstation}`
                                                }
                                            })} />
                                        <Description attributes={{ "htmlFor": "help_WsCountPublic" }}>Wieviele der insgesamt verfügbaren Terminarbeitsplätze sollen für das Internet zur Verfügung gestellt werden.</Description>
                                    </Controls>
                                </FormGroup>
                            </div>
                        </div>
                    </fieldset>
                    : null}
            </form>
        </div>
    )
}

FormContent.propTypes = {
    availabilityList: PropTypes.array,
    errorList: PropTypes.object,
    conflictList: PropTypes.object,
    today: PropTypes.number,
    data: PropTypes.object,
    errors: PropTypes.object,
    onChange: PropTypes.func,
    setErrorRef: PropTypes.func,
}

export default FormContent 

Copy link
Contributor

coderabbitai bot commented Feb 5, 2025

Oops, something went wrong! Please try again later. 🐰 💔

3 similar comments
Copy link
Contributor

coderabbitai bot commented Feb 5, 2025

Oops, something went wrong! Please try again later. 🐰 💔

Copy link
Contributor

coderabbitai bot commented Feb 5, 2025

Oops, something went wrong! Please try again later. 🐰 💔

Copy link
Contributor

coderabbitai bot commented Feb 5, 2025

Oops, something went wrong! Please try again later. 🐰 💔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant