PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
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 1.7.3 All 36 releases
vikbooking / admin / helpers / src / task / driveraware.php

driveraware.php in VikBooking Hotel Booking Engine & PMS trunk, at admin/helpers/src/task/driveraware.php

743 lines 25.7 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 core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2025 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 /**
15 * Declares all task driver methods.
16 *
17 * @since 1.18.0 (J) - 1.8.0 (WP)
18 */
19 abstract class VBOTaskDriveraware implements VBOTaskDriverinterface
20 {
21 /**
22 * @var ?VBOTaskArea
23 */
24 protected $area;
25
26 /**
27 * @var array
28 */
29 protected $settings = [];
30
31 /**
32 * @var array
33 */
34 protected $operators = [];
35
36 /**
37 * @var VBOTaskDrivercollector
38 */
39 protected $collector;
40
41 /**
42 * Proxy to construct the task driver object.
43 *
44 * @param ?VBOTaskArea $area The task area object.
45 *
46 * @return VBOTaskDriverinterface
47 */
48 public static function getInstance(?VBOTaskArea $area = null)
49 {
50 return new static($area);
51 }
52
53 /**
54 * Class constructor.
55 *
56 * @param ?VBOTaskArea $area The task area object.
57 */
58 public function __construct(?VBOTaskArea $area = null)
59 {
60 // set task area
61 $this->area = $area;
62
63 if ($this->area) {
64 // load task settings from current task area
65 $this->settings = $this->area->loadSettings();
66 }
67
68 // start a new collector registry
69 $this->collector = VBOTaskDrivercollector::getInstance();
70 }
71
72 /**
73 * Returns the name of the task driver.
74 *
75 * @return string The driver readable name.
76 */
77 public function getName()
78 {
79 return ucfirst($this->getID());
80 }
81
82 /**
83 * Returns the task driver icon.
84 *
85 * @return string The font-icon class identifier.
86 */
87 public function getIcon()
88 {
89 return VikBookingIcons::i('tasks');
90 }
91
92 /**
93 * Returns the task driver parameters to configure an area.
94 *
95 * @return array List of driver parameters.
96 */
97 public function getParams()
98 {
99 return [];
100 }
101
102 /**
103 * @inheritDoc
104 */
105 public function onManageTask(string $position, VBOTaskTaskregistry $task, VBOTaskArea $area)
106 {
107 /**
108 * Trigger event to inject custom HTML within the "taskmanager.tasks.managetask" layout.
109 *
110 * @param string $position The position where the output will be displayed.
111 * @param VBOTaskTaskregistry $task The task we are updating.
112 * @param VBOTaskArea $area The area this task belongs to.
113 *
114 * @return string The HTML to output.
115 *
116 * @since 1.18.15 (J) - 1.8.15 (WP)
117 */
118 $results = \VBOFactory::getPlatform()->getDispatcher()->filter('onDisplayManageTask', [$position, $task, $area]);
119
120 return implode("\n", $results);
121 }
122
123 /**
124 * @inheritDoc
125 */
126 public function scheduleBookingConfirmation(VBOTaskBooking $booking)
127 {
128 // no automatic scheduling supported upon booking confirmation
129 }
130
131 /**
132 * @inheritDoc
133 */
134 public function scheduleBookingAlteration(VBOTaskBooking $booking)
135 {
136 // no automatic scheduling supported upon booking alteration
137 }
138
139 /**
140 * @inheritDoc
141 */
142 public function scheduleBookingCancellation(VBOTaskBooking $booking)
143 {
144 // no automatic scheduling supported upon booking cancellation
145 }
146
147 /**
148 * Returns the task driver settings for the configured area.
149 *
150 * @return array
151 */
152 public function getSettings()
153 {
154 return $this->settings;
155 }
156
157 /**
158 * Sets the task driver settings.
159 *
160 * @param array $settings The settings to set.
161 * @param bool $merge True for merging the previous settings.
162 *
163 * @return void
164 */
165 public function setSettings(array $settings, bool $merge = false)
166 {
167 $this->settings = array_merge(($merge ? $this->settings : []), $settings);
168 }
169
170 /**
171 * Saves the task driver settings into its current area.
172 *
173 * @param ?array $settings Optional settings to save.
174 *
175 * @return void
176 */
177 public function saveSettings(?array $settings = null)
178 {
179 $this->area->saveSettings((is_array($settings) ? $settings : $this->settings));
180 }
181
182 /**
183 * Returns a specific task driver setting.
184 *
185 * @param string $name The setting name.
186 * @param mixed $default The default setting.
187 *
188 * @return mixed
189 */
190 public function getSetting(string $name, $default = null)
191 {
192 return $this->settings[$name] ?? $default;
193 }
194
195 /**
196 * Sets a value for a specific task driver setting.
197 *
198 * @param string $name The setting name.
199 * @param mixed $value The value to set.
200 *
201 * @return void
202 */
203 public function setSetting(string $name, $value)
204 {
205 $this->settings[$name] = $value;
206 }
207
208 /**
209 * Returns the current task driver collector.
210 *
211 * @param bool $reset True for resetting the collector.
212 *
213 * @return VBOTaskDrivercollector
214 */
215 public function getCollector(bool $reset = false)
216 {
217 if ($reset) {
218 return $this->collector->reset();
219 }
220
221 return $this->collector;
222 }
223
224 /**
225 * Returns the current project/area ID, if available.
226 *
227 * @return int The current area ID or 0.
228 */
229 public function getAreaID()
230 {
231 return $this->area ? $this->area->getID() : 0;
232 }
233
234 /**
235 * Returns the current project/area name, if available.
236 *
237 * @return string The current area name or empty string.
238 */
239 public function getAreaName()
240 {
241 return $this->area ? $this->area->getName() : '';
242 }
243
244 /**
245 * Returns the default status for new tasks for the current project/area, if any.
246 *
247 * @return ?string The default status enumeration or null.
248 */
249 public function getDefaultStatus()
250 {
251 return $this->area ? ($this->area->getDefaultStatus() ?: null) : null;
252 }
253
254 /**
255 * Returns the default task duration in minutes.
256 *
257 * @return int
258 */
259 public function getDefaultDuration()
260 {
261 // the driver may declare a parameter for the task default duration in minutes
262 return intval($this->getSetting('taskduration', 0)) ?: 60;
263 }
264
265 /**
266 * Returns the eligible operator IDs for the task driver.
267 *
268 * @return array List of eligible operator IDs or empty array.
269 */
270 public function getOperatorIds()
271 {
272 // the driver may declare a parameter to filter the eligible operators
273 return array_values(array_filter((array) $this->getSetting('operators', [])));
274 }
275
276 /**
277 * Returns the eligible listing IDs for the task driver.
278 *
279 * @return array List of eligible listing IDs or empty array.
280 */
281 public function getListingIds()
282 {
283 // the driver may declare a parameter to filter the eligible listings
284 return array_values(array_filter((array) $this->getSetting('listings', [])));
285 }
286
287 /**
288 * Tells whether a listing ID is eligible according to the current task driver settings.
289 *
290 * @param int $listingId The listing ID to evaluate.
291 *
292 * @return bool
293 */
294 public function isListingEligible(int $listingId)
295 {
296 $eligible_ids = array_map('intval', $this->getListingIds());
297
298 return !$eligible_ids || in_array($listingId, $eligible_ids);
299 }
300
301 /**
302 * Loads the eligible operators for the task driver.
303 *
304 * @param bool $elements True for getting the operators as elements to render.
305 * @param array $activeAssignees Optional list of active assignee IDs to merge.
306 *
307 * @return array Associative (by ID) list of operator array records.
308 */
309 public function getOperators(bool $elements = false, array $activeAssignees = [])
310 {
311 $operatorIds = array_values(array_unique(array_merge($this->getOperatorIds(), array_filter($activeAssignees))));
312
313 if ($elements) {
314 // always avoid caching when element records are requested
315 return VikBooking::getOperatorInstance()->getElements($operatorIds);
316 }
317
318 if ($this->operators) {
319 // return the cached operator records
320 return $this->operators;
321 }
322
323 // get all the eligible operators
324 $operators = VikBooking::getOperatorInstance()->getAll($operatorIds);
325
326 // map some internal properties
327 $operators = array_map(function($operator) {
328 // decode or set the needed information
329 $operator['perms'] = !empty($operator['perms']) ? (is_string($operator['perms']) ? (array) json_decode($operator['perms'], true) : $operator['perms']) : [];
330 $operator['work_days_week'] = !empty($operator['work_days_week']) ? (is_string($operator['work_days_week']) ? (array) json_decode($operator['work_days_week'], true) : $operator['work_days_week']) : [];
331 $operator['work_days_exceptions'] = !empty($operator['work_days_exceptions']) ? (is_string($operator['work_days_exceptions']) ? (array) json_decode($operator['work_days_exceptions'], true) : $operator['work_days_exceptions']) : [];
332
333 // return the manipulated operator record
334 return $operator;
335 }, $operators);
336
337 // cache the eligible operator records
338 $this->operators = $operators;
339
340 return $operators;
341 }
342
343 /**
344 * Returns the operator record ID, if any.
345 *
346 * @param int $operatorId The operator ID.
347 *
348 * @return array
349 */
350 public function getOperatorFromId(int $operatorId)
351 {
352 foreach ($this->getOperators() as $operator) {
353 if ($operator['id'] == $operatorId) {
354 // return the requested record found
355 return $operator;
356 }
357 }
358
359 return [];
360 }
361
362 /**
363 * Returns the working hours configured by the specified operator for the requested date.
364 *
365 * @param int|array $operator Either the operator ID or its details.
366 * @param DateTime $date The requested date.
367 *
368 * @return int The number of working hours.
369 */
370 public function getDateWorkingHours($operator, DateTime $date)
371 {
372 if (is_numeric($operator)) {
373 $operator = $this->getOperatorFromId((int) $operator);
374 }
375
376 if (empty($operator)) {
377 return 0;
378 }
379
380 if (!is_array($operator['work_days_week'] ?? null)) {
381 $operator['work_days_week'] = [];
382 }
383
384 if (!is_array($operator['work_days_exceptions'] ?? null)) {
385 $operator['work_days_exceptions'] = [];
386 }
387
388 $ymd = $date->format('Y-m-d');
389
390 // scan working day exceptions backward, to give higher priority to rules created last
391 for ($i = count($operator['work_days_exceptions']) - 1; $i >= 0; $i--) {
392 $rule = $operator['work_days_exceptions'][$i];
393
394 if (empty($rule['from'])) {
395 // missing from date, malformed rule, move on
396 continue;
397 }
398
399 if (empty($rule['to'])) {
400 // single date provided, to date same as from date
401 $rule['to'] = $rule['from'];
402 }
403
404 // check whether the date is contained within the configured range
405 if ($rule['from'] <= $ymd && $ymd <= $rule['to']) {
406 // yep, return the number of working hours, if any
407 return (int) ($rule['hours'] ?? 0);
408 }
409 }
410
411 // no exceptions for the specified date, fallback to the default week days
412 $weekDay = (int) $date->format('w');
413
414 foreach ($operator['work_days_week'] as $rule) {
415 if (!isset($rule['wday'])) {
416 // missing day of the week, malformed rule, move on
417 continue;
418 }
419
420 // check whether the day of the week matches the specified date
421 if ($rule['wday'] == $weekDay) {
422 // yep, return the number of working hours, if any
423 return (int) ($rule['hours'] ?? 0);
424 }
425 }
426
427 // no working hours defined, we have a day off for this operator
428 return 0;
429 }
430
431 /**
432 * Loads the eligible listings for the task driver.
433 *
434 * @return array List of listing array records.
435 */
436 public function getListings()
437 {
438 return VikBooking::getAvailabilityInstance(true)->loadRooms($this->getListingIds(), 0, true);
439 }
440
441 /**
442 * Given a list of scheduling interval enumerations for a specific booking, builds
443 * and returns a list of task schedule objects for when tasks should be scheduled.
444 *
445 * @param array $scheduling List of scheduling interval enumerations.
446 * @param VBOTaskBooking $booking The current task booking registry.
447 *
448 * @return VBOTaskScheduleInterface[]
449 */
450 public function getBookingSchedulingDates(array $scheduling, VBOTaskBooking $booking)
451 {
452 $schedulesList = [];
453
454 foreach ($scheduling as $scheduleEnum) {
455 // obtain the schedule data for the current interval type
456 $schedule = VBOTaskSchedule::getType($scheduleEnum, $booking);
457 if ($schedule) {
458 // push the identified schedule data
459 $schedulesList[] = $schedule;
460 }
461 }
462
463 // sort schedule objects by ordering (ascending)
464 usort($schedulesList, function($a, $b) {
465 return $a->getOrdering() <=> $b->getOrdering();
466 });
467
468 return $schedulesList;
469 }
470
471 /**
472 * Returns the first available operator on the given date to handle the provided booking task.
473 *
474 * @param DateTime $dt The date (local timezone) for which the operator should be available.
475 * @param int $areaId The area where the new task should be scheduled.
476 *
477 *
478 * @return array Available operator record or empty array.
479 */
480 public function getAvailableOperator(DateTime $dt, int $areaId)
481 {
482 $dbo = JFactory::getDbo();
483
484 // build a list of available operator IDs according to their work days
485 $availableOperators = [];
486
487 foreach ($this->getOperators() as $operator) {
488 // get operator working hours for the specified date
489 $workingHours = $this->getDateWorkingHours($operator, $dt);
490
491 if ($workingHours) {
492 // register available operator with available minutes
493 $availableOperators[(int) $operator['id']] = $workingHours * 60;
494 }
495 }
496
497 if (!$availableOperators) {
498 // no operators configured to be available for work on this day
499 return [];
500 }
501
502 // sort available operators by working hours descending
503 arsort($availableOperators);
504
505 // obtain a date object in UTC and related SQL dates
506 $utc_dt = JFactory::getDate($dt->format('Y-m-d H:i:s'), $dt->getTimezone()->getName());
507 $utc_dt->modify('00:00:00');
508 $utc_start_sql = $utc_dt->toSql();
509 $utc_dt->modify('23:59:59');
510 $utc_end_sql = $utc_dt->toSql();
511
512 $areas = [
513 // preload the details for the requested area
514 $areaId => VBOTaskArea::getRecordInstance($areaId),
515 ];
516
517 // query the database to see what operators have got tasks assigned for this day
518 // this would be the right query to eventually implement a number of do-able tasks per day per operator (default to 1)
519 $dbo->setQuery(
520 $dbo->getQuery(true)
521 ->select($dbo->qn('ta.id_operator'))
522 ->select($dbo->qn('t.id_area'))
523 ->select('COUNT(1) AS ' . $dbo->qn('tot_tasks'))
524 ->from($dbo->qn('#__vikbooking_tm_tasks', 't'))
525 ->innerJoin($dbo->qn('#__vikbooking_tm_task_assignees', 'ta') . ' ON ' . $dbo->qn('t.id') . ' = ' . $dbo->qn('ta.id_task'))
526 ->where($dbo->qn('ta.id_operator') . ' IN (' . implode(', ', array_keys($availableOperators)) . ')')
527 ->where($dbo->qn('t.dueon') . ' BETWEEN ' . $dbo->q($utc_start_sql) . ' AND ' . $dbo->q($utc_end_sql))
528 ->group($dbo->qn('ta.id_operator'))
529 ->group($dbo->qn('t.id_area'))
530 );
531
532 foreach ($dbo->loadObjectList() as $operatorTasks) {
533 if (!isset($availableOperators[$operatorTasks->id_operator])) {
534 // operator not found, move on
535 continue;
536 }
537
538 if (!isset($areas[$operatorTasks->id_area])) {
539 // cache task area details
540 $areas[$operatorTasks->id_area] = VBOTaskArea::getRecordInstance($operatorTasks->id_area);
541 }
542
543 // get default duration per task
544 $duration = $areas[$operatorTasks->id_area]->getDefaultDuration();
545
546 // decrease working minutes by the duration of all scheduled tasks
547 $availableOperators[$operatorTasks->id_operator] -= $duration * $operatorTasks->tot_tasks;
548 }
549
550 // take only the operators that still have enough space to accept the new task
551 $availableOperators = array_keys(array_filter($availableOperators, function($minutes) use ($areas, $areaId) {
552 return ($minutes - $areas[$areaId]->getDefaultDuration()) >= 0;
553 }));
554
555 if (!$availableOperators) {
556 // no operators are free on this day
557 return [];
558 }
559
560 if (count($availableOperators) === 1 || VBOFactory::getConfig()->get('tm_op_assignment_strategy') === 'sequential') {
561 // there's only one free operator, or the assignment strategy is not "balanced", so we return the first one
562 return $this->getOperatorFromId($availableOperators[0]);
563 }
564
565 // check what operators have worked more on the closest dates (one week less and one week more)
566 $utc_dt->modify('00:00:00');
567 $utc_dt->modify('-7 days');
568 $utc_back_sql = $utc_dt->toSql();
569 $utc_dt->modify('+14 days');
570 $utc_forth_sql = $utc_dt->toSql();
571
572 // query the database to see what operators have got more tasks assigned on the closest dates
573 $dbo->setQuery(
574 $dbo->getQuery(true)
575 ->select($dbo->qn('ta.id_operator'))
576 ->select('COUNT(*) AS ' . $dbo->qn('tot_tasks'))
577 ->from($dbo->qn('#__vikbooking_tm_tasks', 't'))
578 ->innerJoin($dbo->qn('#__vikbooking_tm_task_assignees', 'ta') . ' ON ' . $dbo->qn('t.id') . ' = ' . $dbo->qn('ta.id_task'))
579 ->where($dbo->qn('ta.id_operator') . ' IN (' . implode(', ', $availableOperators) . ')')
580 ->where($dbo->qn('t.dueon') . ' BETWEEN ' . $dbo->q($utc_back_sql) . ' AND ' . $dbo->q($utc_forth_sql))
581 ->group($dbo->qn('ta.id_operator'))
582 );
583
584 $operatorTasks = $dbo->loadAssocList();
585
586 if (!$operatorTasks) {
587 // nobody has got tasks assigned on the closest dates, so we return the first operator available
588 return $this->getOperatorFromId($availableOperators[0]);
589 }
590
591 // build a list of operator IDs and number of assigned tasks
592 $workersTaskCount = array_combine(array_column($operatorTasks, 'id_operator'), array_column($operatorTasks, 'tot_tasks'));
593
594 $workersRanking = [];
595 foreach ($availableOperators as $operator_id) {
596 $workersRanking[] = [
597 'id_operator' => $operator_id,
598 'tot_tasks' => (int) ($workersTaskCount[$operator_id] ?? 0),
599 ];
600 }
601
602 // sort the operators task counter in ascending order
603 usort($workersRanking, function($a, $b) {
604 return $a['tot_tasks'] <=> $b['tot_tasks'];
605 });
606
607 // ensure spreading tasks across all the operators by taking the first sorted, hence with less tasks assigned
608 return $this->getOperatorFromId($workersRanking[0]['id_operator']);
609 }
610
611 /**
612 * Common method for all task drivers that support tasks scheduling upon booking confirmation.
613 *
614 * @param VBOTaskBooking $booking The current task booking registry.
615 * @param array $options Associative list of task scheduling options.
616 *
617 * @return int Number of tasks created.
618 */
619 protected function createBookingConfirmationTasks(VBOTaskBooking $booking, array $options = [])
620 {
621 // start counter
622 $created = 0;
623
624 // access the task model
625 $model = VBOTaskModelTask::getInstance();
626
627 // get all records that belong to this project/area and booking ID
628 $prevRecords = $model->getItemIds([
629 'id_area' => [
630 'value' => $this->getAreaID(),
631 ],
632 'id_order' => [
633 'value' => $booking->getID(),
634 ],
635 ]);
636
637 if ($prevRecords) {
638 // prevent duplicate tasks for the same project/area and booking ID from being created
639 return $created;
640 }
641
642 // prepare associative task/area information for the task(s) description
643 $info = [
644 'booking_id' => $booking->getID(),
645 'task_enum' => $this->getID(),
646 'area_id' => $this->getAreaID(),
647 'area_name' => $this->getAreaName(),
648 ];
649
650 // iterate over the listings involved in the reservation
651 foreach ($booking->getRooms() as $index => $listing) {
652 // set current room index
653 $booking->setCurrentRoomIndex($index);
654
655 if (!$this->isListingEligible((int) $listing['idroom'])) {
656 // listing not eligible in the current project/area settings
657 continue;
658 }
659
660 // iterate over the task scheduling dates
661 foreach ($this->getBookingSchedulingDates((array) ($options['scheduling'] ?? []), $booking) as $schedule) {
662 // get the scheduler type (frequency)
663 $scheduler = $schedule->getType();
664
665 // iterate over the schedule dates, if any
666 foreach ($schedule->getDates() as $scheduleCounter => $dt) {
667 // prepare booking task record
668 $task = [
669 'id_area' => $this->getAreaID(),
670 'status_enum' => $this->getDefaultStatus(),
671 'scheduler' => $scheduler,
672 'title' => $schedule->getDescription($info, $scheduleCounter) . ' - ' . JText::translate('VBDASHBOOKINGID') . ' ' . $booking->getID(),
673 'id_order' => $booking->getID(),
674 'id_room' => $listing['idroom'],
675 'room_index' => $listing['roomindex'] ?: null,
676 'dueon' => $dt->format('Y-m-d H:i:s'),
677 'assignees' => [],
678 ];
679
680 $warnAdmin = false;
681
682 if ($options['autoassignment'] ?? null) {
683 // fetch the first available operator
684 $assignee = $this->getAvailableOperator($dt, $task['id_area']);
685
686 if ($assignee) {
687 // push the available operator ID
688 $task['assignees'][] = $assignee['id'];
689 } else {
690 // unable to automatically assign the task to an operator, warn the admin after saving the task
691 $warnAdmin = true;
692 }
693 }
694
695 /**
696 * Trigger event to allow third-party plugins to manipulate the task payload.
697 */
698 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeScheduleBookingConfirmationTask', [&$task, $booking, $options]);
699
700 // store the task record
701 $taskId = $model->save($task);
702
703 if (!$taskId) {
704 continue;
705 }
706
707 // register the new task within the collector by setting the ID obtained
708 $this->getCollector()->register(array_merge($task, ['id' => $taskId]));
709
710 // increase counter
711 $created++;
712
713 if ($warnAdmin) {
714 try {
715 // store a notification to warn the administrator that we have a scheduled task without assignee
716 VBOFactory::getNotificationCenter()->store([
717 [
718 'sender' => 'operators',
719 'type' => 'task.unassigned',
720 'title' => JText::translate('VBO_TASK_NOTIF_SCHEDULING_UNASSIGNED_TITLE'),
721 'summary' => JText::sprintf('VBO_TASK_NOTIF_SCHEDULING_UNASSIGNED_SUMMARY', $task['title']),
722 'widget' => 'booking_details',
723 'widget_options' => [
724 'bid' => $task['id_order'],
725 'task_id' => $taskId,
726 ],
727 // always skip signature check, so that we can allow a duplicate insert
728 '_signature' => md5(time()),
729 ],
730 ]);
731 } catch (Exception $e) {
732 // silently catch the error
733 return false;
734 }
735 }
736 }
737 }
738 }
739
740 return $created;
741 }
742 }
743