PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.6
VikBooking Hotel Booking Engine & PMS v1.8.6
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 1.8.6, at admin/helpers/src/task/driveraware.php

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