PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / site / layouts / tools / task_manager.php

task_manager.php in VikBooking Hotel Booking Engine & PMS 1.8.16, at site/layouts/tools/task_manager.php

659 lines 23.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - E4J srl
6 * @copyright Copyright (C) 2025 E4J srl. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 /**
14 * Obtain vars from arguments received in the layout file.
15 * This is the layout file for the "task_manager" operator tool.
16 *
17 * @var string $tool The tool identifier.
18 * @var array $operator The operator record accessing the tool.
19 * @var object $permissions The operator-tool permissions registry.
20 * @var string $tool_uri The base URI for rendering this tool.
21 */
22 extract($displayData);
23
24 /**
25 * Load the VBOCore JS class and chat assets.
26 */
27 $vbo_app = VikBooking::getVboApplication();
28 $vbo_app->loadCoreJS();
29 $vbo_app->loadContextMenuAssets();
30 VBOFactory::getChatMediator()->useAssets();
31 VikBookingIcons::loadRemoteAssets();
32
33 $geo = VikBooking::getGeocodingInstance();
34
35 if ($geo->isSupported()) {
36 $geo->loadAssets([
37 'callback' => 'vbo_gm_ready',
38 'libraries' => 'marker',
39 'loading' => 'async',
40 ]);
41 }
42
43 JHtml::fetch('vbohtml.tmscripts.changestatus');
44
45 // access the CMS application
46 $app = JFactory::getApplication();
47
48 // get the current filters
49 $filters = (array) $app->getUserStateFromRequest("vbo.tm.filters", 'filters', [], 'array');
50
51 // determine the current calendar type
52 $allowedTypes = [
53 'month',
54 'day',
55 'taskdetails',
56 ];
57 $calendarType = $filters['calendar_type'] ?? $allowedTypes[0];
58 $calendarType = in_array($calendarType, $allowedTypes) ? $calendarType : $allowedTypes[0];
59
60 // make sure to set the proper calendar type filter
61 $filters['calendar_type'] = $calendarType;
62
63 // build operator TM iCal URL
64 $operator_signature = base64_encode($operator['id'] . ':' . md5($operator['code']));
65 $tm_ical_uri = VBOFactory::getPlatform()->getUri()->route('index.php?option=com_vikbooking&view=operators&task=operatortool.tm_ical&opsid=' . urlencode($operator_signature));
66
67 // get the list of area/project IDs to which the current operator belongs
68 $areaIds = VBOTaskModelArea::getInstance()->getAssigneeItems($operator['id']);
69
70 // get the current operator permissions
71 $accept_tasks = (bool) ($permissions ? $permissions->get('accept_tasks', 0) : 0);
72
73 // check from the permissions whether tasks can be accepted by the operator,
74 // hence null assigness should be included - use a different filter otherwise
75 $operatorTasksFilterName = $accept_tasks ? 'operator' : 'assignee';
76
77 // build filter options for counting all future tasks (from the right areas/projects if fetching also the unassigned tasks)
78 $fetchOptions = array_merge($filters, [
79 $operatorTasksFilterName => $operator['id'],
80 'id_areas' => $accept_tasks ? $areaIds : null,
81 'future' => true,
82 ]);
83 $futureTasksCount = VBOTaskModelTask::getInstance()->filterItems($fetchOptions, 0, 1, $count = true);
84
85 // count future tasks assigned and unassigned, if permissions enabled
86 $assignedTasksCount = 0;
87 $unassignedTasksCount = 0;
88 if ($accept_tasks) {
89 // build filter options for counting the future tasks assigned
90 $fetchOptions = array_merge($filters, [
91 'assignee' => $operator['id'],
92 'future' => true,
93 ]);
94 $assignedTasksCount = VBOTaskModelTask::getInstance()->filterItems($fetchOptions, 0, 1, $count = true);
95
96 // build filter options for counting the future tasks unassigned (yet from the right areas/projects)
97 $fetchOptions = array_merge($filters, [
98 'assignee' => -1,
99 'id_areas' => $areaIds,
100 'future' => true,
101 ]);
102 $unassignedTasksCount = VBOTaskModelTask::getInstance()->filterItems($fetchOptions, 0, 1, $count = true);
103 }
104
105 ?>
106
107 <!--
108 Create CSS rule to "hide" the textarea by keeping it
109 active to allow the browser to copy its contents.
110 -->
111
112 <style>
113 input.keep-active-but-hidden {
114 width: 0 !important;
115 height: 0 !important;
116 opacity: 0 !important;
117 float: right;
118 cursor: default;
119 }
120 </style>
121
122 <div class="vbo-tm-operator-head">
123 <div class="vbo-tm-operator-block" data-type="future_tasks">
124 <div class="vbo-tm-operator-block-title"><?php echo JText::translate('VBO_FUTURE_TASKS'); ?></div>
125 <div class="vbo-tm-operator-block-cont">
126 <span><?php echo $futureTasksCount; ?></span>
127 </div>
128 <div class="vbo-tm-operator-block-icon"><?php VikBookingIcons::e('tasks'); ?></div>
129 </div>
130 <?php
131 if ($accept_tasks) {
132 ?>
133 <div class="vbo-tm-operator-block" data-type="assigned_tasks">
134 <div class="vbo-tm-operator-block-title"><?php echo JText::translate('VBO_ASSIGNED_TASKS'); ?></div>
135 <div class="vbo-tm-operator-block-cont">
136 <span><?php echo $assignedTasksCount; ?></span>
137 </div>
138 <div class="vbo-tm-operator-block-icon"><?php VikBookingIcons::e('user-check'); ?></div>
139 </div>
140 <div class="vbo-tm-operator-block" data-type="unassigned_tasks">
141 <div class="vbo-tm-operator-block-title"><?php echo JText::translate('VBO_UNASSIGNED_TASKS'); ?></div>
142 <div class="vbo-tm-operator-block-cont">
143 <span><?php echo $unassignedTasksCount; ?></span>
144 </div>
145 <div class="vbo-tm-operator-block-icon"><?php VikBookingIcons::e('user-plus'); ?></div>
146 </div>
147 <?php
148 }
149 ?>
150 <div class="vbo-tm-operator-block" data-type="ical">
151 <div class="vbo-tm-operator-block-cont">
152 <a href="javascript:void(0)" data-url="<?php echo $tm_ical_uri; ?>" id="subscribe-calendar-url">
153 <span class="long"><?php echo JText::translate('VBO_SUBSCRIBE_CALENDAR'); ?></span>
154 <span class="short"><?php echo JText::translate('VBO_SUBSCRIBE'); ?></span>
155 <i class="<?php echo VikBookingIcons::i('chevron-down'); ?>" style="margin-left: 4px"></i>
156 </a>
157 <input type="text" id="subscribe-calendar-url-input" value="<?php echo $tm_ical_uri; ?>" class="keep-active-but-hidden" readonly />
158 </div>
159 <div class="vbo-tm-operator-block-icon"><?php VikBookingIcons::e('calendar'); ?></div>
160 </div>
161 </div>
162
163 <div class="vbo-tm-calendar-tasks-wrap">
164 <?php
165 // display task manager calendar month layout with NO tasks
166 $layout_data = [
167 'tool' => $tool,
168 'operator' => $operator,
169 'permissions' => $permissions,
170 'tool_uri' => $tool_uri,
171 'data' => [
172 'filters' => $filters,
173 ],
174 // inject flag for not loading any tasks and display just an empty calendar
175 'no_tasks' => 1,
176 ];
177
178 echo JLayoutHelper::render('taskmanager.calendar.' . $allowedTypes[0], $layout_data, null, [
179 'component' => 'com_vikbooking',
180 'client' => 'site',
181 ]);
182 ?>
183 </div>
184
185 <script type="text/javascript">
186 /**
187 * Register global taskmanager filters.
188 */
189 const vboTmFilters = <?php echo json_encode(($filters ?: (new stdClass))); ?>;
190
191 /**
192 * Listen to the event for applying new filters.
193 */
194 document.addEventListener('vbo-tm-apply-filters', (e) => {
195 if (!e || !e.detail || !e.detail.filters || typeof e.detail.filters !== 'object') {
196 return;
197 }
198
199 for (const [type, value] of Object.entries(e.detail.filters)) {
200 // set the requested filter value
201 vboTmFilters[type] = value;
202 }
203
204 // dispatch the filters-changed event
205 VBOCore.emitEvent('vbo-tm-filters-changed', {
206 filters: vboTmFilters,
207 });
208 });
209
210 /**
211 * Register listener for the filters changed event.
212 */
213 document.addEventListener('vbo-tm-filters-changed', (e) => {
214 // obtain the global filters
215 let filters = e?.detail?.filters;
216
217 // re-render tasks calendar
218 vboTmCalendarLoadTasks(filters);
219
220 const currentURL = new URL('<?php echo VBOFactory::getPlatform()->getUri()->route('index.php?option=com_vikbooking&view=operators&tool=task_manager'); ?>');
221
222 for (const [key, val] of Object.entries(filters)) {
223 if (val) {
224 // set the requested filter value in new URL
225 currentURL.searchParams.append(`filters[${key}]`, val);
226 }
227 }
228
229 // change browser URL without performing any refresh
230 history.replaceState(null, '', currentURL);
231 });
232
233 /**
234 * Register listener to refresh the details of a task whenever the status changes.
235 */
236 document.addEventListener('vbo-task-status-changed', (event) => {
237 VBOCore.emitEvent('vbo-tm-apply-filters', {
238 filters: {}
239 });
240 });
241
242 /**
243 * Register function to activate the month loading animation.
244 */
245 const vboTmCalendarSetMonthLoading = (filters) => {
246 let month_info = document.querySelector('.vbo-tm-calendar-info');
247
248 if (!month_info) {
249 return;
250 }
251
252 month_info.innerHTML = '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>';
253 };
254
255 /**
256 * Register function to load the area tasks.
257 */
258 const vboTmCalendarLoadTasks = (filters) => {
259 // activate loading
260 vboTmCalendarSetMonthLoading();
261
262 // detect the calendar layout type to load
263 let calendarType = filters && filters?.calendar_type ? filters.calendar_type : '<?php echo $calendarType; ?>';
264
265 // access the tasks wrapper
266 let tasksWrapper = document.querySelector('.vbo-tm-calendar-tasks-wrap');
267
268 // destroy the current chat before loading the new task details, if any
269 VBOChat.getInstance().destroy();
270
271 // make the request for loading the tasks
272 VBOCore.doAjax(
273 "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=operatortool.renderLayout'); ?>",
274 {
275 tool: '<?php echo $tool; ?>',
276 type: calendarType == 'taskdetails' ? 'taskmanager.' + calendarType : 'taskmanager.calendar.' + calendarType,
277 data: {
278 filters: filters || vboTmFilters || {},
279 },
280 },
281 (resp) => {
282 try {
283 // decode the response (if needed)
284 let obj_res = typeof resp === 'string' ? JSON.parse(resp) : resp;
285
286 // set the loading result
287 jQuery(tasksWrapper).html(obj_res['html']);
288 } catch (err) {
289 console.error('Error decoding the response', err, resp);
290 }
291 },
292 (error) => {
293 // display error message
294 alert(error.responseText);
295
296 /**
297 * In case of permission errors, the interface may stick to the same page.
298 * For this reason, in case we were trying to access a task, we should reload
299 * the default monthly calendar.
300 */
301 if (vboTmFilters.calendar_type === 'taskdetails') {
302 vboTmFilters.calendar_type = 'month';
303 delete vboTmFilters.task_id;
304
305 VBOCore.emitEvent('vbo-tm-filters-changed', {filters: vboTmFilters});
306 }
307 }
308 );
309 };
310
311 VBOCore.DOMLoaded(() => {
312 /**
313 * Load calendar tasks upon page loading.
314 */
315 vboTmCalendarLoadTasks();
316 });
317
318 (function($) {
319 'use strict';
320
321 $(function() {
322 $('#subscribe-calendar-url').vboContextMenu({
323 placement: 'bottom-right',
324 buttons: [
325 // Apple iCal
326 {
327 text: 'Apple iCal',
328 icon: '<?php echo VikBookingIcons::i('fab fa-apple'); ?>',
329 action: (root, event) => {
330 // replace HTTP(S) with WEBCAL protocol
331 let url = $(root).data('url').replace(/^https?:\/\//, 'webcal://');
332 // add subscriber software name
333 url += '&sub=apple';
334
335 setTimeout(function() {
336 // open subscription URL in a new browser page
337 window.open(url, '_blank');
338 }, 256);
339 },
340 },
341 // Google Calendar
342 {
343 text: 'Google Calendar',
344 icon: '<?php echo VikBookingIcons::i('fab fa-google'); ?>',
345 action: (root, event) => {
346 // replace HTTP(S) with WEBCAL protocol
347 let url = $(root).data('url').replace(/^https?:\/\//, 'webcal://');
348 // add subscriber software name
349 url += '&sub=google';
350 // encode URL and prepend Google Calendar renderer
351 url = 'https://www.google.com/calendar/render?cid=' + encodeURIComponent(url);
352
353 setTimeout(function() {
354 // open subscription URL in a new browser page
355 window.open(url, '_blank');
356 }, 256);
357 },
358 },
359 // Other
360 {
361 text: <?php echo json_encode(JText::translate('VBO_OTHER_CALENDAR')); ?>,
362 icon: '<?php echo VikBookingIcons::i('calendar-alt'); ?>',
363 separator: true,
364 action: (root, event) => {
365 // copy URL within the clipboard
366 VBOCore.copyToClipboard(document.getElementById('subscribe-calendar-url-input')).then((success) => {
367 alert(<?php echo json_encode(JText::translate('VBO_CALENDAR_COPIED_OK')); ?>);
368 }).catch((err) => {
369 alert('Copy error!');
370 });
371 },
372 },
373 // Download
374 {
375 text: <?php echo json_encode(JText::translate('VBO_DOWNLOAD')); ?>,
376 icon: '<?php echo VikBookingIcons::i('download'); ?>',
377 action: (root, event) => {
378 // open download link in a new page
379 window.open($(root).data('url'), '_blank');
380 },
381 }
382 ],
383 });
384 });
385 })(jQuery);
386
387 (function($) {
388 'use strict';
389
390 /**
391 * Defines the ratio to scale the size of the elements.
392 *
393 * @var float
394 */
395 let TABLE_SCALE_RATIO = 2.5;
396
397 /**
398 * Checks whether the specified intervals collide.
399 *
400 * @param Date start1 The initial date time of the first interval.
401 * @param Date end1 The ending date time of the first interval.
402 * @param Date start2 The initial date time of the second interval.
403 * @param Date end1 The ending date time of the second interval.
404 *
405 * @return boolean
406 */
407 const checkIntersection = (start1, end1, start2, end2) => {
408 return (start1 <= start2 && start2 < end1)
409 || (start1 < end2 && end2 <= end1)
410 || (start2 < start1 && end1 < end2);
411 }
412
413 /**
414 * Proxy used to speed up the usage of checkIntersection by passing 2 valid events.
415 *
416 * @param object event1 The first event.
417 * @param object event2 The second event.
418 *
419 * @return boolean
420 */
421 const checkEventsIntersection = (event1, event2) => {
422 return checkIntersection(
423 new Date(event1.start),
424 new Date(event1.end),
425 new Date(event2.start),
426 new Date(event2.end)
427 );
428 }
429
430 /**
431 * Returns a list containing all the events that collide with the specified one.
432 *
433 * @param object event An object holding the event details.
434 * @param mixed level An optional threshold to obtain only the
435 * events on the left of the specified one.
436 *
437 * @return array
438 */
439 const countIntersections = (event, level) => {
440 let list = [];
441
442 $('.vbo-tm-calendar-day-timeline-rows').find('.event').each(function() {
443 let event2 = $(this).data('event');
444
445 if (checkEventsIntersection(event, event2)) {
446 if (typeof level === 'undefined' || parseInt($(this).data('index')) < level) {
447 list.push(this);
448 }
449 }
450 });
451
452 return list;
453 }
454
455 /**
456 * Recursively adjusts the location and size of all the events that
457 * collide with the specified one.
458 *
459 * @param object event An object holding the event details.
460 *
461 * @return void
462 */
463 const fixSiblingsCount = (event) => {
464 let did = [];
465
466 // recursive fix
467 _fixSiblingsCount(event, did);
468 }
469
470 /**
471 * Recursively adjusts the location and size of all the events that
472 * collide with the specified one.
473 * @visibility protected
474 *
475 * @param object event An object holding the event details.
476 * @param array did An array containing all the events that
477 * have been already fixed, just to avoid
478 * increasing them more than once.
479 *
480 * @return void
481 */
482 const _fixSiblingsCount = (event, did) => {
483 let index = parseInt($(event).data('index'));
484
485 let intersections = countIntersections($(event).data('event'), index);
486
487 if (intersections.length) {
488 intersections.forEach((e) => {
489 let found = false;
490
491 // make sure we didn't already fetch this event
492 did.forEach((ei) => {
493 found = found || $(e).is(ei);
494 });
495
496 if (!found) {
497 // get counters
498 let tmp = parseInt($(e).data('siblings'));
499 let index = parseInt($(e).data('index'));
500
501 // adjust counter, size and position
502 $(e).data('siblings', tmp + 1);
503 $(e).css('width', 'calc(calc(100% / ' + (tmp + 2) + ') - 2px)');
504 $(e).css('left', 'calc(calc(calc(100% / ' + (tmp + 2) + ') * ' + (index) + ') + 2px)');
505
506 // flag event as already adjusted
507 did.push(e);
508
509 // recursively adjust the colliding events
510 _fixSiblingsCount(e, did);
511 }
512 });
513 }
514 }
515
516 /**
517 * Adds the specified event into the calendar.
518 *
519 * @param object data An object holding the event details.
520 *
521 * @return void
522 */
523 const addCalendarEvent = (data) => {
524 // search the row matching the hour of the event
525 const hourRow = $('.vbo-tm-calendar-day-timeline-rows').find('.vbo-tm-calendar-day-timeline-row[data-hour="' + data.hour + '"]');
526
527 if (!hourRow.length) {
528 return false;
529 }
530
531 // create event
532 const event = $(data.html).addClass('event');
533 delete data.html;
534
535 // event.attr('id', 'event-' + data.id);
536 // event.attr('data-order-id', data.id);
537 event.data('event', data);
538
539 if (data.duration <= 15) {
540 event.addClass('xsmall-block');
541 } else if (data.duration < 30) {
542 event.addClass('small-block');
543 }
544
545 // calculate event offset from top
546 let offset = (data.hour * 60 + data.min) * TABLE_SCALE_RATIO;
547 // calculate the threshold that cannot be exceeded
548 let ceil = 1440 * TABLE_SCALE_RATIO;
549
550 // make sure the height doesn't exceed the ceil
551 let height = Math.min(data.duration * TABLE_SCALE_RATIO, ceil - offset) - 1;
552
553 // vertically locate and resize the event box
554 event.css('top', (data.min * TABLE_SCALE_RATIO) + 'px');
555 event.css('height', height + 'px');
556
557 // set color according to the selected service
558 // let color = ('' + data.service_color).replace(/^#/, '');
559
560 // event.css('background-color', '#' + color + '80');
561 // event.css('border-left-color', '#' + color);
562
563 // count number of events that intersect the appointment
564 let intersections = countIntersections(data);
565
566 let count = 0;
567
568 // find the highest index position among the colliding events
569 intersections.forEach((e) => {
570 count = Math.max(count, parseInt($(e).data('index')) + 1);
571 });
572
573 // init siblings counter and index with the amount previously found
574 event.data('siblings', count);
575 event.data('index', count);
576
577 // recursively adjust the counter of any other colliding event
578 fixSiblingsCount(event);
579
580 // locate and size the event before attaching it
581 event.css('width', 'calc(calc(100% / ' + (count + 1) + ') - 2px)');
582 event.css('left', 'calc(calc(calc(100% / ' + (count + 1) + ') * ' + (count) + ') + 2px)');
583
584 // attach event to calendar
585 hourRow.find('.vbo-tm-calendar-day-tasks').append(event);
586 }
587
588 /**
589 * Configures the calendar by adding all the specified events.
590 *
591 * @param array events A list of events to append.
592 *
593 * @return void
594 */
595 window.taskManagerSetupCalendar = (events) => {
596 $('.vbo-tm-calendar-day-tasks').html('');
597
598 if (!events.length) {
599 // do nothing
600 return;
601 }
602
603 // init events
604 events.forEach((event) => {
605 event.intersections = [];
606 });
607
608 // scan conflicts between times
609 for (var i = 0; i < events.length - 1; i++) {
610 for (var j = i + 1; j < events.length; j++) {
611 let a = events[i];
612 let b = events[j];
613
614 if (checkEventsIntersection(a, b)) {
615 a.intersections.push(b);
616 b.intersections.push(a);
617 }
618 }
619 }
620
621 // sort events by conflicts and ascending time
622 events.sort((a, b) => {
623 let diff = a.intersections.length - b.intersections.length;
624
625 if (diff == 0) {
626 // same intersections, sort by check-in time
627 diff = (a.hour * 60 + a.min) - (b.hour * 60 + b.min);
628 }
629
630 return diff;
631 });
632
633 // attach events to calendar one by one
634 events.forEach((event) => {
635 addCalendarEvent(event);
636 });
637
638 let bounds = [24, -1];
639
640 // calculate the minimum and the maximum hours that hold at least a task
641 events.forEach((event) => {
642 bounds[0] = Math.min(bounds[0], event.hour);
643 bounds[1] = Math.max(bounds[1], event.hour + Math.ceil(event.duration / 60));
644 });
645
646 // hide all the hours before and after the calculated bounds
647 $('.vbo-tm-calendar-day-timeline-row[data-hour]').each(function() {
648 const hour = parseInt($(this).data('hour'));
649
650 if (hour < bounds[0] - 1 || hour > bounds[1] + 1) {
651 $(this).hide();
652 } else {
653 $(this).show();
654 }
655 });
656 }
657 })(jQuery);
658
659 </script>