File "task_manager.php"

Full Path: /home/romayxjt/public_html/wp-content/plugins/vikbooking/site/layouts/tools/task_manager.php
File size: 23.76 KB
MIME-type: text/x-php
Charset: utf-8

<?php
/**
 * @package     VikBooking
 * @subpackage  com_vikbooking
 * @author      Alessio Gaggii - E4J srl
 * @copyright   Copyright (C) 2025 E4J srl. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE
 * @link        https://vikwp.com
 */

defined('ABSPATH') or die('No script kiddies please!');

/**
 * Obtain vars from arguments received in the layout file.
 * This is the layout file for the "task_manager" operator tool.
 * 
 * @var string 	$tool 		   The tool identifier.
 * @var array 	$operator      The operator record accessing the tool.
 * @var object 	$permissions   The operator-tool permissions registry.
 * @var string 	$tool_uri 	   The base URI for rendering this tool.
 */
extract($displayData);

/**
 * Load the VBOCore JS class and chat assets.
 */
$vbo_app = VikBooking::getVboApplication();
$vbo_app->loadCoreJS();
$vbo_app->loadContextMenuAssets();
VBOFactory::getChatMediator()->useAssets();
VikBookingIcons::loadRemoteAssets();

$geo = VikBooking::getGeocodingInstance();

if ($geo->isSupported()) {
    $geo->loadAssets();
}

JHtml::fetch('vbohtml.tmscripts.changestatus');

// access the CMS application
$app = JFactory::getApplication();

// get the current filters
$filters = (array) $app->getUserStateFromRequest("vbo.tm.filters", 'filters', [], 'array');

// determine the current calendar type
$allowedTypes = [
    'month',
    'day',
    'taskdetails',
];
$calendarType = $filters['calendar_type'] ?? $allowedTypes[0];
$calendarType = in_array($calendarType, $allowedTypes) ? $calendarType : $allowedTypes[0];

// make sure to set the proper calendar type filter
$filters['calendar_type'] = $calendarType;

// build operator TM iCal URL
$operator_signature = base64_encode($operator['id'] . ':' . md5($operator['code']));
$tm_ical_uri = VBOFactory::getPlatform()->getUri()->route('index.php?option=com_vikbooking&view=operators&task=operatortool.tm_ical&opsid=' . urlencode($operator_signature));

// get the list of area/project IDs to which the current operator belongs
$areaIds = VBOTaskModelArea::getInstance()->getAssigneeItems($operator['id']);

// get the current operator permissions
$accept_tasks = (bool) ($permissions ? $permissions->get('accept_tasks', 0) : 0);

// check from the permissions whether tasks can be accepted by the operator,
// hence null assigness should be included - use a different filter otherwise
$operatorTasksFilterName = $accept_tasks ? 'operator' : 'assignee';

// build filter options for counting all future tasks (from the right areas/projects if fetching also the unassigned tasks)
$fetchOptions = array_merge($filters, [
    $operatorTasksFilterName => $operator['id'],
    'id_areas' => $accept_tasks ? $areaIds : null,
    'future' => true,
]);
$futureTasksCount = VBOTaskModelTask::getInstance()->filterItems($fetchOptions, 0, 1, $count = true);

// count future tasks assigned and unassigned, if permissions enabled
$assignedTasksCount = 0;
$unassignedTasksCount = 0;
if ($accept_tasks) {
    // build filter options for counting the future tasks assigned
    $fetchOptions = array_merge($filters, [
        'assignee' => $operator['id'],
        'future' => true,
    ]);
    $assignedTasksCount = VBOTaskModelTask::getInstance()->filterItems($fetchOptions, 0, 1, $count = true);

    // build filter options for counting the future tasks unassigned (yet from the right areas/projects)
    $fetchOptions = array_merge($filters, [
        'assignee' => -1,
        'id_areas' => $areaIds,
        'future' => true,
    ]);
    $unassignedTasksCount = VBOTaskModelTask::getInstance()->filterItems($fetchOptions, 0, 1, $count = true);
}

?>

<!--
    Create CSS rule to "hide" the textarea by keeping it
    active to allow the browser to copy its contents.
-->

<style>
    input.keep-active-but-hidden {
        width: 0 !important;
        height: 0 !important;
        opacity: 0 !important;
        float: right;
        cursor: default;
    }
</style>

<div class="vbo-tm-operator-head">
    <div class="vbo-tm-operator-block" data-type="future_tasks">
        <div class="vbo-tm-operator-block-title"><?php echo JText::translate('VBO_FUTURE_TASKS'); ?></div>
        <div class="vbo-tm-operator-block-cont">
            <span><?php echo $futureTasksCount; ?></span>
        </div>
        <div class="vbo-tm-operator-block-icon"><?php VikBookingIcons::e('tasks'); ?></div>
    </div>
<?php
if ($accept_tasks) {
    ?>
    <div class="vbo-tm-operator-block" data-type="assigned_tasks">
        <div class="vbo-tm-operator-block-title"><?php echo JText::translate('VBO_ASSIGNED_TASKS'); ?></div>
        <div class="vbo-tm-operator-block-cont">
            <span><?php echo $assignedTasksCount; ?></span>
        </div>
        <div class="vbo-tm-operator-block-icon"><?php VikBookingIcons::e('user-check'); ?></div>
    </div>
    <div class="vbo-tm-operator-block" data-type="unassigned_tasks">
        <div class="vbo-tm-operator-block-title"><?php echo JText::translate('VBO_UNASSIGNED_TASKS'); ?></div>
        <div class="vbo-tm-operator-block-cont">
            <span><?php echo $unassignedTasksCount; ?></span>
        </div>
        <div class="vbo-tm-operator-block-icon"><?php VikBookingIcons::e('user-plus'); ?></div>
    </div>
    <?php
}
?>
    <div class="vbo-tm-operator-block" data-type="ical">
        <div class="vbo-tm-operator-block-cont">
            <a href="javascript:void(0)" data-url="<?php echo $tm_ical_uri; ?>" id="subscribe-calendar-url">
                <span class="long"><?php echo JText::translate('VBO_SUBSCRIBE_CALENDAR'); ?></span>
                <span class="short"><?php echo JText::translate('VBO_SUBSCRIBE'); ?></span>
                <i class="<?php echo VikBookingIcons::i('chevron-down'); ?>" style="margin-left: 4px"></i>
            </a>
            <input type="text" id="subscribe-calendar-url-input" value="<?php echo $tm_ical_uri; ?>" class="keep-active-but-hidden" readonly />
        </div>
        <div class="vbo-tm-operator-block-icon"><?php VikBookingIcons::e('calendar'); ?></div>
    </div>
</div>

<div class="vbo-tm-calendar-tasks-wrap">
<?php
// display task manager calendar month layout with NO tasks
$layout_data = [
    'tool'        => $tool,
    'operator'    => $operator,
    'permissions' => $permissions,
    'tool_uri'    => $tool_uri,
    'data' => [
        'filters' => $filters,
    ],
    // inject flag for not loading any tasks and display just an empty calendar
    'no_tasks' => 1,
];

echo JLayoutHelper::render('taskmanager.calendar.' . $allowedTypes[0], $layout_data, null, [
    'component' => 'com_vikbooking',
    'client'    => 'site',
]);
?>
</div>

<script type="text/javascript">
    /**
     * Register global taskmanager filters.
     */
    const vboTmFilters = <?php echo json_encode(($filters ?: (new stdClass))); ?>;

    /**
     * Listen to the event for applying new filters.
     */
    document.addEventListener('vbo-tm-apply-filters', (e) => {
        if (!e || !e.detail || !e.detail.filters || typeof e.detail.filters !== 'object') {
            return;
        }

        for (const [type, value] of Object.entries(e.detail.filters)) {
            // set the requested filter value
            vboTmFilters[type] = value;
        }

        // dispatch the filters-changed event
        VBOCore.emitEvent('vbo-tm-filters-changed', {
            filters: vboTmFilters,
        });
    });

    /**
     * Register listener for the filters changed event.
     */
    document.addEventListener('vbo-tm-filters-changed', (e) => {
        // obtain the global filters
        let filters = e?.detail?.filters;

        // re-render tasks calendar
        vboTmCalendarLoadTasks(filters);

        const currentURL = new URL('<?php echo VBOFactory::getPlatform()->getUri()->route('index.php?option=com_vikbooking&view=operators&tool=task_manager'); ?>');

        for (const [key, val] of Object.entries(filters)) {
            if (val) {
                // set the requested filter value in new URL
                currentURL.searchParams.append(`filters[${key}]`, val);
            }
        }

        // change browser URL without performing any refresh
        history.replaceState(null, '', currentURL);
    });

    /**
     * Register listener to refresh the details of a task whenever the status changes.
     */
    document.addEventListener('vbo-task-status-changed', (event) => {
        VBOCore.emitEvent('vbo-tm-apply-filters', {
            filters: {}
        });
    });

    /**
     * Register function to activate the month loading animation.
     */
    const vboTmCalendarSetMonthLoading = (filters) => {
        let month_info = document.querySelector('.vbo-tm-calendar-info');

        if (!month_info) {
            return;
        }

        month_info.innerHTML = '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>';
    };

    /**
     * Register function to load the area tasks.
     */
    const vboTmCalendarLoadTasks = (filters) => {
        // activate loading
        vboTmCalendarSetMonthLoading();

        // detect the calendar layout type to load
        let calendarType = filters && filters?.calendar_type ? filters.calendar_type : '<?php echo $calendarType; ?>';

        // access the tasks wrapper
        let tasksWrapper = document.querySelector('.vbo-tm-calendar-tasks-wrap');

        // destroy the current chat before loading the new task details, if any
        VBOChat.getInstance().destroy();

        // make the request for loading the tasks
        VBOCore.doAjax(
            "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=operatortool.renderLayout'); ?>",
            {
                tool: '<?php echo $tool; ?>',
                type: calendarType == 'taskdetails' ? 'taskmanager.' + calendarType : 'taskmanager.calendar.' + calendarType,
                data: {
                    filters: filters || vboTmFilters || {},
                },
            },
            (resp) => {
                try {
                    // decode the response (if needed)
                    let obj_res = typeof resp === 'string' ? JSON.parse(resp) : resp;

                    // set the loading result
                    jQuery(tasksWrapper).html(obj_res['html']);
                } catch (err) {
                    console.error('Error decoding the response', err, resp);
                }
            },
            (error) => {
                // display error message
                alert(error.responseText);

                /**
                 * In case of permission errors, the interface may stick to the same page.
                 * For this reason, in case we were trying to access a task, we should reload
                 * the default monthly calendar.
                 */
                if (vboTmFilters.calendar_type === 'taskdetails') {
                    vboTmFilters.calendar_type = 'month';
                    delete vboTmFilters.task_id;

                    VBOCore.emitEvent('vbo-tm-filters-changed', {filters: vboTmFilters});
                }
            }
        );
    };

    VBOCore.DOMLoaded(() => {
        /**
         * Load calendar tasks upon page loading.
         */
        vboTmCalendarLoadTasks();
    });

    (function($) {
        'use strict';

        $(function() {
            $('#subscribe-calendar-url').vboContextMenu({
                placement: 'bottom-right',
                buttons: [
                    // Apple iCal
                    {
                        text: 'Apple iCal',
                        icon: '<?php echo VikBookingIcons::i('fab fa-apple'); ?>',
                        action: (root, event) => {
                            // replace HTTP(S) with WEBCAL protocol
                            let url = $(root).data('url').replace(/^https?:\/\//, 'webcal://');
                            // add subscriber software name
                            url += '&sub=apple';

                            setTimeout(function() {
                                // open subscription URL in a new browser page
                                window.open(url, '_blank');
                            }, 256);
                        },
                    },
                    // Google Calendar
                    {
                        text: 'Google Calendar',
                        icon: '<?php echo VikBookingIcons::i('fab fa-google'); ?>',
                        action: (root, event) => {
                            // replace HTTP(S) with WEBCAL protocol
                            let url = $(root).data('url').replace(/^https?:\/\//, 'webcal://');
                            // add subscriber software name
                            url += '&sub=google';
                            // encode URL and prepend Google Calendar renderer
                            url = 'https://www.google.com/calendar/render?cid=' + encodeURIComponent(url);

                            setTimeout(function() {
                                // open subscription URL in a new browser page
                                window.open(url, '_blank');
                            }, 256);
                        },
                    },
                    // Other
                    {
                        text: <?php echo json_encode(JText::translate('VBO_OTHER_CALENDAR')); ?>,
                        icon: '<?php echo VikBookingIcons::i('calendar-alt'); ?>',
                        separator: true,
                        action: (root, event) => {
                            // copy URL within the clipboard
                            VBOCore.copyToClipboard(document.getElementById('subscribe-calendar-url-input')).then((success) => {
                                alert(<?php echo json_encode(JText::translate('VBO_CALENDAR_COPIED_OK')); ?>);
                            }).catch((err) => {
                                alert('Copy error!');
                            });
                        },
                    },
                    // Download
                    {
                        text: <?php echo json_encode(JText::translate('VBO_DOWNLOAD')); ?>,
                        icon: '<?php echo VikBookingIcons::i('download'); ?>',
                        action: (root, event) => {
                            // open download link in a new page
                            window.open($(root).data('url'), '_blank');
                        },
                    }
                ],
            });
        });
    })(jQuery);

    (function($) {
        'use strict';

        /**
         * Defines the ratio to scale the size of the elements.
         *
         * @var float
         */
        let TABLE_SCALE_RATIO = 2.5;

        /**
         * Checks whether the specified intervals collide.
         *
         * @param   Date  start1  The initial date time of the first interval.
         * @param   Date  end1    The ending date time of the first interval.
         * @param   Date  start2  The initial date time of the second interval.
         * @param   Date  end1    The ending date time of the second interval.
         *
         * @return  boolean 
         */
        const checkIntersection = (start1, end1, start2, end2) => {
            return (start1 <= start2 && start2 <  end1)
                || (start1 <  end2   && end2   <= end1)
                || (start2 <  start1 && end1   <  end2);
        }

        /**
         * Proxy used to speed up the usage of checkIntersection by passing 2 valid events.
         *
         * @param   object  event1  The first event.
         * @param   object  event2  The second event.
         *
         * @return  boolean 
         */
        const checkEventsIntersection = (event1, event2) => {
            return checkIntersection(
                new Date(event1.start),
                new Date(event1.end),
                new Date(event2.start),
                new Date(event2.end)
            );
        }

        /**
         * Returns a list containing all the events that collide with the specified one.
         *
         * @param   object  event  An object holding the event details.
         * @param   mixed   level  An optional threshold to obtain only the
         *                         events on the left of the specified one.
         *
         * @return  array
         */
        const countIntersections = (event, level) => {
            let list = [];

            $('.vbo-tm-calendar-day-timeline-rows').find('.event').each(function() {
                let event2 = $(this).data('event');

                if (checkEventsIntersection(event, event2)) {
                    if (typeof level === 'undefined' || parseInt($(this).data('index')) < level) {
                        list.push(this);
                    }
                }
            });

            return list;
        }

        /**
         * Recursively adjusts the location and size of all the events that
         * collide with the specified one.
         *
         * @param   object  event  An object holding the event details.
         *
         * @return  void
         */
        const fixSiblingsCount = (event) => {
            let did = [];

            // recursive fix
            _fixSiblingsCount(event, did);
        }

        /**
         * Recursively adjusts the location and size of all the events that
         * collide with the specified one.
         * @visibility protected
         *
         * @param   object  event  An object holding the event details.
         * @param   array   did    An array containing all the events that
         *                         have been already fixed, just to avoid
         *                         increasing them more than once.
         *
         * @return  void
         */
        const _fixSiblingsCount = (event, did) => {
            let index = parseInt($(event).data('index'));

            let intersections = countIntersections($(event).data('event'), index);

            if (intersections.length) {
                intersections.forEach((e) => {
                    let found = false;

                    // make sure we didn't already fetch this event
                    did.forEach((ei) => {
                        found = found || $(e).is(ei);
                    });

                    if (!found) {
                        // get counters
                        let tmp   = parseInt($(e).data('siblings'));
                        let index = parseInt($(e).data('index'));

                        // adjust counter, size and position
                        $(e).data('siblings', tmp + 1);
                        $(e).css('width', 'calc(calc(100% / ' + (tmp + 2) + ') - 2px)');
                        $(e).css('left', 'calc(calc(calc(100% / ' + (tmp + 2) + ') * ' + (index) + ') + 2px)');

                        // flag event as already adjusted
                        did.push(e);

                        // recursively adjust the colliding events
                        _fixSiblingsCount(e, did);
                    }
                });
            }
        }

        /**
         * Adds the specified event into the calendar.
         *
         * @param   object  data  An object holding the event details.
         *
         * @return  void
         */
        const addCalendarEvent = (data) => {
            // search the row matching the hour of the event
            const hourRow = $('.vbo-tm-calendar-day-timeline-rows').find('.vbo-tm-calendar-day-timeline-row[data-hour="' + data.hour + '"]');

            if (!hourRow.length) {
                return false;
            }

            // create event
            const event = $(data.html).addClass('event');
            delete data.html;

            // event.attr('id', 'event-' + data.id);
            // event.attr('data-order-id', data.id);
            event.data('event', data);

            if (data.duration <= 15) {
                event.addClass('xsmall-block');
            } else if (data.duration < 30) {
                event.addClass('small-block');
            }

            // calculate event offset from top
            let offset = (data.hour * 60 + data.min) * TABLE_SCALE_RATIO;
            // calculate the threshold that cannot be exceeded
            let ceil = 1440 * TABLE_SCALE_RATIO;

            // make sure the height doesn't exceed the ceil
            let height = Math.min(data.duration * TABLE_SCALE_RATIO, ceil - offset) - 1;

            // vertically locate and resize the event box
            event.css('top', (data.min * TABLE_SCALE_RATIO) + 'px');
            event.css('height', height + 'px');

            // set color according to the selected service
            // let color = ('' + data.service_color).replace(/^#/, '');

            // event.css('background-color', '#' + color + '80');
            // event.css('border-left-color', '#' + color);

            // count number of events that intersect the appointment
            let intersections = countIntersections(data);

            let count = 0;

            // find the highest index position among the colliding events
            intersections.forEach((e) => {
                count = Math.max(count, parseInt($(e).data('index')) + 1);
            });

            // init siblings counter and index with the amount previously found
            event.data('siblings', count);
            event.data('index', count);

            // recursively adjust the counter of any other colliding event
            fixSiblingsCount(event);

            // locate and size the event before attaching it
            event.css('width', 'calc(calc(100% / ' + (count + 1) + ') - 2px)');
            event.css('left', 'calc(calc(calc(100% / ' + (count + 1) + ') * ' + (count) + ') + 2px)');

            // attach event to calendar
            hourRow.find('.vbo-tm-calendar-day-tasks').append(event);
        }

        /**
         * Configures the calendar by adding all the specified events.
         *
         * @param   array  events  A list of events to append.
         *
         * @return  void
         */
        window.taskManagerSetupCalendar = (events) => {
            $('.vbo-tm-calendar-day-tasks').html('');

            if (!events.length) {
                // do nothing
                return;
            }

            // init events
            events.forEach((event) => {
                event.intersections = [];
            });

            // scan conflicts between times
            for (var i = 0; i < events.length - 1; i++) {
                for (var j = i + 1; j < events.length; j++) {
                    let a = events[i];
                    let b = events[j];

                    if (checkEventsIntersection(a, b)) {
                        a.intersections.push(b);
                        b.intersections.push(a);
                    }
                }
            }

            // sort events by conflicts and ascending time
            events.sort((a, b) => {
                let diff = a.intersections.length - b.intersections.length;

                if (diff == 0) {
                    // same intersections, sort by check-in time
                    diff = (a.hour * 60 + a.min) - (b.hour * 60 + b.min);
                }

                return diff;
            });

            // attach events to calendar one by one
            events.forEach((event) => {
                addCalendarEvent(event);
            });

            let bounds = [24, -1];

            // calculate the minimum and the maximum hours that hold at least a task
            events.forEach((event) => {
                bounds[0] = Math.min(bounds[0], event.hour);
                bounds[1] = Math.max(bounds[1], event.hour + Math.ceil(event.duration / 60));
            });

            // hide all the hours before and after the calculated bounds
            $('.vbo-tm-calendar-day-timeline-row[data-hour]').each(function() {
                const hour = parseInt($(this).data('hour'));

                if (hour < bounds[0] - 1 || hour > bounds[1] + 1) {
                    $(this).hide();
                } else {
                    $(this).show();
                }
            });
        }
    })(jQuery);

</script>