/* global SCE, moment, $ */
SCE.add('sce-ds-scheduler-view', function (Y) {
    'use strict'

    const CLASS_NAME = 'sceDsSchedulerView'
    const CALENDAR_RESOURCE_NAME = 'Dock'
    const SCHEDULER = Y.SCE.Scheduler
    const CALENDAR_MANAGER = Y.SCE.ds.CalendarManager
    const TIME_SLOT_COVER_SERVICE = Y.SCE.ds.timeSlotCoverService
    const ERROR = Y.SCE.Error;
    const CONTEXT = Y.SCE.Context
    const TOOLTIP_ELEMENT = `<div class='tooltips' data-sce-intl='ds:views/SchedulerView:toRescheduleTooltip'>tooltip</div>`
    const PICKER_CLOSE_EVENT = Y.SCE.Picker.Select.CLOSE_EVT

    /**
     * [arraysCompare description]
     * @param  {[type]} first  [description]
     * @param  {[type]} second [description]
     * @return {[type]}        [description]
     */
    function arraysCompare(first, second) {
        if (!first && !second) {
            return true
        } else if (!first || !second) {
            return false
        }

        if (first.length !== second.length) {
            return false
        }

        for (let i = 0; i < first.length; i++) {
            // Check if there are nested arrays
            if (first[i] instanceof Array && second[i] instanceof Array) {
                // recurse into the nested arrays
                if (!arraysCompare(first[i], second[i])) {
                    return false
                }
            } else if (first[i] !== second[i]) {
                // Warning: doesn't work with objects
                return false
            }
        }
        return true
    }

    /**
     * Determinate if the dragged element is dropped on the external-events div
     * @param x
     * @param y
     * @return
     */
    function _isEventOverExternalEventsDiv(x, y) {
        let externalEvents = $('.external-events')
        let offset = externalEvents.offset()
        offset.right = externalEvents.width() + offset.left
        offset.bottom = externalEvents.height() + offset.top

        if (x >= offset.left &&
            y >= offset.top &&
            x <= offset.right &&
            y <= offset.bottom) {
            return true
        }
        return false
    }

    /**
     * Check if two events (time slot) are overlapping
     * @param  Object e1 first event
     * @param  Object e2 second event
     * @return boolean
     */
    function _areEventsOverlapping(e1, e2) {
        return ((e1.start >= e2.start &&
                e1.start < e2.end) ||
            (e1.end > e2.start &&
                e1.end <= e2.end))
    }

    /**
     * [_getOverlappingEvents description]
     * @param  {[type]} currentEvent [description]
     * @param  {[type]} events       [description]
     * @return {[type]}              [description]
     */
    function _getOverlappingEvents(currentEvent, events) {
        let overlappingEvents = events.filter(e => e !== currentEvent &&
            _areEventsOverlapping(e, currentEvent))
        if (overlappingEvents.length === 0) {
            return 0
        }
        return 1 + parseInt(overlappingEvents.map(e => _getOverlappingEvents(e, overlappingEvents)))
    }

    /**
     * [_collapseNavigation description]
     * @return {[type]} [description]
     */
    function _collapseNavigation() {
        let mainContentNode = Y.one('#sce-app-main-content')
        let navigationNodes = Y.all('.sce-app-nav')
        let isExpanded = mainContentNode.hasClass('sce-app-full-width')
        if (!isExpanded) {
            mainContentNode.addClass('sce-app-full-width')
            navigationNodes.hide()
        }
    }

    /**
     * [_enableUserInteraction description]
     * @param  {[type]} className [description]
     * @return {[type]}           [description]
     */
    function _enableUserInteraction(className) {
        $(`.${CLASS_NAME}`).removeClass(`disable-pointer-event-${className}`)
    }

    /**
     * [_disableUserInteraction description]
     * @param  {[type]} className [description]
     * @return {[type]}           [description]
     */
    function _disableUserInteraction(className) {
        $(`.${CLASS_NAME}`).addClass(`disable-pointer-event-${className}`)
    }

    function getIntersection(setA, setB) {
        const intersection = new Set(
            [...setA].filter(element => setB.has(element))
        );
        return intersection;
    }

    /**
     * Set activity's constraint on resources merging type matches from items and trucks
     * @param  {[type]} activity  [description]
     * @param  {[type]} resources [description]
     * @return {[type]}           [description]
     */
    function _checkCompatibilityBtwActivityAndResources(activity, resources) {
        // Set constraint on resources merging type matches from items and trucks
        let constraint = new Set(resources.map(r => r.id))
        if (activity.itemTypeIds && activity.itemTypeIds.length > 0) {
            let constraintItemType = resources.filter(r => !r.itemTypeConstraints || r.itemTypeConstraints.length === 0 || r.itemTypeConstraints.indexOf(parseInt(activity.itemTypeIds[0].entityId)) !== -1).map(r => r.id)
            constraint = getIntersection(constraintItemType, constraint);
        }
        if (activity.truckTypeId) {
            let constraintTruckType = resources.filter(r => !r.truckTypeConstraints || r.truckTypeConstraints.length === 0 || r.truckTypeConstraints.indexOf(activity.truckTypeId) !== -1).map(r => r.id)
            constraint = getIntersection(constraintTruckType, constraint);
        }
        if (activity.activityType) {
            let constraintActivityType = resources.filter(r => !r.activityTypeConstraints || r.activityTypeConstraints.length === 0 || r.activityTypeConstraints.indexOf(parseInt(activity.activityType.id)) !== -1).map(r => r.id)
            constraint = getIntersection(constraintActivityType, constraint);
        }
        constraint = [...constraint]

        var siteConstraint = resources.filter(r => r.site === activity.appointmentReference.site.code).map(r => r.id);
        var plantConstraint = siteConstraint;
        if (activity.appointmentReference.referenceRequest.plant) {
            plantConstraint = resources.filter(r => r.plant === activity.appointmentReference.referenceRequest.plant.code).map(r => r.id);
        }
        constraint = constraint.filter(r => {
            return siteConstraint.indexOf(r) > -1 && plantConstraint.indexOf(r) > -1;
        });
        return constraint
    }

    /* **************** HANDLEBARS HELPER **************** */
    Y.Handlebars.registerHelper('json', (context) => {
        return JSON.stringify(context)
    })

    Y.Handlebars.registerHelper('formatDate', (context) => {
        return Y.SCE.Picker.Date.formatOutputValue(context, {dataType: 'java.time.LocalDate'}).replace(/\//g, '-')
    })
    /* *************************************************** */

    Y.namespace('SCE.ds').SchedulerView = Y.Base.create(CLASS_NAME, Y.View, [], {

        events: {
            '.dock-filter .sce-picker-multiselect .tagscontainer .selectedelem a i': {
                click: '_removeDockFromPickerClick'
            },
            '.requestor-filter .sce-picker-multiselect .tagscontainer .selectedelem a i': {
                click: '_removeRequestorFromPickerClick'
            },
            '.date-filter input': {
                valuechange: '_valueChangeDateFilter'
            }
        },


        /**
         * Called when page size changes
         * @method _onWindowResize
         * @private
         */
        _onWindowResize(parameters){
            try {
                if(parameters && parameters.data && parameters.data.caller) {
                    parameters.data.caller._setContainerHeight();
                }
            } catch (error) {
                ERROR.notify(error);
                console.error('_onWindowResize' + error);
                throw new Error(error);
            }
        },

        initializer(params) {
            _collapseNavigation()

            // Flag to avoid multiple call for dates of unplaced activities
            this.avoidCallForDates = false


            this.startWorkingDay = null
            this.endWorkingDay = null

            this.accordionPartialTemplate = Y.Handlebars.partials['requestor-unplaced-activities-partial-template']

            // HTML node element reference used to re-attach dragged node
            this.draggedNodeParent = null

            this._eventHandles = []
            this.siteTimeZone= null;
            this.formatter= null;

            // Add html picker events
            this._eventHandles.push.apply(this._eventHandles, Y.SCE.Picker.Base.addHTMLPickerEvents(this.get('container'), this))
            this._eventHandles.push($(window).on('resize', { caller : this }, this._onWindowResize));

            // Array of displayed resources
            this.resources = []

            // Array of resources selected from filter
            this.selectedResources = []

            // Array of requestors selected from filter
            this.selectedRequestors = []
            this.selectedRequestorsPrevious = []

            // Array of delivery windows of requestors
            this.deliveryWindows = []

            // Array of docks selected from filter
            this.selectedDocks = []
            this.selectedDocksPrevious = []

            // Array of activities already placed on calendar
            this.activitiesOnCalendar = []

            // Array of requestor's activities
            this.requestorActivities = []

            // Create model for pickers
            this.pickerModel = new Y.Model({
                requstorPickerSelect: [],
                dockPickerSelect: [],
                dateSelect: null
            })


            this.dockPickerSelectHandle = null
            this.requestorPickerSelectHandle = null;

            this.datePicker = new Y.SCE.Picker.Date({
                attributeConfig: {
                    dataType: 'java.time.LocalDate',
                    name: 'dateSelect'
                },
                model: this.pickerModel,
                displayMode: Y.SCE.Picker.Base.DISPLAY_MODE_EDIT
            });
            this.datePicker.addTarget(this);

            this._eventHandles.push(
                this.datePicker.after('dateSelectChange', this._valueChangeDateFilter)
            )

            // Get data for multiselect picker of requestors
            this.loadRequestorsPromise = CALENDAR_MANAGER.loadRequestors()
                .then(requestors => {
                    this.requestorPickerSelectSource = requestors
                    // Create picker to filter requestors
                    this.requestorPickerSelect = new Y.SCE.Picker.Select({
                        model: this.pickerModel,
                        displayMode: Y.SCE.Picker.Base.DISPLAY_MODE_EDIT,
                        attributeConfig: {
                            name: 'requstorPickerSelect'
                        },
                        multiselect: true,
                        groupedOptions: [
                            {
                                groupName: '',
                                optionsList: requestors.map(r => ({
                                    value: (r.description && r.description !== '' ? r.description : r.code),
                                    key: r.code
                                }))
                            }
                        ]
                    })

                    this.requestorPickerSelect.addTarget(this)

                    // Add callback when requestor picker disappears
                    if (this.requestorPickerSelectHandle !== null) {
                        this.requestorPickerSelectHandle.detach()
                        this.requestorPickerSelectHandle = null
                    }
                    this.requestorPickerSelectHandle = this.requestorPickerSelect.on(PICKER_CLOSE_EVENT, () => {
                        this.selectedRequestors = this.requestorPickerSelectSource.filter(r => (Object.values(this.requestorPickerSelect.optionsMap).filter(o => this.requestorPickerSelect.getData().includes(o.value)).map(o => o.key)).includes(r.code))
                        if (!arraysCompare(this.selectedRequestors, this.selectedRequestorsPrevious)) {
                            this.scheduler.fullCalendar('refetchEvents')
                            this.selectedRequestorsPrevious = this.selectedRequestors
                        }
                    })
                })

            // Get data for multiselect picker of docks
            this.filterBySite = params.dockFilteredBySite && params.dockFilteredBySite === true;
            this.dockPickerSelectSource = [];
            this.loadDocksPromise = CALENDAR_MANAGER.loadDocks(this.filterBySite ? CONTEXT.get('ds.site').code : null)
                .then(docks => {
                    this.dockPickerSelectSource = docks
                    // Create picker to filter docks
                    this.dockPickerSelect = new Y.SCE.Picker.Select({
                        model: this.pickerModel,
                        displayMode: Y.SCE.Picker.Base.DISPLAY_MODE_EDIT,
                        attributeConfig: {
                            name: 'dockPickerSelect'
                        },
                        multiselect: true,
                        selectAllOpts: true,
                        groupedOptions: [
                            {
                                groupName: '',
                                optionsList: docks.map(d => ({
                                    value: d.description ? d.description : d.code,
                                    key: d.code
                                }))
                            }
                        ]
                    })

                    this.dockPickerSelect.addTarget(this)

                    // Add callback when dock picker disappears
                    if (this.dockPickerSelectHandle !== null) {
                        this.dockPickerSelectHandle.detach()
                        this.dockPickerSelectHandle = null
                    }
                    this.dockPickerSelectHandle = this.dockPickerSelect.on(PICKER_CLOSE_EVENT, () => {
                        let docksSelectedElem = Object.values(this.dockPickerSelect.optionsMap).filter(o => this.dockPickerSelect.getData().includes(o.value))
                        this.selectedDocks = this.dockPickerSelectSource.filter(r => docksSelectedElem.map(o => o.key).includes(r.code))
                        if (!arraysCompare(this.selectedDocks, this.selectedDocksPrevious)) {
                            if(this.scheduler!=null){
                                this.scheduler.fullCalendar('refetchResources')
                                this.selectedDocksPrevious = this.selectedDocks

                            }
                        }
                    })
                })

            // Calculate time window in which the user can move activities even if they are inside the cut-off time
            let regexp = /(\d\d):(\d\d):(\d\d)/g
            let match = regexp.exec(params.timeWindowBeforeCutOffControl)
            this.timeWindowBeforeCutOffControl = (parseInt(match[1]) * 3600 + parseInt(match[2]) * 60 + parseInt(match[3])) * 1000

            /* **************** FULLCALENDAR **************** */

            this.fullCalendar = params.fullCalendar

            this.fullCalendar.resourceLabelText = CALENDAR_RESOURCE_NAME

            this.fullCalendar.dragRevertDuration = 0

            let slotDuration = this.fullCalendar.views.agendaDay.slotDuration
            this.slotDurationMinutes = parseInt(slotDuration.substring(0, 2)) * 60 + parseInt(slotDuration.substring(3, 5))


            // Add refresh custom button
            this.fullCalendar.customButtons = {
                'refresh': {
                    'text': 'Refresh',
                    'click': () => {
                        this.scheduler.fullCalendar('refetchEvents')
                    },
                    icon: 'refresh'
                }
            }

            // See {@link https://fullcalendar.io/docs/resource_data/resources_function/}
            this.fullCalendar.resources = callback => {
                let docks = (this.selectedDocks.length > 0 ? this.selectedDocks : (this.dockPickerSelectSource.length > 0 ? this.dockPickerSelectSource : []))

                if (docks.length > 0) {
                    CALENDAR_MANAGER.loadResources({
                        name: CALENDAR_RESOURCE_NAME,
                        entityId: docks.map(sd => sd.id)
                    })
                        .then(resources => {
                            this.resources = resources

                            CALENDAR_MANAGER.loadItemTypeMatch(this.selectedDocks.map(d => d.entityId))
                                .then(itemTypeConstraints => {
                                    this.resources.forEach(r => {
                                        r.itemTypeConstraints = itemTypeConstraints[r.referencedEntity.entityId]
                                    })
                                })
                            docks.forEach(d => {
                                this.resources.filter(r => r.referencedEntity.entityId == d.id).forEach(rr => {
                                    rr.plant = d.building.plant.code;
                                    rr.site = d.building.site.code;
                                })
                            })
                            CALENDAR_MANAGER.loadActivityTypeMatch(this.selectedDocks.map(r => r.entityId))
                                .then(activityTypeConstraints => {
                                    this.resources.forEach(r => {
                                        r.activityTypeConstraints = activityTypeConstraints[r.referencedEntity.entityId]
                                    })
                                })
                            CALENDAR_MANAGER.loadTruckTypeMatch(this.selectedDocks.map(d => d.entityId))
                                .then(truckTypeConstraints => {
                                    this.resources.forEach(r => {
                                        r.truckTypeConstraints = truckTypeConstraints[r.referencedEntity.entityId]
                                    })
                                })

                            let dockPlantMap = docks.map(d => ({'id': d.id, 'plantCode': d.building.plant.code}))
                            new Set(docks.map(d => d.building.plant.code)).forEach(pc => {
                                CALENDAR_MANAGER.loadActivitiesOverlappingMinutesLimit(pc)
                                    .then(overlapLimit => {
                                        this.resources
                                            .filter(r => dockPlantMap.filter(d => d.plantCode === pc)
                                                .map(d => d.id)
                                                .includes(parseInt(r.referencedEntity.entityId)))
                                            .forEach(r => {
                                                r.overlapLimit = overlapLimit * 60 * 1000
                                            })
                                    })
                            })
                            this.resources.forEach(function (r) {
                                let completeTitle = r.referencedEntity.businessKey;
                                if (r.capacity) {
                                    completeTitle = completeTitle + ' (' + r.capacity + ')';
                                }
                                r.title = completeTitle;
                            });
                            callback(this.resources)
                        })
                } else {
                    this.resources = []
                    callback(this.resources)
                }
            }

            /**
             * Triggered when event fetching starts/stops.
             * !!! NOTE: Triggered also when resource fetching starts/stops. !!!
             * See {@link https://fullcalendar.io/docs/event_data/loading/}
             * @param  {[type]} isLoading [description]
             * @param  {[type]} view      [description]
             * @return {[type]}           [description]
             */
            this.fullCalendar.loading = (isLoading, view) => {
                if (isLoading) {
                    // Disable mouse events
                    _disableUserInteraction('loading')
                    $('html').css('cursor', 'wait')
                } else {
                    // Finished loading events

                    // Re-enable mouse events
                    _enableUserInteraction('loading')
                    $('html').css('cursor', 'auto')
                }
            }



            /**
             * FullCalendar will call this function whenever it needs new event data.
             * This is triggered when the user clicks prev/next or switches views.
             * See {@link https://fullcalendar.io/docs/event_data/events_function/ Fullcalendar}
             */
            this.fullCalendar.events = (start, end, timezone, callback) => {

                // Create token to avoid multiple parallel requests

                let currentToken = this.token = Math.random()

                let from = this.preserveTimeInTimezone(start, this.siteTimeZone).valueOf();
                let to = this.preserveTimeInTimezone(end, this.siteTimeZone).valueOf();

                //set the date of the picker to the date of the calendar
                this.datePicker.setData(moment(from));
                this.currentDay = moment(from);
                this._changeDatePickerDisplayMode(true)
                let allPlacedActivities = []
                let inactivity = []

                let requestorUnplacedActivities = []
                let requestorPlacedActivities = []

                let promises = []

                // Retrieve all the activities included in the temporal interval shown in the calendar
                promises.push(CALENDAR_MANAGER.loadActivities(from, to, this.selectedRequestors.map(r => r.id), 'manager', params.appointmentConf, this.filterBySite ? CONTEXT.get('ds.site').code : null)
                    .then(activities => {
                        activities.forEach(activity => {
                            activity.draggableIndicatorString = "" //default is empty
                            activity.appStatusString = `<div class='appointment-status-indicator' data-sce-intl=` + "'ds:resources/Appointment:status." + activity.appointmentReference.status + "'>" + activity.appointmentReference.status + "</div>"

                            switch (activity.appointmentReference.status) {
                                case 'TO_PLAN':
                                    activity.className.push('appointment-to-plan')
                                    activity.draggableIndicatorString = `<div class="draggable-indicator"></div>` // Add handle to drag
                                    break
                                case 'PLANNING':
                                    activity.className.push('appointment-planning')
                                    activity.draggableIndicatorString = `<div class="draggable-indicator"></div>` // Add handle to drag
                                    break
                                case 'PLANNED':
                                    activity.className.push('appointment-planned')
                                    activity.draggableIndicatorString = `<div class="draggable-indicator"></div>` // Add handle to drag
                                    break
                                case 'IN_PROGRESS':
                                    activity.className.push('appointment-in-progress')
                                    break
                                case 'COMPLETED':
                                    activity.className.push('appointment-completed')
                                    activity.editable = false
                                    break
                                case 'CONFIRMED':
                                    activity.className.push('appointment-confirmed')
                                    activity.editable = false
                                    break
                            }

                            switch (activity.timeSlotAllocationStatus) {
                                case 'PENDING':
                                    // Correct activity title escaping double quotes
                                    let myRegexp = /(<a.+>)(.*<\/a>)/g
                                    let match = myRegexp.exec(activity.title)
                                    let escapedTagContent = match[1].replace(/"/g, '\\"')
                                    let escapedDraggableIndicatorString = activity.draggableIndicatorString.replace(/"/g, '\\"')
                                    activity.title = escapedDraggableIndicatorString.concat(escapedTagContent).concat(match[2])

                                    requestorUnplacedActivities.push(activity)
                                    break
                                case 'PARTIAL':
                                    // TODO
                                    break
                                case 'COMPLETE':
                                    if (activity.toReschedule) {
                                        // Add icon to indicate that the activity has to be rescheduled
                                        activity.toRescheduleIconString = `<div class='to-reschedule-icon'></div>`
                                        activity.title = activity.title.concat(activity.toRescheduleIconString)
                                    }

                                    // TODO: Get also completed activities that are placed on more days?
                                    // TODO: Check separately timeslots
                                    // TOEVALUATE: Get only completed activities that start and end in the selected day
                                    // if (activity.timeSlots[0].start.valueOf() > from && activity.timeSlots[0].end.valueOf() < to) {

                                    // Add drag handle to activity title
                                    activity.title = activity.draggableIndicatorString.concat(activity.title)

                                    requestorPlacedActivities.push(activity)
                                    break
                                default:
                                    // TODO raise error?
                                    break
                            }

                            //if recurrent appointment
                            if(activity.appointmentReference.recurrentAppointment){
                                let recurrentIconString = `<span class="fc-icon fc-icon-refresh recurrent-appointment-icon"></span>`
                                activity.title = `<div class="fc-title-recurrent-appointment">` + activity.title + recurrentIconString + `</div>`
                            }

                            //status string
                            activity.title = activity.title.concat(activity.appStatusString)
                        })
                        requestorUnplacedActivities = this._hideIfCantPlanToday(requestorUnplacedActivities,requestorPlacedActivities, from)

                        this.requestorActivities = requestorUnplacedActivities.concat(requestorPlacedActivities)
                        return CALENDAR_MANAGER.loadActivitiesResourceId(requestorUnplacedActivities.map(a => a.id))
                    })
                    .then(activitiesResources => {
                        if (activitiesResources && activitiesResources.length > 0) {
                            requestorUnplacedActivities.map(ua => {
                                if (activitiesResources.map(ar => ar.activity.id).indexOf(ua.id) !== -1) {
                                    ua.resourceId = activitiesResources.find(ar => ar.activity.id === ua.id).resource.id
                                }
                            })
                        }
                    }))

                promises.push(CALENDAR_MANAGER.loadRequestors()
                    .then(requestors => {
                        let tmp = requestors.map(sr => CONTEXT.get('ds.site').code.concat(` ${sr.code}`))
                        let deliveryWindowFilter = [{
                            name: 'DeliveryWindow',
                            businessKeys: tmp
                        }]
                        return CALENDAR_MANAGER.loadResourceAllocationInfo(from, to, null, null, true, deliveryWindowFilter)
                    })
                    .then(allocationInfo => {
                        this.deliveryWindows = allocationInfo.map((info, index) => {
                            let regex = /[^ ]+ ([^-]+)/g
                            let matches = regex.exec(info.resource.referencedEntity.businessKey)
                            if (matches) {
                                return ({
                                    requestorCode: matches[1],
                                    inactivity: info.inactivity
                                })
                            }
                        })
                    }))

                let docks = (this.selectedDocks.length > 0 ? this.selectedDocks : (this.dockPickerSelectSource.length > 0 ? this.dockPickerSelectSource : []))
                let resourcesFilter = [{
                    name: 'Requestor',
                    businessKeys: this.selectedRequestors.map(sr => sr.code)
                },
                    {
                        name: CALENDAR_RESOURCE_NAME,
                        businessKeys: docks.map(sd => sd.code)
                    }]
                promises.push(CALENDAR_MANAGER.loadResourceAllocationInfo(from, to, null, null, true, resourcesFilter)
                    .then(allocationInfo => {
                        return CALENDAR_MANAGER.loadResourceAllocationInfo(from, to, null, null, true, [{
                            name: "Site",
                            businessKeys: CONTEXT.get('ds.site').code
                        }]).then(infoSite => {
                            allocationInfo.forEach(info => {
                                // Resource
                                // Handled in another way for now. See loadResources in fullCalendar.resources

                                if (!info.activities) {
                                    info.activities = []
                                }
                                allPlacedActivities = allPlacedActivities.concat(info.activities.map(x => ({
                                    id: x.activity.id,
                                    title: '',
                                    resourceId: info.resource.id,
                                    start: moment(x.from),
                                    end: moment(x.to),
                                    editable: false,
                                    overlap: true,
                                    appointmentReference: {
                                        id: x.activity.referencedEntity.entityId
                                    },
                                    className: 'other-requestors-activities'
                                })))
                                // Inactivities
                                if (!info.inactivity) {
                                    info.inactivity = []
                                }
                                if (info.resource.referencedEntity.entityName === "Dock") {
                                    inactivity = inactivity.concat(info.inactivity.map((range, index) => ({
                                        id: `${info.resource.id}.${index}`,
                                        title: 'dockInactivity',
                                        resourceId: info.resource.id,
                                        start: moment(range.from),
                                        end: moment(range.to),
                                        editable: false,
                                        rendering: 'background',
                                        overlap: false,
                                        className: 'inactivities'
                                    })))
                                }
                                if (infoSite[0] && infoSite[0].inactivity) {
                                    inactivity = inactivity.concat(infoSite[0].inactivity.map((range, index) => ({
                                        id: `${infoSite[0].resource.id}.${index}`,
                                        title: 'siteInactivity',
                                        resourceId: info.resource.id,//this adds the inactivity to the dock resource. (shouldn't be infoSite[0])
                                        start: moment(range.from),
                                        end: moment(range.to),
                                        editable: false,
                                        rendering: 'background',
                                        overlap: false,
                                        className: 'inactivities'
                                    })))
                                }
                                this.reloadInactivity = true
                            })
                        })
                    }))

                if (!this.avoidCallForDates) {
                    promises.push(CALENDAR_MANAGER.loadActivitiesDates(this.startOfTodayInTimeZone.valueOf(), this.selectedRequestors.map(r => r.id), this.filterBySite ? CONTEXT.get('ds.site').code : null)
                        .then(dates => {
                            // Create structure of data that will be inserted in the partial template
                            this.templateData = {}
                            dates.forEach(date => {
                                if( date >= this.startOfTodayInTimeZone.valueOf()) {
                                this.templateData[date] = {}
                                }
                            })
                        }))
                } else {
                    this.avoidCallForDates = false
                }

                Promise.all(promises)
                    .then(() => {
                        if (currentToken === this.token) {
                            // Find common start and end of working day on resources
                            let inactivitiesEnds = inactivity.filter(i => i.start.isSame(moment(this.startOfTodayInTimeZone))
                            && i.title === 'siteInactivity').map(i => i.end)
                            if (inactivitiesEnds && inactivitiesEnds.length > 0) {
                                this.startWorkingDay = moment.min(inactivitiesEnds)
                            } else {
                                this.startWorkingDay = moment(from).startOf('day')
                            }

                            //it is required to show calendar starting from midnight(default)
                            //https://fullcalendar.io/docs/v3/minTime
                            //this.scheduler.fullCalendar('option', 'minTime', this.startWorkingDay.format('HH:mm'))

                            let inactivitiesStarts = inactivity.filter(i => i.end.isSame(moment(from).add(1, 'day').startOf('day'))
                                && i.title === 'siteInactivity').map(i => i.start)
                            if (inactivitiesStarts && inactivitiesStarts.length > 0) {
                                this.endWorkingDay = moment.max(inactivitiesStarts)
                            } else {
                                this.endWorkingDay = moment(to).endOf('day')
                            }

                            //it is required to show calendar ending from 24:00 (default)
                            //https://fullcalendar.io/docs/v3/maxTime
                            //this.scheduler.fullCalendar('option', 'maxTime', this.endWorkingDay.format('kk:mm'))

                            // Set scrollTime
                            let scrollTime = ((parseInt(this.startWorkingDay.format('HH')) + (parseInt(this.startWorkingDay.format('mm')) / 60)) + (parseInt(this.endWorkingDay.format('HH')) + (parseInt(this.endWorkingDay.format('mm') / 60)))) / 2
                            let scrollTimeInt = Math.floor(scrollTime)
                            let scrollTimeDecimal = (scrollTime - scrollTimeInt) * 60
                            this.scheduler.fullCalendar('option', 'scrollTime', `${scrollTimeInt}:${scrollTimeDecimal}`)

                            // Fill missing data of requestor's placed activities (timeSlots and resources)
                            requestorPlacedActivities.forEach(a => {
                                let f = allPlacedActivities.find(x => x.id === a.id)
                                // If f is undefined than a is an activity that starts and ends in another day
                                if (f) {
                                    a.start = f.start
                                    a.end = f.end
                                    a.resourceId = f.resourceId
                                }
                            })
                            // requestorPlacedActivities = requestorPlacedActivities.filter(a => a.start.valueOf() > from && a.end.valueOf() < to)

                            if (requestorUnplacedActivities.length > 0) {
                                // Get list of distinct appointments on calendar date
                                let appointmentReferences = requestorUnplacedActivities.map(a => a.appointmentReference)
                                let distinctApp = appointmentReferences.filter((app, index, self) => index === self.findIndex((a) => (a.id === app.id)))

                                // Fill data structure with requestor's unplaced activities
                                distinctApp.forEach(app => {
                                    let myRegexpAppLink = /(<a.+>)(.*)(<\/a>)/g
                                    let matchAppLink = myRegexpAppLink.exec(app.appointmentLink)
                                    let appointmentLinkEmpty = matchAppLink[1].concat(matchAppLink[3])
                                    var singleAppointmentTemplate = this.templateData[this.scheduler.fullCalendar('getDate').valueOf()];
                                    if (singleAppointmentTemplate && app.status !== 'COMPLETED') {
                                        singleAppointmentTemplate[app.code] = ({
                                            appointmentCode: app.code,
                                            appointmentLinkEmpty: appointmentLinkEmpty,
                                            activities: requestorUnplacedActivities.filter(reqUnpAct => reqUnpAct.appointmentReference.id === app.id)
                                        })
                                    }
                                })

                                // Keep only needed info in appointment reference
                                /** NB: in requestor-unplaced-activities-partial-template this object is inserted
                                 * in a string sourranded by single quote.
                                 * So if one of the strings inside this object contains a single quote the complete
                                 * string ends and the JSON.parse() function gives error.
                                 */
                                requestorUnplacedActivities.forEach(a => {
                                    let appRefSlim = {}
                                    appRefSlim.id = a.appointmentReference.id
                                    appRefSlim.requestorId = a.appointmentReference.requestorId
                                    appRefSlim.requestorCode = a.appointmentReference.requestorCode
                                    appRefSlim.site = a.appointmentReference.site
                                    appRefSlim.referenceRequest = a.appointmentReference.referenceRequest
                                    a.appointmentReference = appRefSlim
                                })
                            }

                            // TOEVALUATE, TOFIX: the unplaced activities can be filtered in order to show only those activities that have bounds inside the working hours.
                            // If the filter is applied then the list of the date has to be modified in order to delete the date for which there isn't any activity.
                            // requestorUnplacedActivities = requestorUnplacedActivities.filter(a => a.constraint.start < endWorkingDay.valueOf() && a.constraint.end > startWorkingDay.valueOf())

                            // if (requestorUnplacedActivities.length > 0) {
                            //   // Get list of distinct appointments on calendar date
                            //   let appointmentReferences = requestorUnplacedActivities.map(a => a.appointmentReference)
                            //   let distinctAppIds = Array.from(new Set(appointmentReferences.map(r => r.id)))
                            //   let distinctApp = appointmentReferences.filter(r => distinctAppIds.includes(r.id))

                            //   // Fill data structure with requestor unplaced activities
                            //   distinctApp.forEach(a => {
                            //     let reqUnpAct = requestorUnplacedActivities.filter(reqUnpAct => reqUnpAct.appointmentReference.id === a.id)
                            //     if (reqUnpAct && reqUnpAct.length > 0) {
                            //       templateData[this.scheduler.fullCalendar('getDate').valueOf()][a.code] = reqUnpAct
                            //     }
                            //   })
                            // } else {
                            //   // If there aren't any activities delete object key
                            //   delete templateData[this.scheduler.fullCalendar('getDate').valueOf()]
                            // }

                            $('.external-events').html(this.accordionPartialTemplate({unplacedActivitiesGroupedByDateAndAppointment: this.templateData}))

                            // Disable drag from link
                            $('.external-events .sce-picker-link').mousedown(() => {
                                return false
                            })

                            // Add click event to accordion's elements
                            $('.date-accordion').on('toggled', (event, accordion) => {
                                let eventDate = moment(parseInt(accordion[0].dataset.accordionDate))
                                if (!eventDate.isSame(this.scheduler.fullCalendar('getDate'))) {
                                    this.scheduler.fullCalendar('gotoDate', eventDate)
                                }
                            })
                            $('.appointment-accordion').on('toggled', (event, accordion) => {
                                this.activeAppointmentAccordion = accordion[0].dataset.accordionAppointment
                            })

                            this.activitiesOnCalendar = allPlacedActivities.filter(activity => !requestorPlacedActivities.map(activity => activity.id).includes(activity.id)).concat(requestorPlacedActivities).concat(inactivity)

                            callback(this.activitiesOnCalendar)
                        }
                    })
                    .then(() => {

                        this._setContainerHeight();

                        // TOFIX
                        // let stringDate = this.scheduler.fullCalendar('getDate').startOf('day').valueOf()
                        // if ($('.content.active').length > 0) {
                        //   if ($('.content.active')[0].id.substring(1, $('.content.active')[0].id.length - 1) === stringDate) {
                        //     $(`#r${stringDate}`).parent().addClass('active')
                        //     $(`#r${stringDate}`).addClass('active')
                        //   }
                        // }
                        // Open date accordion
                        let stringDate = this.scheduler.fullCalendar('getDate').startOf('day').valueOf()
                        $(`#r${stringDate}`).parent().addClass('active')
                        $(`#r${stringDate}`).addClass('active')
                        // Open appointment accordion
                        if (!this.activeAppointmentAccordion) {
                            $(`.appointment-accordion .appointment-accordion-item:first-child`).addClass('active')
                            $(`.appointment-accordion .appointment-accordion-item:first-child > div.content`).addClass('active')
                        } else {
                            $(`#r${this.activeAppointmentAccordion}`).parent().addClass('active')
                            $(`#r${this.activeAppointmentAccordion}`).addClass('active')
                        }
                        this._changeDatePickerDisplayMode(false)
                    })
            }

            /**
             * Triggered while an event is being rendered.
             * See {@link https://fullcalendar.io/docs/event_rendering/eventRender/}
             * @param  {[type]} event   [description]
             * @param  {[type]} element [description]
             * @param  {[type]} view    [description]
             * @return {[type]}         [description]
             */
            this.fullCalendar.eventRender = (event, element, view) => {
                if (event.rendering !== 'background') {
                    let title = element.find('.fc-title')
                    title.html(title.text())
                }

                return (this.selectedRequestors.length === 0 ||
                    !event.appointmentReference ||
                    !event.appointmentReference.requestorId ||
                    this.selectedRequestors.map(sr => parseInt(sr.id)).includes(event.appointmentReference.requestorId))
            }

            /**
             * Triggered after an event has been placed on the calendar in its final position.
             * See {@link https://fullcalendar.io/docs/event_rendering/eventAfterRender/}
             * @param  {[type]} event   [description]
             * @param  {[type]} element [description]
             * @param  {[type]} view    [description]
             * @return {[type]}         [description]
             */
            this.fullCalendar.eventAfterRender = (event, element, view) => {
                // $(element).draggable({ handle: '.fc-time' })
                // $(element).draggable('option', 'handle', '.draggable-indicator')
            }

            /**
             * Triggered after all events have finished rendering.
             * @param  {[type]} view [description]
             * @return {[type]}      [description]
             */
            this.fullCalendar.eventAfterAllRender = (view) => {
                //translate appointment status of activities
                Y.SCE.Intl.intlDomNodes($('div.appointment-status-indicator'))

                // Enable/Disable tooltip for activities that need to be rescheduled
                $('.to-reschedule-icon').hover(
                    function () {
                        $('.tooltips').css('display', 'block')
                        $('.tooltips').css('visibility', 'visible')
                        $('.tooltips').css('position', 'absolute')
                        $('.tooltips').css('left', $(this).offset().left + $(this).width())
                        $('.tooltips').css('top', $(this).offset().top + $(this).height())
                        $('.tooltips').css('zIndex', 999)
                    },
                    function () {
                        $('.tooltips').css('display', 'none')
                        $('.tooltips').css('visibility', 'hidden')
                    }
                )
            }

            /**
             * Triggered when event dragging begins.
             * See {@link https://fullcalendar.io/docs/event_ui/eventDragStart/}
             * @param  {[type]} event   [description]
             * @param  {[type]} jsEvent [description]
             * @param  {[type]} ui      [description]
             * @param  {[type]} view    [description]
             * @return {[type]}         [description]
             */
            this.fullCalendar.eventDragStart = (event, jsEvent, ui, view) => {
                _disableUserInteraction('dragging-event')
                /******************************************************/
                // TODO:
                // let realDragObject = $(jsEvent.target).parents('.fc-event.fc-draggable')

                // // Hide draggable bar
                // $(jsEvent.target).detach()
                // $(jsEvent.target).css('display', 'none')

                // this.draggedNodeParent = realDragObject.parent()
                // realDragObject.detach()
                // $('body').prepend($(jsEvent.target))
                // realDragObject.css('position', 'absolute')
                // realDragObject.css('left', jsEvent.clientX)
                // realDragObject.css('top', jsEvent.clientY)
                // realDragObject.css('zIndex', 999)

                /******************************************************/

                let now = moment().valueOf()
                if (event.lowerBoundNow === true && now < event.constraint.end) {
                    event.constraint.start = Math.max(event.constraint.start, now)
                }

                // Fix activity constraints respect to working hour
                if (this.startWorkingDay.isSame(event.constraint.start, 'day')) {
                    // For activities that can be scheduled on more days the check on start has to be done only for the first day
                    event.constraint.start = Math.max(event.constraint.start, this.startWorkingDay.valueOf())
                }
                event.constraint.end = Math.min(event.constraint.end, this.endWorkingDay.valueOf())

                // Highlights time slots in which the event can't be dropped due to time constraints
                if (this.endWorkingDay.valueOf() != event.constraint.end || this.startWorkingDay.valueOf() < event.constraint.start) {
                    TIME_SLOT_COVER_SERVICE.toggleTimeSlotCover(event.constraint.start, event.constraint.end, 'cover-bounds', true,this.currentDay, this.slotDurationMinutes)
                }

                // Get start and end of other activities of the same appointment
                this.activitiesOnCalendar.filter(a => a.appointmentReference && a.appointmentReference.id === event.appointmentReference.id && a.id !== event.id).forEach(a => {
                    let res = this.resources.find(r => r.id === a.resourceId);
                    if (res) {
                        this.overlappingLimit = res.overlapLimit ? res.overlapLimit : 0;
                        // Highlights time slots in which the event can't be dropped due to non-ubiquity of user
                        TIME_SLOT_COVER_SERVICE.toggleTimeSlotCover(a.start.valueOf() + this.overlappingLimit, a.end.valueOf() - this.overlappingLimit, 'cover-sibling-activities', false,this.currentDay, this.slotDurationMinutes)
                    }
                })

                // Highlights time slots in which the event can't be dropped due to requestor's delivery window
                let deliveryWindow = this.deliveryWindows.find(dw => dw.requestorCode === event.appointmentReference.requestorCode)
                if (deliveryWindow && deliveryWindow.inactivity) {
                    deliveryWindow.inactivity.forEach(i => {
                        TIME_SLOT_COVER_SERVICE.toggleTimeSlotCover(i.from, i.to, 'cover-delivery-window', false,this.currentDay,this.slotDurationMinutes)
                    })
                }

                // Set constraint btw activites and resources
                if (!event.constraint.resourceIds) {
                    event.constraint.resourceIds = _checkCompatibilityBtwActivityAndResources(event, this.resources)
                }

                // Highlight resources in which the event can't be dropped
                this.resources.map(r => r.id).filter(id => event.constraint.resourceIds.indexOf(id) === -1).forEach(r => $(`.fc-bg .fc-day.fc-widget-content[data-resource-id="${r}"]`).addClass('cover-resources'))

                // Change style of activities that belong to other appointments
                $(`.fc-event[data-appointment-id!="${event.appointmentReference.id}"]`).addClass('otherAppointmentActivities')
            }

            this.fullCalendar.dateClick = (arg)=> {
                console.log(arg.date.toUTCString()); // use *UTC* methods on the native Date Object
                // will output something like 'Sat, 01 Sep 2018 00:00:00 GMT'
            },

            /**
             * Triggered when event dragging stops (From calendar to outside)
             * See {@link https://fullcalendar.io/docs/event_ui/eventDragStop/}
             * @param  {[type]} event   [description]
             * @param  {[type]} jsEvent [description]
             * @param  {[type]} ui      [description]
             * @param  {[type]} view    [description]
             * @return {[type]}         [description]
             */
            this.fullCalendar.eventDragStop = (event, jsEvent, ui, view) => {
                /******************************************************/
                // TODO:
                // // Re-append detached element
                // let realDragObject = $(jsEvent.target).parent().parent().parent()
                // realDragObject.css('position', 'relative')
                // realDragObject.css('top', 'initial')
                // realDragObject.css('left', 'initial')
                // realDragObject.css('zIndex', 'initial')
                // this.draggedNodeParent.prepend(realDragObject)

                // Show draggable bar
                // $(jsEvent.target).prepend()
                // $(jsEvent.target).css('display', 'initial')

                // ************************************************************* //

                let now = moment();
                if (event.lowerBoundNow === true && now < event.constraint.end) {
                    event.constraint.start = Math.max(event.constraint.start, now)
                }

                // Fix activity constraints respect to working hour
                if (this.startWorkingDay.isSame(event.constraint.start, 'day')) {
                    // For activities that can be scheduled on more days the check on start has to be done only for the first day
                    event.constraint.start = Math.max(event.constraint.start, this.startWorkingDay.valueOf())
                }
                event.constraint.end = Math.min(event.constraint.end, this.endWorkingDay.valueOf())

                // Delete cover of lower and upper bound
                if (this.endWorkingDay.valueOf() != event.constraint.end || this.startWorkingDay.valueOf() < event.constraint.start) {
                    TIME_SLOT_COVER_SERVICE.toggleTimeSlotCover(event.constraint.start, event.constraint.end, 'cover-bounds', true,this.currentDay,this.slotDurationMinutes)
                }

                // Get start and end of other activities of the same appointment
                this.activitiesOnCalendar.filter(a => a.appointmentReference && a.appointmentReference.id === event.appointmentReference.id && a.id !== event.id).forEach(a => {
                    let res = this.resources.find(r => r.id === a.resourceId);
                    if (res) {
                        this.overlappingLimit = res.overlapLimit ? res.overlapLimit : 0;
                        // Delete covers of "non-ubiquity of user"
                        TIME_SLOT_COVER_SERVICE.toggleTimeSlotCover(a.start.valueOf() + this.overlappingLimit, a.end.valueOf() - this.overlappingLimit, 'cover-sibling-activities', false,this.currentDay,this.slotDurationMinutes)
                    }
                })

                // Delete cover of requestor's delivery window
                let deliveryWindow = this.deliveryWindows.find(dw => dw.requestorCode === event.appointmentReference.requestorCode)
                if (deliveryWindow && deliveryWindow.inactivity) {
                    deliveryWindow.inactivity.forEach(i => {
                        TIME_SLOT_COVER_SERVICE.toggleTimeSlotCover(i.from, i.to, 'cover-delivery-window', false,this.currentDay,this.slotDurationMinutes)
                    })
                }

                // Remove covers for time intervals
                this.resources.map(r => r.id).filter(id => event.constraint.resourceIds.indexOf(id) === -1).forEach(r => $(`.fc-bg .fc-day.fc-widget-content[data-resource-id="${r}"]`).removeClass('cover-resources'))

                // Restore style of activities that belong to other appointments
                $(`.fc-event[data-appointment-id!="${event.appointmentReference.id}"]`).removeClass('otherAppointmentActivities')

                if (_isEventOverExternalEventsDiv(jsEvent.clientX, jsEvent.clientY)) {
                    _disableUserInteraction('running-action')
                    CALENDAR_MANAGER.unbindTimeSlotToActivity(event.id, parseInt(event.resourceId), now.valueOf(), false)
                        .then(() => {
                            this.scheduler.fullCalendar('removeEvents', event.id)
                        })
                        // TOFIX: This catch+then implement "finally" clause
                        .catch(() => {
                        })
                        .then(() => {
                            _enableUserInteraction('running-action')
                            this.reloadInactivity = false
                            this.scheduler.fullCalendar('refetchEvents')
                        })
                }

                _enableUserInteraction('dragging-event')
            }

            /**
             * Triggered when an event has moved to a different day/time
             * See {@link https://fullcalendar.io/docs/event_ui/eventDrop/}
             * @param  {[type]} event      [description]
             * @param  {[type]} delta      [description]
             * @param  {[type]} revertFunc [description]
             * @param  {[type]} jsEvent    [description]
             * @param  {[type]} ui         [description]
             * @param  {[type]} view       [description]
             * @return {[type]}            [description]
             */
            this.fullCalendar.eventDrop = (event, delta, revertFunc, jsEvent, ui, view) => {
                // Calculate the delta-minutes from the start date of the dragged time slot.
                var deltaDataObj = delta._data
                var deltaMinutes = deltaDataObj.minutes + deltaDataObj.hours * 60 + deltaDataObj.days * 3600

                var oldStartDate = new Date(event.start.valueOf() - (deltaMinutes * 60000))

                _disableUserInteraction('running-action')

                if (!_isEventOverExternalEventsDiv(jsEvent.clientX, jsEvent.clientY)) {
                    CALENDAR_MANAGER.changeTimeSlotToActivity(event.id, event.start, event.end, oldStartDate, parseInt(event.resourceId), now.valueOf(), false)
                        .catch(() => {
                            // Take events to original position
                            revertFunc()
                        })
                        // Implement "finally" clause
                        .then(() => {
                            _enableUserInteraction('running-action')
                            this.reloadInactivity = false
                            this.scheduler.fullCalendar('refetchEvents')
                        })
                }
            }

            /**
             * Triggered when an event has dragged from outside into the calendar
             * After _onDragend
             * See {@link https://fullcalendar.io/docs/dropping/drop/}
             * @param  {[type]} date       [description]
             * @param  {[type]} jsEvent    [description]
             * @param  {[type]} ui         [description]
             * @param  {[type]} resourceId [description]
             * @return {[type]}            [description]
             */
            this.fullCalendar.drop = (date, jsEvent, ui, resourceId) => {
            }

            /**
             * Triggered when an event has dragged from outside into the calendar
             * After _onDragend
             * After drop
             * See {@link https://fullcalendar.io/docs/dropping/eventReceive/}
             * @param  {[type]} event [description]
             * @return {[type]}       [description]
             */
            this.fullCalendar.eventReceive = event => {
                let now = moment();

                _disableUserInteraction('running-action')
                CALENDAR_MANAGER.bindTimeSlotToActivity(event.id, event.start.valueOf(), event.end.valueOf(), parseInt(event.resourceId), now.valueOf(), false)
                    // TOFIX: This catch+then implement "finally" clause
                    .catch(() => {
                    })
                    .then(() => {
                        _enableUserInteraction('running-action')
                        this.reloadInactivity = false
                        this.scheduler.fullCalendar('refetchEvents')
                    })
            }

            /**
             * Exact programmatic control over where an event can be dropped
             * See {@link https://fullcalendar.io/docs/event_ui/eventAllow/}
             * @param  {[type]} dropLocation [description]
             * @param  {[type]} draggedEvent [description]
             * @return {[type]}              [description]
             */
            this.fullCalendar.eventAllow = (dropLocation, draggedEvent) => {
                // Check on delivery window
                let deliveryWindow = this.deliveryWindows.find(dw => dw.requestorCode === draggedEvent.appointmentReference.requestorCode)
                if (deliveryWindow && deliveryWindow.inactivity) {
                    let overlappingDeliveryWindow = false
                    deliveryWindow.inactivity.forEach(i => {
                        overlappingDeliveryWindow |= _areEventsOverlapping(dropLocation, ({
                            'start': i.from,
                            'end': i.to
                        }))
                    })
                    if (overlappingDeliveryWindow) {
                        return false
                    }
                }

                // Check on capacity
                let activitiesInDropLocation = this.activitiesOnCalendar.filter(a => a.resourceId === parseInt(dropLocation.resourceId) &&
                    a.rendering !== 'background' &&
                    a.id !== draggedEvent.id &&
                    _areEventsOverlapping(a, dropLocation)).sort((a, b) => a.start - b.start)

                let occupiedCapacity = 0

                if (activitiesInDropLocation.length === 0) {
                    occupiedCapacity = 0
                } else {
                    let overlaps = 1 + parseInt(activitiesInDropLocation.map(e => _getOverlappingEvents(e, activitiesInDropLocation)))
                    occupiedCapacity = Math.max(overlaps)
                }

                let resource = this.resources.find(resource => resource.id === parseInt(dropLocation.resourceId))
                console.log("current event site: " + draggedEvent.appointmentReference.site.code + "and target resource site: " + resource.site);
                return resource.capacity > occupiedCapacity && resource.site === draggedEvent.appointmentReference.site.code;
            }
        },


        /**
         * Converts a UTC time to the same wall clock time in the target timezone.
         * For example:
         * - 2024-03-24 00:00:00 UTC -> 2024-03-24 00:00:00 GMT-3 (returns 2024-03-24T03:00:00Z)
         * - 2024-03-24 14:30:00 UTC -> 2024-03-24 14:30:00 GMT-3 (returns 2024-03-24T17:30:00Z)
         *
         * @param {Date | string | number} utcTime - The UTC time to convert
         * @param {string} targetTimeZone - The IANA timezone string (e.g., 'America/Sao_Paulo')
         * @returns {Date} - A Date object representing the same wall clock time in the target timezone (stored in UTC)
         */
        preserveTimeInTimezone(utcTime, targetTimeZone) {
        // Ensure we have a Date object
        const utcDate = new Date(utcTime);

        // Get the time components from the original UTC date
        const day = utcDate.getUTCDate();
        const month = utcDate.getUTCMonth();
        const year = utcDate.getUTCFullYear();
        const hours = utcDate.getUTCHours();
        const minutes = utcDate.getUTCMinutes();
        const seconds = utcDate.getUTCSeconds();
        const ms = utcDate.getUTCMilliseconds();

        // Create a formatter to get the timezone offset
        const formatter = new Intl.DateTimeFormat('en-US', {
            timeZone: targetTimeZone,
            timeZoneName: 'longOffset',
            year: 'numeric',
            month: 'numeric',
            day: 'numeric',
            hour: 'numeric',
            minute: 'numeric',
            second: 'numeric',
            hour12: false
        });

        // Format the UTC date to get the target timezone date and offset
        const parts = formatter.formatToParts(utcDate);

        // Get the timezone offset
        const timeZonePart = parts.find(part => part.type === 'timeZoneName');
        const offsetMatch = timeZonePart.value.match(/GMT([+-])(\d{2}):(\d{2})/);

        if (!offsetMatch) {
            throw new Error('Unable to parse timezone offset');
        }

        const [, sign, offsetHours, offsetMinutes] = offsetMatch;
        const offsetInMilliseconds = (
            parseInt(offsetHours, 10) * 60 * 60 * 1000 +
            parseInt(offsetMinutes, 10) * 60 * 1000
        ) * (sign === '+' ? -1 : 1);

        // Return the date adjusted by the offset
        return new Date(utcDate.getTime() + offsetInMilliseconds);
    },

        /**
         * Calculates the UTC timestamp for the start of the day (00:00) in a specified timezone.
         *
         * @param {string} timeZone - The IANA timezone string (e.g., 'Europe/Rome').
         * @param {Date | string} [date=new Date()] - The date for which to calculate the start of the day. Defaults to the current date.
         * @returns {Date} - A JavaScript Date object representing 00:00 in UTC for the specified timezone.
         */
        getStartOfDayUTC(timeZone, date = new Date()) {
            // Initialize Intl.DateTimeFormat to extract year, month, day in the target timezone
            const formatter = new Intl.DateTimeFormat('en-US', {
                timeZone,
                year: 'numeric',
                month: 'numeric',
                day: 'numeric',
            });

            const parts = formatter.formatToParts(date);

            const year = parseInt(parts.find(p => p.type === 'year').value, 10);
            const month = parseInt(parts.find(p => p.type === 'month').value, 10) - 1; // Months are 0-based in Date.UTC
            const day = parseInt(parts.find(p => p.type === 'day').value, 10);

            // Create a UTC Date object at 00:00 for the extracted date
            const utcDate = new Date(Date.UTC(year, month, day, 0, 0, 0));

            // Use Intl.DateTimeFormat to get the time components in the target timezone
            const timeFormatter = new Intl.DateTimeFormat('en-US', {
                timeZone,
                hour: '2-digit',
                minute: '2-digit',
                second: '2-digit',
                hour12: false,
            });

            const timeParts = timeFormatter.formatToParts(utcDate);

            const hour = parseInt(timeParts.find(p => p.type === 'hour').value, 10);
            const minute = parseInt(timeParts.find(p => p.type === 'minute').value, 10);
            const second = parseInt(timeParts.find(p => p.type === 'second').value, 10);

            // Create a Date object representing the extracted time in UTC
            const targetTimeDate = new Date(Date.UTC(year, month, day, hour, minute, second));

            // Calculate the timezone offset in milliseconds
            const offset = targetTimeDate.getTime() - utcDate.getTime();

            // Adjust the UTC date by the offset to get the correct UTC timestamp
            const startOfDayUTC = new Date(utcDate.getTime() - offset);

            return startOfDayUTC;
        },

        /**
         * Converts a UTC time to the same wall clock time in a different timezone.
         * For example, 2024-03-24 00:00:00 UTC -> 2024-03-24 00:00:00 America/New_York
         *
         * @param {Date | string | number} utcTime - The UTC time to convert
         * @param {string} targetTimeZone - The IANA timezone string (e.g., 'America/New_York')
         * @returns {Date} - A Date object representing the same wall clock time in the target timezone (stored in UTC)
         *
         * @example
         * // If it's 2024-03-24 00:00:00 UTC, this will return a Date representing
         * // 2024-03-24 00:00:00 in New York (which is 2024-03-24 04:00:00 UTC)
         */
        convertSameTimeToTimezone(utcTime, targetTimeZone) {
        // Ensure we have a Date object
        const utcDate = new Date(utcTime);

        // Create formatter for the target timezone to get the offset
        const formatter = new Intl.DateTimeFormat('en-US', {
            timeZone: targetTimeZone,
            year: 'numeric',
            month: 'numeric',
            day: 'numeric',
            hour: 'numeric',
            minute: 'numeric',
            second: 'numeric',
            hour12: false,
            timeZoneName: 'longOffset'
        });

        // Format the date to get timezone parts
        const parts = formatter.formatToParts(utcDate);

        // Find the timezone offset part
        const timeZonePart = parts.find(part => part.type === 'timeZoneName');
        const offsetString = timeZonePart.value;

        // Parse the offset (format will be like GMT+01:00 or GMT-05:00)
        const offsetMatch = offsetString.match(/GMT([+-])(\d{2}):(\d{2})/);
        if (!offsetMatch) {
            throw new Error('Unable to parse timezone offset');
        }

        const [, sign, hours, minutes] = offsetMatch;
        const offsetInMilliseconds = (
            parseInt(hours, 10) * 60 * 60 * 1000 +
            parseInt(minutes, 10) * 60 * 1000
        ) * (sign === '+' ? 1 : -1);

        // Return a new Date adjusted by the offset
        return new Date(utcDate.getTime() + offsetInMilliseconds);
    },
        /**
         * Converts a UTC time to local time in a specified timezone.
         *
         * @param {Date | string | number} utcTime - The UTC time to convert. Can be a Date object, ISO string, or timestamp.
         * @param {string} targetTimeZone - The IANA timezone string (e.g., 'America/New_York', 'Europe/London').
         * @returns {Date} - A Date object representing the local time in the specified timezone (but stored in UTC).
         *
         * @example
         * // Convert current UTC time to New York time
         * const nyTime = convertUTCToTimezone(new Date(), 'America/New_York');
         *
         * // Convert specific UTC time to Rome time
         * const romeTime = convertUTCToTimezone('2024-03-15T14:30:00Z', 'Europe/Rome');
         */
        convertUTCToTimezone(utcTime, targetTimeZone) {
        // Ensure we have a Date object to work with
        const utcDate = new Date(utcTime);

        // Create formatters for both UTC and target timezone
        const utcFormatter = new Intl.DateTimeFormat('en-US', {
            timeZone: 'UTC',
            year: 'numeric',
            month: 'numeric',
            day: 'numeric',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
            hour12: false
        });

        const targetFormatter = new Intl.DateTimeFormat('en-US', {
            timeZone: targetTimeZone,
            year: 'numeric',
            month: 'numeric',
            day: 'numeric',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
            hour12: false
        });

        // Get the parts for both UTC and target timezone
        const utcParts = utcFormatter.formatToParts(utcDate);
        const targetParts = targetFormatter.formatToParts(utcDate);

        // Extract UTC components
        const utcComponents = {
            year: parseInt(utcParts.find(p => p.type === 'year').value, 10),
            month: parseInt(utcParts.find(p => p.type === 'month').value, 10) - 1,
            day: parseInt(utcParts.find(p => p.type === 'day').value, 10),
            hour: parseInt(utcParts.find(p => p.type === 'hour').value, 10),
            minute: parseInt(utcParts.find(p => p.type === 'minute').value, 10),
            second: parseInt(utcParts.find(p => p.type === 'second').value, 10)
        };

        // Extract target timezone components
        const targetComponents = {
            year: parseInt(targetParts.find(p => p.type === 'year').value, 10),
            month: parseInt(targetParts.find(p => p.type === 'month').value, 10) - 1,
            day: parseInt(targetParts.find(p => p.type === 'day').value, 10),
            hour: parseInt(targetParts.find(p => p.type === 'hour').value, 10),
            minute: parseInt(targetParts.find(p => p.type === 'minute').value, 10),
            second: parseInt(targetParts.find(p => p.type === 'second').value, 10)
        };

        // Calculate the time difference
        const utcTimestamp = Date.UTC(
            utcComponents.year,
            utcComponents.month,
            utcComponents.day,
            utcComponents.hour,
            utcComponents.minute,
            utcComponents.second
        );

        const targetTimestamp = Date.UTC(
            targetComponents.year,
            targetComponents.month,
            targetComponents.day,
            targetComponents.hour,
            targetComponents.minute,
            targetComponents.second
        );

        const timeDiff = targetTimestamp - utcTimestamp;

        // Apply the difference to get the correct local time
        return new Date(utcDate.getTime() + timeDiff);
    },
        _hideIfCantPlanToday(requestorUnplacedActivities,requestorPlacedActivities, from) {
            var requestorPlacedActivitiesFiltered = requestorPlacedActivities.filter(a => {
                var plannedTime = a.plannedTime.dayOfYear();
                var dayOfFrom = moment(from).dayOfYear();
                if (plannedTime) {
                    return plannedTime != dayOfFrom;
                }
                return false;
            });
            return requestorUnplacedActivities.filter(activity => {
                return requestorPlacedActivitiesFiltered.filter(a => a.appointmentReference && a.appointmentReference.id === activity.appointmentReference.id).length === 0
            });
        },

        render() {
            this.avoidCallForDates = true

            // Render tooltip
            $('body').prepend(TOOLTIP_ELEMENT)
            Y.SCE.Intl.intlDomNodes($('div.tooltips'))

            // Add lateral bar to container
            let container = this.get('container')
            container.setHTML(this.template())

            this.get('container').one('.date-filter').append(this.datePicker.get('container'));
            this.datePicker.render();
            Y.SCE.Intl.intlDomNodes(this.datePicker.get('container'))
            this.loadRequestorsPromise.then(() => {
                // Render requestors filter
                this.get('container').one('.requestor-filter').append(this.requestorPickerSelect.get('container'))
                this.requestorPickerSelect.render()
                Y.SCE.Intl.intlDomNodes(this.requestorPickerSelect.get('container'))
            })

            this.loadDocksPromise.then(() => {
                // Render docks filter
                this.get('container').one('.dock-filter').append(this.dockPickerSelect.render().get('container'))
                this.dockPickerSelect.render()
                Y.SCE.Intl.intlDomNodes(this.dockPickerSelect.get('container'))
            })

            Y.SCE.Intl.intlDomNodes(container)

            // Set interactjs property for drag&drop
            let context = container.one('.external-events').getDOMNode()
            this._externalsInteractable = interact('.external-events .fc-event', {context})
                .allowFrom('.draggable-indicator')
                .draggable({})
                .on('dragstart', this._onDragstart.bind(this))
                .on('dragmove', this._onDragmove.bind(this))
                .on('dragend', this._onDragend.bind(this))

            // Render scheduler
            CALENDAR_MANAGER.loadResources({
                name: "Site", businessKey: CONTEXT.get('ds.site').code
            }).then(infoSite => {
                if (infoSite[0]) {
                    this.siteTimeZone = infoSite[0].calendar.timeZone;

                    this.startOfTodayInUTC = moment(this.getStartOfDayUTC(this.siteTimeZone)).startOf('day')
                    this.startOfTodayInTimeZone = moment(this.getStartOfDayUTC(this.siteTimeZone))
                    // Disable navigation in the past
                    this.fullCalendar.validRange = {
                        start:  this.startOfTodayInUTC
                    }
                    this.fullCalendar.defaultDate = this.startOfTodayInUTC
                    //set the date of the picker to the initial date of the calendar
                    this.datePicker.setData(this.startOfTodayInUTC)
                    this.fullCalendar.timezone=this.siteTimeZone

                    return this.startOfTodayInTimeZone
                }
            }).then( startOfTodayInTimeZone=>
                // Retrieve all the dates in which there are unplaced activities
                // TOEVALUATE: can this.selectedRequestors be != [] ???
                CALENDAR_MANAGER.loadActivitiesDates(startOfTodayInTimeZone.valueOf(), this.selectedRequestors.map(r => r.id), this.filterBySite ? CONTEXT.get('ds.site').code : null)
                    .then(dates => {
                        // Create structure of data that will be inserted in the partial template
                        this.templateData = {}
                        dates.forEach(date => {
                            if( date >= startOfTodayInTimeZone.valueOf()) {
                                this.templateData[date] = {}
                            }
                        })
                        this.scheduler = new SCHEDULER({fullCalendar: this.fullCalendar})
                        this.scheduler.render()
                        container.one('.scheduler').append(this.scheduler.get('container'))
                        $(container.getDOMNode()).foundation('accordion', 'reflow')
                        this.scheduler.sync()
                        return dates && dates.length > 0 ? dates[0] : null
                    }))


            this._setContainerHeight();
        },


        /**
         *
         * Call this method to resize Height of the page.
         *
         * @method _setContainerMapWidth
         * @param  {boolean} [leaMenuOpening] true if lea menu is gonna to be opened, false/undefined otherwise
         * @private
         */
        _setContainerHeight: function () {
            try {
                let screenHeight = window.outerHeight;
                let scrap = 10;
                if (screenHeight >= 1030) {
                    scrap = 7;
                }

                //this is the top bar ion whicj is the site, ecc
                let topBar = $('.top-bar').outerHeight(true);
                let breadcrumb = $('#sce-app-breadcrumb').outerHeight(true);
                let lateralBarTitle = $('.lateral-bar-title').outerHeight(true);
                let date = $('.date-filter').outerHeight(true);
                let mainTabs = $('#sce-app-main-tabs').outerHeight(true);
                let footer = $('footer').outerHeight(true);

                let totalMenuHeight = mainTabs + date + lateralBarTitle + breadcrumb + topBar + footer;
                if(!isNaN(totalMenuHeight)) {
                    let percentageTotalMenuHeight = totalMenuHeight / screenHeight;
                    let percentageToSetHeight = (1 - percentageTotalMenuHeight) * 100;
                    let totalWithScrap = percentageToSetHeight - scrap;
                    let itemsToBePlacedHeight = totalWithScrap;
                    $('.external-events').css('height', itemsToBePlacedHeight + 'vh');
                }
            } catch (error) {
                ERROR.notify(error);
                console.error('_setMainWidthAndHeight => ' + error);
                throw new Error(error);
            }
        },

        destructor() {
            this._externalsInteractable.unset()
            this._eventHandles.forEach(handle => handle.detach())
        },

        sync() {
            if (this.scheduler) {
                this.scheduler.sync()
            }
        },

        _removeHTMLElementFromEvent(e) {
            let firstComma = e.indexOf(',');
            let secondComma = e.indexOf(',', firstComma + 1);
            return e.slice(0, firstComma) + e.slice(secondComma);
        },


        _onDragstart(ev) {
            _disableUserInteraction('dragging-event')

            // Detach element to drag over all other elements
            this.draggedNodeParent = $(ev.target).parent()
            $(ev.target).detach()
            $('body').prepend($(ev.target))
            $(ev.target).css('position', 'absolute')
            $(ev.target).css('left', ev.clientX)
            $(ev.target).css('top', ev.clientY)
            $(ev.target).css('zIndex', 999)

            let activity = JSON.parse(this._removeHTMLElementFromEvent(ev.target.dataset.event))

            let now = moment().valueOf()
            if (activity.lowerBoundNow === true && now < activity.constraint.end) {
                activity.constraint.start = Math.max(activity.constraint.start, now)
            }

            // Fix activity constraints respect to working hour
            if (this.startWorkingDay.isSame(activity.constraint.start, 'day')) {
                // For activities that can be scheduled on more days the check on start has to be done only for the first day
                activity.constraint.start = Math.max(activity.constraint.start, this.startWorkingDay.valueOf())
            }
            activity.constraint.end = Math.min(activity.constraint.end, this.endWorkingDay.valueOf())

            // Highlights time slots in which the event can't be dropped due to lower and upper bound
            if (this.endWorkingDay.valueOf() != activity.constraint.end || this.startWorkingDay.valueOf() < activity.constraint.start) {
                TIME_SLOT_COVER_SERVICE.toggleTimeSlotCover(activity.constraint.start, activity.constraint.end, 'cover-bounds', true, this.currentDay,this.slotDurationMinutes)
            }

            // Get start and end of other activities of the same appointment
            this.activitiesOnCalendar.filter(a => a.appointmentReference && a.appointmentReference.id === activity.appointmentReference.id).forEach(a => {
                let res = this.resources.find(r => r.id === a.resourceId);
                if (res) {
                    let overlapLimit = res.overlapLimit ? res.overlapLimit : 0;
                    // Highlights time slots in which the event can't be dropped due to "non-ubiquity of user"
                    TIME_SLOT_COVER_SERVICE.toggleTimeSlotCover(a.start.valueOf() + overlapLimit, a.end.valueOf() - overlapLimit, 'cover-sibling-activities', false,this.currentDay,this.slotDurationMinutes)
                }

            })

            // Highlights time slots in which the event can't be dropped due to requestor's delivery window
            let deliveryWindow = this.deliveryWindows.find(dw => dw.requestorCode === activity.appointmentReference.requestorCode)
            if (deliveryWindow && deliveryWindow.inactivity) {
                deliveryWindow.inactivity.forEach(i => {
                    TIME_SLOT_COVER_SERVICE.toggleTimeSlotCover(i.from, i.to, 'cover-delivery-window', false,this.currentDay,this.slotDurationMinutes)
                })
            }

            // Check if the reference of the resourceId is already present
            if (activity.resourceId) {
                activity.constraint.resourceIds = [parseInt(activity.resourceId)]
            } else {
                // Set resourceId to null instead of empty string otherwise none of the resources can be target of drop
                activity.resourceId = null
                // Set constraint btw activites and resources
                activity.constraint.resourceIds = _checkCompatibilityBtwActivityAndResources(activity, this.resources)
            }
            ev.target.dataset.event = JSON.stringify(activity)

            // Create covers for resources in which the event can't be dropped
            this.resources.map(r => r.id).filter(id => activity.constraint.resourceIds.indexOf(id) === -1).forEach(resourceId => {
                $(`.fc-bg .fc-day.fc-widget-content[data-resource-id="${resourceId}"]`).addClass('cover-resources')
            })

            // Change style of activities that belong to other appointments
            $(`.fc-event[data-appointment-id!="${activity.appointmentReference.id}"]`).addClass('otherAppointmentActivities')
        },

        _onDragmove(event) {
            let target = event.target
            // Keep the dragged position in the data-x/data-y attributes
            let x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx
            let y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy

            // Translate the element
            target.style.webkitTransform = target.style.transform = 'translate(' + x + 'px, ' + y + 'px)'

            // Update the posiion attributes
            target.setAttribute('data-x', x)
            target.setAttribute('data-y', y)
        },

        _onDragend(ev) {
            let target = ev.target
            target.removeAttribute('data-x')
            target.removeAttribute('data-y')
            target.style.webkitTransform = target.style.transform = null

            // Re-append detached element
            $(ev.target).css('position', 'relative')
            $(ev.target).css('top', 'initial')
            $(ev.target).css('left', 'initial')
            $(ev.target).css('zIndex', 'initial')
            this.draggedNodeParent.prepend($(ev.target))

            let activity = JSON.parse(this._removeHTMLElementFromEvent(ev.target.dataset.event))

            let now = moment().valueOf()
            if (activity.lowerBoundNow === true && now < activity.constraint.end) {
                activity.constraint.start = Math.max(activity.constraint.start, now)
            }

            // Fix activity constraints respect to working hour
            if (this.startWorkingDay.isSame(activity.constraint.start, 'day')) {
                // For activities that can be scheduled on more days the check on start has to be done only for the first day
                activity.constraint.start = Math.max(activity.constraint.start, this.startWorkingDay.valueOf())
            }
            activity.constraint.end = Math.min(activity.constraint.end, this.endWorkingDay.valueOf())

            // Delete cover of lower and upper bound
            if (this.endWorkingDay.valueOf() != activity.constraint.end || this.startWorkingDay.valueOf() < activity.constraint.start) {
                TIME_SLOT_COVER_SERVICE.toggleTimeSlotCover(activity.constraint.start, activity.constraint.end, 'cover-bounds', true,this.currentDay,this.slotDurationMinutes)
            }

            // Get start and end of other activities of the same appointment
            this.activitiesOnCalendar.filter(a => a.appointmentReference && a.appointmentReference.id === activity.appointmentReference.id).forEach(a => {
                let res = this.resources.find(r => r.id === a.resourceId);
                if (res) {
                    let overlapLimit = res.overlapLimit ? res.overlapLimit : 0;
                    // Delete covers of "non-ubiquity of user"
                    TIME_SLOT_COVER_SERVICE.toggleTimeSlotCover(a.start.valueOf() + overlapLimit, a.end.valueOf() - overlapLimit, 'cover-sibling-activities', false,this.currentDay)
                }
            })

            // Delete covers of requestor's delivery window
            let deliveryWindow = this.deliveryWindows.find(dw => dw.requestorCode === activity.appointmentReference.requestorCode)
            if (deliveryWindow && deliveryWindow.inactivity) {
                deliveryWindow.inactivity.forEach(i => {
                    TIME_SLOT_COVER_SERVICE.toggleTimeSlotCover(i.from, i.to, 'cover-delivery-window', false, this.currentDay,this.slotDurationMinutes)
                })
            }

            // Remove covers for resources
            this.resources.map(r => r.id).filter(id => activity.constraint.resourceIds.indexOf(id) === -1).forEach(resourceId => {
                $(`.fc-bg .fc-day.fc-widget-content[data-resource-id="${resourceId}"]`).removeClass('cover-resources')
            })

            // Restore style of activities that belong to other appointments
            $(`.fc-event[data-appointment-id!="${activity.appointmentReference.id}"]`).removeClass('otherAppointmentActivities')

            _enableUserInteraction('dragging-event')
        },



        /** ----- LISTENER'S CALLBACKS ----- */

        /**
         * [_removeDockFromPickerClick description]
         * @return {[type]}     [description]
         */
        _removeDockFromPickerClick(e) {
            let deletedDock = this.resources.find(r => r.referencedEntity.businessKey === $(e.target.getDOMNode()).parent().prev().data('selectedkey'))
            this.scheduler.fullCalendar('removeResource', deletedDock)
            this.resources.splice(this.resources.map(r => r.id).indexOf(deletedDock.id), 1)
            this.selectedDocks.splice(this.selectedDocks.map(sd => parseInt(sd.id)).indexOf(deletedDock.id), 1)
            this.selectedDocksPrevious = this.selectedDocks
            if (!this.resources || this.resources.length === 0) {
                this.scheduler.fullCalendar('refetchResources')
            }
        },

        /**
         * [_removeRequestorFromPickerClick description]
         * @return {[type]}     [description]
         */
        _removeRequestorFromPickerClick(e) {
            this.selectedRequestors.splice(this.selectedRequestors.map(sd => sd.code).indexOf($(e.target.getDOMNode()).parent().prev().data('selectedkey')), 1)
            this.selectedRequestorsPrevious = this.selectedRequestors
            this.scheduler.fullCalendar('refetchEvents')
        },

        _valueChangeDateFilter(e) {
            // Avoid getting value from picker's model because on Chrome the event is fired also
            // when the user select a different month and in that case tha model is still
            // undefined if it is the first time the user select a date.
            // A possible alternative solution could be setting a starting value on the picker.
            // let d = moment(this.pickerModel.get('dateSelect'));
            if(e.newVal !== e.prevVal && this._isValidDate(e.newVal)){
                this.scheduler.fullCalendar('gotoDate', moment(e.newVal).valueOf());
            }
        },

        _isValidDate(dateString) {
        // Regular expression to check the format 'YYYY-MM-DD'
        const regex = /^(\d{4})-(\d{2})-(\d{2})$/;

        const match = dateString.match(regex);
        if (!match) {
            return false; // Does not match the pattern
        }

        const year = parseInt(match[1], 10);
        const month = parseInt(match[2], 10);
        const day = parseInt(match[3], 10);

        // Check if year is in the valid range
        if (year < 0 || year > 9999) {
            return false;
        }

        // Check if date is valid
        const date = new Date(year, month - 1, day);
        return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
        },

        /**
         * Changes the datePicket to read mode or edit mode
         * The datePicker should be in read mode if the page is loading,
         * it should be in edit mode if the page is not loading
         * @param loading
         * @private
         */
        _changeDatePickerDisplayMode(loading){
            this.datePicker.set('displayMode', loading ? Y.SCE.Picker.Base.DISPLAY_MODE_READ : Y.SCE.Picker.Base.DISPLAY_MODE_EDIT);

        },


    }, {
        ATTRS: {}
    })
}, '0.0.1', {
    requires: [
        'sce-scheduler',
        'sce-ds-calendar-manager',
        'sce-ds-time-slot-cover-service',
        'sce-foundation',
        'sce-intl'
    ]
})


how can i retrieve the timezone from here?