<?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!');
/**
* Task manager implementation.
*
* @since 1.18.0 (J) - 1.8.0 (WP)
*/
final class VBOTaskManager
{
/**
* @var array
*/
private $drivers = [];
/**
* @var array
*/
private $errors = [];
/**
* @var array
*/
private $statusGroupTypes = [];
/**
* @var array
*/
private $statusTypes = [];
/**
* @var string
*/
private $taskClassPrefix = 'VBOTaskDriver';
/**
* @var string
*/
private $taskStatusGroupClassPrefix = 'VBOTaskStatusGroupType';
/**
* @var string
*/
private $taskStatusClassPrefix = 'VBOTaskStatusType';
/**
* Class constructor.
*/
public function __construct()
{
// pre-load the available task drivers
$this->loadDrivers();
// pre-load the available task status group type implementations
$this->loadStatusGroupTypes();
// pre-load the available task status type implementations
$this->loadStatusTypes();
}
/**
* Triggers the operations for scheduling tasks across all
* projects/areas upon a booking confirmation event.
*
* @param array $booking The booking record.
* @param array $booking_rooms The booking room records.
*
* @return bool
*/
public function processBookingConfirmation(array $booking, array $booking_rooms = [])
{
// reset the errors pool before starting
$this->errors = [];
// wrap the booking information into a registry
$taskBooking = VBOTaskBooking::getInstance($booking, $booking_rooms);
// iterate over all the active projects/areas, if any
foreach ($this->getAreas() as $area) {
// wrap the execution within a try-catch statement
try {
// bind the area record within a task-area registry
$area = VBOTaskArea::getInstance((array) $area);
// invoke the task area driver
$taskDriver = $this->getDriverInstance($area->getType(), [$area]);
// schedule tasks upon booking confirmation
$taskDriver->scheduleBookingConfirmation($taskBooking);
if ($newTasks = $taskDriver->getCollector()->getCreated()) {
// store booking history record
VikBooking::getBookingHistoryInstance($taskBooking->getID())
->setBookingData($booking, $booking_rooms)
->setExtraData(array_column($newTasks, 'id'))
->store('NT', implode(', ', array_map(function($id) {
return sprintf('#%d', $id);
}, array_column($newTasks, 'id'))));
}
} catch (Throwable $e) {
// push the error caught
$this->errors[] = $e;
}
}
return (bool) (!$this->errors);
}
/**
* Triggers the operations for re-scheduling tasks across all
* projects/areas upon a booking modification event.
*
* @param array $booking The booking record.
* @param array $booking_rooms The booking room records.
* @param array $prev_booking The previous booking record.
*
* @return bool
*/
public function processBookingModification(array $booking, array $booking_rooms = [], array $prev_booking = [])
{
// reset the errors pool before starting
$this->errors = [];
// wrap the booking information into a registry
$taskBooking = VBOTaskBooking::getInstance($booking, $booking_rooms, $prev_booking);
// iterate over all the active projects/areas, if any
foreach ($this->getAreas() as $area) {
// wrap the execution within a try-catch statement
try {
// bind the area record within a task-area registry
$area = VBOTaskArea::getInstance((array) $area);
// invoke the task area driver
$taskDriver = $this->getDriverInstance($area->getType(), [$area]);
// re-schedule tasks upon booking alteration, if needed
$taskDriver->scheduleBookingAlteration($taskBooking);
if ($modifiedTasks = $taskDriver->getCollector()->getModified()) {
// store booking history record
VikBooking::getBookingHistoryInstance($taskBooking->getID())
->setBookingData($booking, $booking_rooms)
->setExtraData(array_column($modifiedTasks, 'id'))
->store('MT', implode(', ', array_map(function($id) {
return sprintf('#%d', $id);
}, array_column($modifiedTasks, 'id'))));
}
} catch (Throwable $e) {
// push the error caught
$this->errors[] = $e;
}
}
return (bool) (!$this->errors);
}
/**
* Triggers the operations for un-scheduling tasks across all
* projects/areas upon a booking cancellation event.
*
* @param array $booking The booking record.
* @param array $booking_rooms The booking room records.
*
* @return bool
*/
public function processBookingCancellation(array $booking, array $booking_rooms = [])
{
// reset the errors pool before starting
$this->errors = [];
// wrap the booking information into a registry
$taskBooking = VBOTaskBooking::getInstance($booking, $booking_rooms);
// iterate over all the active projects/areas, if any
foreach ($this->getAreas() as $area) {
// wrap the execution within a try-catch statement
try {
// bind the area record within a task-area registry
$area = VBOTaskArea::getInstance((array) $area);
// invoke the task area driver
$taskDriver = $this->getDriverInstance($area->getType(), [$area]);
// un-schedule tasks upon booking cancellation
$taskDriver->scheduleBookingCancellation($taskBooking);
if ($oldTasks = $taskDriver->getCollector()->getCancelled()) {
// store booking history record
VikBooking::getBookingHistoryInstance($taskBooking->getID())
->setBookingData($booking, $booking_rooms)
->setExtraData(array_column($oldTasks, 'id'))
->store('CT', implode(', ', array_map(function($id) {
return sprintf('#%d', $id);
}, array_column($oldTasks, 'id'))));
}
} catch (Throwable $e) {
// push the error caught
$this->errors[] = $e;
}
}
return (bool) (!$this->errors);
}
/**
* Returns the current execution errors, if any.
*
* @return array
*/
public function getErrors()
{
return $this->errors;
}
/**
* Resets the current execution errors.
*
* @return VBOTaskManager
*/
public function resetErrors()
{
$this->errors = [];
return $this;
}
/**
* Returns all the active task area objects.
*
* @return array
*/
public function getAreas()
{
return VBOTaskModelArea::getInstance()->getItems();
}
/**
* Returns all areas with an active visibility by default.
* Relies on the current session by default, then on the db.
*
* @param int $start Query limit start.
* @param int $lim Query records limit.
*
* @return array List of visible area items, if any.
*/
public function getVisibleAreas(int $start = 0, int $lim = 0)
{
$active_area_ids = (array) JFactory::getSession()->get('tm.active_area_ids', [], 'vikbooking');
if ($active_area_ids) {
// get all areas stored in the session
return VBOTaskModelArea::getInstance()->getItems([
'id' => [
'value' => $active_area_ids,
],
], 0, 0);
}
// read the active areas from the db
$active_areas = VBOTaskModelArea::getInstance()->getItems([
'display' => [
'value' => 1,
],
], $start, $lim);
// set them as active
$this->setVisibleArea(array_column($active_areas, 'id'));
return $active_areas;
}
/**
* Sets visible areas in the PHP Session.
*
* @param int|array $id The area ID(s) to set as visible.
*
* @return void
*/
public function setVisibleArea($id)
{
$session = JFactory::getSession();
if (!is_array($id)) {
$id = (array) $id;
}
$id = array_map('intval', $id);
$active_area_ids = array_map('intval', (array) $session->get('tm.active_area_ids', [], 'vikbooking'));
$active_area_ids = array_values(array_unique(array_merge($active_area_ids, $id)));
$session->set('tm.active_area_ids', $active_area_ids, 'vikbooking');
}
/**
* Unsets visible areas in the PHP Session.
*
* @param int|array $id The area ID(s) to unset as visible.
*
* @return void
*/
public function unsetVisibleArea($id)
{
$session = JFactory::getSession();
if (!is_array($id)) {
$id = (array) $id;
}
$id = array_map('intval', $id);
$active_area_ids = array_map('intval', (array) $session->get('tm.active_area_ids', [], 'vikbooking'));
$active_area_ids = array_values(array_unique(array_diff($active_area_ids, $id)));
$session->set('tm.active_area_ids', $active_area_ids, 'vikbooking');
}
/**
* Returns a list of areas that were configured as private,
* hence not visible to operators within the front-end.
*
* @return array List of private area IDs, or empty array.
*/
public function getPrivateAreas()
{
$privateAreaIds = [];
foreach ($this->getAreas() as $areaRecord) {
$area = VBOTaskArea::getInstance((array) $areaRecord);
if ($area->isPrivate()) {
$privateAreaIds[] = $area->getID();
}
}
return array_values(array_filter($privateAreaIds));
}
/**
* Returns a list of default tag colors.
*
* @param bool $keys True to return only the color identifiers.
*
* @return array
*/
public function getTagColors(bool $keys = false)
{
$def_tag_colors = [
'red' => '#fbdcd9',
'green' => '#daebdc',
'olive' => '#c7d8b4',
'blue' => '#bed6fb',
'ocean' => '#d2e5f2',
'brown' => '#f0dfd7',
'yellow' => '#f8e5b3',
'orange' => '#ffe3ca',
'purple' => '#e8ddee',
'pink' => '#f6dfe9',
'black' => '#d0d0d0',
'gray' => '#e5e4e0',
];
return $keys ? array_keys($def_tag_colors) : $def_tag_colors;
}
/**
* Returns a default list of color tags to be used when none is available.
*
* @return object[] List of dummy color tag objects.
*/
public function buildDefaultColorTags()
{
// build the default list of color tags
$defaultTags = [
[
'name' => JText::translate('VBO_IMPORTANT'),
'color' => 'red',
],
[
'name' => JText::translate('VBO_SUPERVISOR_REVIEW'),
'color' => 'yellow',
],
[
'name' => JText::translate('VBO_TM_SCHED_CLEANING_TURNOVER'),
'color' => 'blue',
],
[
'name' => JText::translate('VBO_TM_SCHED_CLEANING_DAILY'),
'color' => 'green',
],
[
'name' => JText::translate('VBO_CHANGE_LINENS'),
'color' => 'ocean',
],
[
'name' => JText::translate('VBO_TM_SCHED_CLEANING_WEEKLY'),
'color' => 'olive',
],
[
'name' => JText::translate('VBO_DEEP_CLEANING'),
'color' => 'purple',
],
[
'name' => JText::translate('VBO_INSPECTION_NEEDED'),
'color' => 'orange',
],
[
'name' => JText::translate('VBO_GUEST_REQUEST'),
'color' => 'pink',
],
[
'name' => JText::translate('VBO_MAINTENANCE_ALERT'),
'color' => 'brown',
],
[
'name' => JText::translate('VBO_NO_SERVICE_REQUESTED'),
'color' => 'gray',
],
[
'name' => JText::translate('VBO_HIGH_PRIORITY'),
'color' => 'black',
],
];
// cast to objects
foreach ($defaultTags as &$tag) {
$tag = (object) $tag;
}
unset($tag);
return $defaultTags;
}
/**
* Returns all the available or requested color tags.
*
* @param array $ids Optional list of tag IDs to fetch.
*
* @return array
*/
public function getColorTags(array $ids = [])
{
// access the color tags model
$ctagModel = VBOTaskModelColortag::getInstance();
if ($ids) {
// return the requested tag IDs
return $ctagModel->getItems([
'id' => [
'value' => $ids,
],
]);
}
// load all items
$tags = $ctagModel->getItems();
if (!$tags) {
// create at runtime the default tags for the first time
$tags = $this->buildDefaultColorTags();
// store the default tags
foreach ($tags as $tag) {
$tag->id = $ctagModel->save($tag);
}
}
return $tags;
}
/**
* Attempts to instantiate the requested driver by passing the provided constructor arguments.
*
* @param string $driver The driver file key identifier.
* @param array $args List of arguments for constructing the object.
*
* @return VBOTaskDriverinterface
*
* @throws InvalidArgumentException
*/
public function getDriverInstance(string $driver, array $args = [])
{
$className = $this->buildDriverClassName($driver);
if (!class_exists($className)) {
throw new InvalidArgumentException(sprintf('Could not load task driver [%s]', $driver), 500);
}
// construct the task driver object by passing the args through the splat operator
return new $className(...$args);
}
/**
* Returns the associative list of the available driver names.
*
* @param array $args Optional list of arguments for constructing the objects.
*
* @return array
*/
public function getDriverNames(array $args = [])
{
$list = [];
foreach ($this->drivers as $key => $path) {
try {
$taskDriver = $this->getDriverInstance($key, $args);
$driverId = $taskDriver->getID() ?: $key;
$list[$driverId] = $taskDriver->getName();
} catch (Exception $e) {
// silently catch the error
}
}
return $list;
}
/**
* Returns the list of the drivers loaded so far.
*
* @return array
*/
public function getDrivers()
{
return $this->drivers;
}
/**
* Tells whether a driver exists, meaning that it was loaded.
*
* @param string $driver The driver file key identifier.
*
* @return bool
*/
public function driverExists(string $driver)
{
return isset($this->drivers[$driver]);
}
/**
* Builds the current task booking information for rendering the record as element.
*
* @param int $bid The booking record ID.
*
* @return array
*/
public function buildBookingElement(int $bid)
{
if (!$bid) {
return [];
}
$booking = VikBooking::getBookingInfoFromID($bid);
if (!$booking) {
return [];
}
$customer = VikBooking::getCPinInstance()->getCustomerFromBooking($booking['id']);
// build booking element
$element = [
'id' => $booking['id'],
'text' => $booking['id'],
'img' => '',
'icon_class' => VikBookingIcons::i('hotel'),
];
if (!empty($customer['first_name'])) {
// use customer nominative when available
$element['text'] = trim($customer['first_name'] . ' ' . $customer['last_name']);
} elseif (!empty($booking['custdata'])) {
$element['text'] = VikBooking::getFirstCustDataField($booking['custdata']);
}
// build "img" property
if (!empty($customer['pic'])) {
// use guest profile picture
$element['img'] = strpos($customer['pic'], 'http') === 0 ? $customer['pic'] : VBO_SITE_URI . 'resources/uploads/' . $customer['pic'];
} elseif (!empty($booking['channel'])) {
// use channel logo
$ch_logo_obj = VikBooking::getVcmChannelsLogo($booking['channel'], true);
$element['img'] = is_object($ch_logo_obj) ? $ch_logo_obj->getTinyLogoURL() : '';
}
if (!empty($element['img'])) {
// unset the default icon class
unset($element['icon_class']);
}
return $element;
}
/**
* Returns a list of task statuses sorted by group types to be rendered as elements.
*
* @param array $statuses Optional list of task status enumerations.
* @param bool $flatten True to ignore the groups and return a linear list of statuses.
*
* @return array
*/
public function getStatusGroupElements(array $statuses = [], bool $flatten = false)
{
$groupElements = [];
if (!$statuses) {
$statuses = $this->getStatusTypes(true);
}
foreach ($statuses as $statusId) {
// get status type object
$statusType = $this->getStatusTypeInstance($statusId);
// get status type values
$statusEnum = $statusType->getEnum();
$statusName = $statusType->getName();
$statusColor = $statusType->getColor();
$statusGroup = $statusType->getGroupEnum();
$statusOrdering = $statusType->getOrdering();
// get status group details
$groupName = $statusGroup;
$groupOrdering = 1;
if ($this->statusGroupTypeExists($statusGroup)) {
// get status group type object
$groupType = $this->getStatusGroupTypeInstance($statusGroup);
// set status group details
$groupName = $groupType->getName();
$groupOrdering = $groupType->getOrdering();
}
if (!isset($groupElements[$statusGroup])) {
// start group container
$groupElements[$statusGroup] = [
'text' => $groupName,
'ordering' => $groupOrdering,
'elements' => [],
];
}
// push status
$groupElements[$statusGroup]['elements'][] = [
'id' => $statusEnum,
'text' => $statusName,
'color' => $statusColor,
'ordering' => $statusOrdering,
];
}
// sort groups by ordering value ascending
uasort($groupElements, function($a, $b) {
return $a['ordering'] <=> $b['ordering'];
});
// iterate all status groups to sort the statuses by ordering
foreach ($groupElements as &$statusGroup) {
// sort statuses by ordering value ascending
usort($statusGroup['elements'], function($a, $b) {
return $a['ordering'] <=> $b['ordering'];
});
}
// unset last reference
unset($statusGroup);
if ($flatten) {
$statuses = [];
foreach ($groupElements as $group) {
foreach ($group['elements'] as $status) {
$statuses[] = $status;
}
}
$groupElements = $statuses;
}
// return the sorted list
return $groupElements;
}
/**
* Attempts to instantiate the requested status group type.
*
* @param string $group The group file key identifier.
*
* @return VBOTaskStatusGroupInterface
*
* @throws InvalidArgumentException
*/
public function getStatusGroupTypeInstance(string $group)
{
$className = $this->buildStatusGroupTypeClassName($group);
if (!class_exists($className)) {
throw new InvalidArgumentException(sprintf('Could not load task status group type [%s]', $group), 500);
}
return new $className;
}
/**
* Returns the list of the task status group types loaded so far.
*
* @return array
*/
public function getStatusGroupTypes()
{
return $this->statusGroupTypes;
}
/**
* Tells whether a status group type exists, meaning that it was loaded.
*
* @param string $group The group file key identifier.
*
* @return bool
*/
public function statusGroupTypeExists(string $group)
{
return isset($this->statusGroupTypes[$group]);
}
/**
* Attempts to instantiate the requested status type.
*
* @param string $status The status file key identifier.
*
* @return VBOTaskStatusInterface
*
* @throws InvalidArgumentException
*/
public function getStatusTypeInstance(string $status)
{
$className = $this->buildStatusTypeClassName($status);
if (!class_exists($className)) {
throw new InvalidArgumentException(sprintf('Could not load task status type [%s]', $status), 500);
}
return new $className;
}
/**
* Returns the list of the task status status types loaded so far.
*
* @param bool $enums True to get a list of status enumerations.
*
* @return array
*/
public function getStatusTypes(bool $enums = false)
{
return $enums ? array_keys($this->statusTypes) : $this->statusTypes;
}
/**
* Tells whether a status type exists, meaning that it was loaded.
*
* @param string $status The status file key identifier.
*
* @return bool
*/
public function statusTypeExists(string $status)
{
return isset($this->statusTypes[$status]);
}
/**
* Builds the task status status type class name.
*
* @param string $status The status file key identifier.
*
* @return string Status type class name or empty string.
*/
private function buildStatusTypeClassName(string $status)
{
if (!$this->statusTypeExists($status)) {
return '';
}
return $this->taskStatusClassPrefix . ucfirst(strtolower($status));
}
/**
* Builds the task status group type class name.
*
* @param string $group The group file key identifier.
*
* @return string Status group type class name or empty string.
*/
private function buildStatusGroupTypeClassName(string $group)
{
if (!$this->statusGroupTypeExists($group)) {
return '';
}
return $this->taskStatusGroupClassPrefix . ucfirst(strtolower($group));
}
/**
* Builds the task driver class name.
*
* @param string $driver The driver file key identifier.
*
* @return string Driver class name or empty string.
*/
private function buildDriverClassName(string $driver)
{
if (!$this->driverExists($driver)) {
return '';
}
return $this->taskClassPrefix . ucfirst(strtolower($driver));
}
/**
* Pre-loads all the available task driver implementations.
*
* @return void
*/
private function loadDrivers()
{
$drivers_base = implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'helpers', 'src', 'task', 'driver', '']);
$drivers_files = glob($drivers_base . '*.php');
/**
* Trigger event to let other plugins register additional drivers.
*
* @return array A list of supported drivers.
*/
$list = VBOFactory::getPlatform()->getDispatcher()->filter('onLoadTaskManagerDrivers');
foreach ($list as $chunk) {
// merge default driver files with the returned ones
$drivers_files = array_merge($drivers_files, (array) $chunk);
}
foreach ($drivers_files as $df) {
// push driver file key identifier and set related path
$driver_base_name = basename($df, '.php');
$this->drivers[$driver_base_name] = $df;
}
}
/**
* Pre-loads all the available task status group type implementations.
*
* @return void
*/
private function loadStatusGroupTypes()
{
$drivers_base = implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'helpers', 'src', 'task', 'status', 'group', 'type', '']);
$drivers_files = glob($drivers_base . '*.php');
/**
* Trigger event to let other plugins register additional status group types.
*
* @return array A list of supported status group types.
*/
$list = VBOFactory::getPlatform()->getDispatcher()->filter('onLoadTaskManagerStatusGroupTypes');
foreach ($list as $chunk) {
// merge default driver files with the returned ones
$drivers_files = array_merge($drivers_files, (array) $chunk);
}
foreach ($drivers_files as $df) {
// push driver file key identifier and set related path
$driver_base_name = basename($df, '.php');
$this->statusGroupTypes[$driver_base_name] = $df;
}
}
/**
* Pre-loads all the available task status type implementations.
*
* @return void
*/
private function loadStatusTypes()
{
$drivers_base = implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'helpers', 'src', 'task', 'status', 'type', '']);
$drivers_files = glob($drivers_base . '*.php');
/**
* Trigger event to let other plugins register additional status types.
*
* @return array A list of supported status types.
*/
$list = VBOFactory::getPlatform()->getDispatcher()->filter('onLoadTaskManagerStatusTypes');
foreach ($list as $chunk) {
// merge default driver files with the returned ones
$drivers_files = array_merge($drivers_files, (array) $chunk);
}
foreach ($drivers_files as $df) {
// push driver file key identifier and set related path
$driver_base_name = basename($df, '.php');
$this->statusTypes[$driver_base_name] = $df;
}
}
}