File "driveraware.php"
Full Path: /home/romayxjt/public_html/wp-content/plugins/vikbooking/admin/helpers/src/task/driveraware.php
File size: 24.66 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* @package VikBooking
* @subpackage core
* @author E4J s.r.l.
* @copyright Copyright (C) 2025 E4J s.r.l. All Rights Reserved.
* @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
* @link https://vikwp.com
*/
// No direct access
defined('ABSPATH') or die('No script kiddies please!');
/**
* Declares all task driver methods.
*
* @since 1.18.0 (J) - 1.8.0 (WP)
*/
abstract class VBOTaskDriveraware implements VBOTaskDriverinterface
{
/**
* @var ?VBOTaskArea
*/
protected $area;
/**
* @var array
*/
protected $settings = [];
/**
* @var array
*/
protected $operators = [];
/**
* @var VBOTaskDrivercollector
*/
protected $collector;
/**
* Proxy to construct the task driver object.
*
* @param ?VBOTaskArea $area The task area object.
*
* @return VBOTaskDriverinterface
*/
public static function getInstance(?VBOTaskArea $area = null)
{
return new static($area);
}
/**
* Class constructor.
*
* @param ?VBOTaskArea $area The task area object.
*/
public function __construct(?VBOTaskArea $area = null)
{
// set task area
$this->area = $area;
if ($this->area) {
// load task settings from current task area
$this->settings = $this->area->loadSettings();
}
// start a new collector registry
$this->collector = VBOTaskDrivercollector::getInstance();
}
/**
* Returns the name of the task driver.
*
* @return string The driver readable name.
*/
public function getName()
{
return ucfirst($this->getID());
}
/**
* Returns the task driver icon.
*
* @return string The font-icon class identifier.
*/
public function getIcon()
{
return VikBookingIcons::i('tasks');
}
/**
* Returns the task driver parameters to configure an area.
*
* @return array List of driver parameters.
*/
public function getParams()
{
return [];
}
/**
* @inheritDoc
*/
public function scheduleBookingConfirmation(VBOTaskBooking $booking)
{
// no automatic scheduling supported upon booking confirmation
}
/**
* @inheritDoc
*/
public function scheduleBookingAlteration(VBOTaskBooking $booking)
{
// no automatic scheduling supported upon booking alteration
}
/**
* @inheritDoc
*/
public function scheduleBookingCancellation(VBOTaskBooking $booking)
{
// no automatic scheduling supported upon booking cancellation
}
/**
* Returns the task driver settings for the configured area.
*
* @return array
*/
public function getSettings()
{
return $this->settings;
}
/**
* Sets the task driver settings.
*
* @param array $settings The settings to set.
* @param bool $merge True for merging the previous settings.
*
* @return void
*/
public function setSettings(array $settings, bool $merge = false)
{
$this->settings = array_merge(($merge ? $this->settings : []), $settings);
}
/**
* Saves the task driver settings into its current area.
*
* @param ?array $settings Optional settings to save.
*
* @return void
*/
public function saveSettings(?array $settings = null)
{
$this->area->saveSettings((is_array($settings) ? $settings : $this->settings));
}
/**
* Returns a specific task driver setting.
*
* @param string $name The setting name.
* @param mixed $default The default setting.
*
* @return mixed
*/
public function getSetting(string $name, $default = null)
{
return $this->settings[$name] ?? $default;
}
/**
* Sets a value for a specific task driver setting.
*
* @param string $name The setting name.
* @param mixed $value The value to set.
*
* @return void
*/
public function setSetting(string $name, $value)
{
$this->settings[$name] = $value;
}
/**
* Returns the current task driver collector.
*
* @param bool $reset True for resetting the collector.
*
* @return VBOTaskDrivercollector
*/
public function getCollector(bool $reset = false)
{
if ($reset) {
return $this->collector->reset();
}
return $this->collector;
}
/**
* Returns the current project/area ID, if available.
*
* @return int The current area ID or 0.
*/
public function getAreaID()
{
return $this->area ? $this->area->getID() : 0;
}
/**
* Returns the current project/area name, if available.
*
* @return string The current area name or empty string.
*/
public function getAreaName()
{
return $this->area ? $this->area->getName() : '';
}
/**
* Returns the default status for new tasks for the current project/area, if any.
*
* @return ?string The default status enumeration or null.
*/
public function getDefaultStatus()
{
return $this->area ? ($this->area->getDefaultStatus() ?: null) : null;
}
/**
* Returns the default task duration in minutes.
*
* @return int
*/
public function getDefaultDuration()
{
// the driver may declare a parameter for the task default duration in minutes
return intval($this->getSetting('taskduration', 0)) ?: 60;
}
/**
* Returns the eligible operator IDs for the task driver.
*
* @return array List of eligible operator IDs or empty array.
*/
public function getOperatorIds()
{
// the driver may declare a parameter to filter the eligible operators
return array_values(array_filter((array) $this->getSetting('operators', [])));
}
/**
* Returns the eligible listing IDs for the task driver.
*
* @return array List of eligible listing IDs or empty array.
*/
public function getListingIds()
{
// the driver may declare a parameter to filter the eligible listings
return array_values(array_filter((array) $this->getSetting('listings', [])));
}
/**
* Tells whether a listing ID is eligible according to the current task driver settings.
*
* @param int $listingId The listing ID to evaluate.
*
* @return bool
*/
public function isListingEligible(int $listingId)
{
$eligible_ids = array_map('intval', $this->getListingIds());
return !$eligible_ids || in_array($listingId, $eligible_ids);
}
/**
* Loads the eligible operators for the task driver.
*
* @param bool $elements True for getting the operators as elements to render.
* @param array $activeAssignees Optional list of active assignee IDs to merge.
*
* @return array Associative (by ID) list of operator array records.
*/
public function getOperators(bool $elements = false, array $activeAssignees = [])
{
$operatorIds = array_values(array_unique(array_merge($this->getOperatorIds(), array_filter($activeAssignees))));
if ($elements) {
// always avoid caching when element records are requested
return VikBooking::getOperatorInstance()->getElements($operatorIds);
}
if ($this->operators) {
// return the cached operator records
return $this->operators;
}
// get all the eligible operators
$operators = VikBooking::getOperatorInstance()->getAll($operatorIds);
// map some internal properties
$operators = array_map(function($operator) {
// decode or set the needed information
$operator['perms'] = !empty($operator['perms']) ? (is_string($operator['perms']) ? (array) json_decode($operator['perms'], true) : $operator['perms']) : [];
$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']) : [];
$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']) : [];
// return the manipulated operator record
return $operator;
}, $operators);
// cache the eligible operator records
$this->operators = $operators;
return $operators;
}
/**
* Returns the operator record ID, if any.
*
* @param int $operatorId The operator ID.
*
* @return array
*/
public function getOperatorFromId(int $operatorId)
{
foreach ($this->getOperators() as $operator) {
if ($operator['id'] == $operatorId) {
// return the requested record found
return $operator;
}
}
return [];
}
/**
* Returns the working hours configured by the specified operator for the requested date.
*
* @param int|array $operator Either the operator ID or its details.
* @param DateTime $date The requested date.
*
* @return int The number of working hours.
*/
public function getDateWorkingHours($operator, DateTime $date)
{
if (is_numeric($operator)) {
$operator = $this->getOperatorFromId((int) $operator);
}
if (empty($operator)) {
return 0;
}
if (!is_array($operator['work_days_week'] ?? null)) {
$operator['work_days_week'] = [];
}
if (!is_array($operator['work_days_exceptions'] ?? null)) {
$operator['work_days_exceptions'] = [];
}
$ymd = $date->format('Y-m-d');
// scan working day exceptions backward, to give higher priority to rules created last
for ($i = count($operator['work_days_exceptions']) - 1; $i >= 0; $i--) {
$rule = $operator['work_days_exceptions'][$i];
if (empty($rule['from'])) {
// missing from date, malformed rule, move on
continue;
}
if (empty($rule['to'])) {
// single date provided, to date same as from date
$rule['to'] = $rule['from'];
}
// check whether the date is contained within the configured range
if ($rule['from'] <= $ymd && $ymd <= $rule['to']) {
// yep, return the number of working hours, if any
return (int) ($rule['hours'] ?? 0);
}
}
// no exceptions for the specified date, fallback to the default week days
$weekDay = (int) $date->format('w');
foreach ($operator['work_days_week'] as $rule) {
if (!isset($rule['wday'])) {
// missing day of the week, malformed rule, move on
continue;
}
// check whether the day of the week matches the specified date
if ($rule['wday'] == $weekDay) {
// yep, return the number of working hours, if any
return (int) ($rule['hours'] ?? 0);
}
}
// no working hours defined, we have a day off for this operator
return 0;
}
/**
* Loads the eligible listings for the task driver.
*
* @return array List of listing array records.
*/
public function getListings()
{
return VikBooking::getAvailabilityInstance(true)->loadRooms($this->getListingIds(), 0, true);
}
/**
* Given a list of scheduling interval enumerations for a specific booking, builds
* and returns a list of task schedule objects for when tasks should be scheduled.
*
* @param array $scheduling List of scheduling interval enumerations.
* @param VBOTaskBooking $booking The current task booking registry.
*
* @return VBOTaskScheduleInterface[]
*/
public function getBookingSchedulingDates(array $scheduling, VBOTaskBooking $booking)
{
$schedulesList = [];
foreach ($scheduling as $scheduleEnum) {
// obtain the schedule data for the current interval type
$schedule = VBOTaskSchedule::getType($scheduleEnum, $booking);
if ($schedule) {
// push the identified schedule data
$schedulesList[] = $schedule;
}
}
// sort schedule objects by ordering (ascending)
usort($schedulesList, function($a, $b) {
return $a->getOrdering() <=> $b->getOrdering();
});
return $schedulesList;
}
/**
* Returns the first available operator on the given date to handle the provided booking task.
*
* @param DateTime $dt The date (local timezone) for which the operator should be available.
* @param int $areaId The area where the new task should be scheduled.
*
*
* @return array Available operator record or empty array.
*/
public function getAvailableOperator(DateTime $dt, int $areaId)
{
$dbo = JFactory::getDbo();
// build a list of available operator IDs according to their work days
$availableOperators = [];
foreach ($this->getOperators() as $operator) {
// get operator working hours for the specified date
$workingHours = $this->getDateWorkingHours($operator, $dt);
if ($workingHours) {
// register available operator with available minutes
$availableOperators[(int) $operator['id']] = $workingHours * 60;
}
}
if (!$availableOperators) {
// no operators configured to be available for work on this day
return [];
}
// obtain a date object in UTC and related SQL dates
$utc_dt = JFactory::getDate($dt->format('Y-m-d H:i:s'), $dt->getTimezone()->getName());
$utc_dt->modify('00:00:00');
$utc_start_sql = $utc_dt->toSql();
$utc_dt->modify('23:59:59');
$utc_end_sql = $utc_dt->toSql();
$areas = [
// preload the details for the requested area
$areaId => VBOTaskArea::getRecordInstance($areaId),
];
// query the database to see what operators have got tasks assigned for this day
// this would be the right query to eventually implement a number of do-able tasks per day per operator (default to 1)
$dbo->setQuery(
$dbo->getQuery(true)
->select($dbo->qn('ta.id_operator'))
->select($dbo->qn('t.id_area'))
->select('COUNT(1) AS ' . $dbo->qn('tot_tasks'))
->from($dbo->qn('#__vikbooking_tm_tasks', 't'))
->innerJoin($dbo->qn('#__vikbooking_tm_task_assignees', 'ta') . ' ON ' . $dbo->qn('t.id') . ' = ' . $dbo->qn('ta.id_task'))
->where($dbo->qn('ta.id_operator') . ' IN (' . implode(', ', array_keys($availableOperators)) . ')')
->where($dbo->qn('t.dueon') . ' BETWEEN ' . $dbo->q($utc_start_sql) . ' AND ' . $dbo->q($utc_end_sql))
->group($dbo->qn('ta.id_operator'))
->group($dbo->qn('t.id_area'))
);
foreach ($dbo->loadObjectList() as $operatorTasks) {
if (!isset($availableOperators[$operatorTasks->id_operator])) {
// operator not found, move on
continue;
}
if (!isset($areas[$operatorTasks->id_area])) {
// cache task area details
$areas[$operatorTasks->id_area] = VBOTaskArea::getRecordInstance($operatorTasks->id_area);
}
// get default duration per task
$duration = $areas[$operatorTasks->id_area]->getDefaultDuration();
// decrease working minutes by the duration of all scheduled tasks
$availableOperators[$operatorTasks->id_operator] -= $duration * $operatorTasks->tot_tasks;
}
// take only the operators that still have enough space to accept the new task
$availableOperators = array_keys(array_filter($availableOperators, function($minutes) use ($areas, $areaId) {
return ($minutes - $areas[$areaId]->getDefaultDuration()) >= 0;
}));
if (!$availableOperators) {
// no operators are free on this day
return [];
}
if (count($availableOperators) === 1) {
// there's only one free operator, so we return it immediately
return $this->getOperatorFromId($availableOperators[0]);
}
// check what operators have worked more on the closest dates (one week less and one week more)
$utc_dt->modify('00:00:00');
$utc_dt->modify('-7 days');
$utc_back_sql = $utc_dt->toSql();
$utc_dt->modify('+14 days');
$utc_forth_sql = $utc_dt->toSql();
// query the database to see what operators have got more tasks assigned on the closest dates
$dbo->setQuery(
$dbo->getQuery(true)
->select($dbo->qn('ta.id_operator'))
->select('COUNT(*) AS ' . $dbo->qn('tot_tasks'))
->from($dbo->qn('#__vikbooking_tm_tasks', 't'))
->innerJoin($dbo->qn('#__vikbooking_tm_task_assignees', 'ta') . ' ON ' . $dbo->qn('t.id') . ' = ' . $dbo->qn('ta.id_task'))
->where($dbo->qn('ta.id_operator') . ' IN (' . implode(', ', $availableOperators) . ')')
->where($dbo->qn('t.dueon') . ' BETWEEN ' . $dbo->q($utc_back_sql) . ' AND ' . $dbo->q($utc_forth_sql))
->group($dbo->qn('ta.id_operator'))
);
$operatorTasks = $dbo->loadAssocList();
if (!$operatorTasks) {
// nobody has got tasks assigned on the closest dates, so we return the first operator available
return $this->getOperatorFromId($availableOperators[0]);
}
// build a list of operator IDs and number of assigned tasks
$workersTaskCount = array_combine(array_column($operatorTasks, 'id_operator'), array_column($operatorTasks, 'tot_tasks'));
$workersRanking = [];
foreach ($availableOperators as $operator_id) {
$workersRanking[] = [
'id_operator' => $operator_id,
'tot_tasks' => (int) ($workersTaskCount[$operator_id] ?? 0),
];
}
// sort the operators task counter in ascending order
usort($workersRanking, function($a, $b) {
return $a['tot_tasks'] <=> $b['tot_tasks'];
});
// ensure spreading tasks across all the operators by taking the first sorted, hence with less tasks assigned
return $this->getOperatorFromId($workersRanking[0]['id_operator']);
}
/**
* Common method for all task drivers that support tasks scheduling upon booking confirmation.
*
* @param VBOTaskBooking $booking The current task booking registry.
* @param array $options Associative list of task scheduling options.
*
* @return int Number of tasks created.
*/
protected function createBookingConfirmationTasks(VBOTaskBooking $booking, array $options = [])
{
// start counter
$created = 0;
// access the task model
$model = VBOTaskModelTask::getInstance();
// get all records that belong to this project/area and booking ID
$prevRecords = $model->getItemIds([
'id_area' => [
'value' => $this->getAreaID(),
],
'id_order' => [
'value' => $booking->getID(),
],
]);
if ($prevRecords) {
// prevent duplicate tasks for the same project/area and booking ID from being created
return $created;
}
// prepare associative task/area information for the task(s) description
$info = [
'booking_id' => $booking->getID(),
'task_enum' => $this->getID(),
'area_id' => $this->getAreaID(),
'area_name' => $this->getAreaName(),
];
// iterate over the listings involved in the reservation
foreach ($booking->getRooms() as $index => $listing) {
// set current room index
$booking->setCurrentRoomIndex($index);
if (!$this->isListingEligible((int) $listing['idroom'])) {
// listing not eligible in the current project/area settings
continue;
}
// iterate over the task scheduling dates
foreach ($this->getBookingSchedulingDates((array) ($options['scheduling'] ?? []), $booking) as $schedule) {
// get the scheduler type (frequency)
$scheduler = $schedule->getType();
// iterate over the schedule dates, if any
foreach ($schedule->getDates() as $scheduleCounter => $dt) {
// prepare booking task record
$task = [
'id_area' => $this->getAreaID(),
'status_enum' => $this->getDefaultStatus(),
'scheduler' => $scheduler,
'title' => $schedule->getDescription($info, $scheduleCounter) . ' - ' . JText::translate('VBDASHBOOKINGID') . ' ' . $booking->getID(),
'id_order' => $booking->getID(),
'id_room' => $listing['idroom'],
'room_index' => $listing['roomindex'] ?: null,
'dueon' => $dt->format('Y-m-d H:i:s'),
'assignees' => [],
];
$warnAdmin = false;
if ($options['autoassignment'] ?? null) {
// fetch the first available operator
$assignee = $this->getAvailableOperator($dt, $task['id_area']);
if ($assignee) {
// push the available operator ID
$task['assignees'][] = $assignee['id'];
} else {
// unable to automatically assign the task to an operator, warn the admin after saving the task
$warnAdmin = true;
}
}
/**
* Trigger event to allow third-party plugins to manipulate the task payload.
*/
VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeScheduleBookingConfirmationTask', [&$task, $booking, $options]);
// store the task record
$taskId = $model->save($task);
if (!$taskId) {
continue;
}
// register the new task within the collector by setting the ID obtained
$this->getCollector()->register(array_merge($task, ['id' => $taskId]));
// increase counter
$created++;
if ($warnAdmin) {
try {
// store a notification to warn the administrator that we have a scheduled task without assignee
VBOFactory::getNotificationCenter()->store([
[
'sender' => 'operators',
'type' => 'task.unassigned',
'title' => JText::translate('VBO_TASK_NOTIF_SCHEDULING_UNASSIGNED_TITLE'),
'summary' => JText::sprintf('VBO_TASK_NOTIF_SCHEDULING_UNASSIGNED_SUMMARY', $task['title']),
'widget' => 'booking_details',
'widget_options' => [
'bid' => $task['id_order'],
'task_id' => $taskId,
],
// always skip signature check, so that we can allow a duplicate insert
'_signature' => md5(time()),
],
]);
} catch (Exception $e) {
// silently catch the error
return false;
}
}
}
}
}
return $created;
}
}