| 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 |
* Obtain vars from arguments received in the layout file. |
| 16 |
* |
| 17 |
* @var string $tool The operator tool identifier calling this layout file. |
| 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 |
* @var array $data The data for rendering the tasks of a given month within a calendar. |
| 22 |
* @var int $no_tasks True for just displaying the month with no tasks, can be undefined. |
| 23 |
*/ |
| 24 |
extract($displayData); |
| 25 |
|
| 26 |
if (!$operator || empty($operator['id'])) { |
| 27 |
throw new Exception('Missing operator details', 400); |
| 28 |
} |
| 29 |
|
| 30 |
// check no-loading flag |
| 31 |
$no_loading = (bool) ($no_tasks ?? 0); |
| 32 |
|
| 33 |
// access the task manager object |
| 34 |
$taskManager = VBOFactory::getTaskManager(); |
| 35 |
|
| 36 |
// get the filters for rendering the calendar tasks |
| 37 |
$filters = (array) ($data['filters'] ?? []); |
| 38 |
|
| 39 |
// get the current operator permissions |
| 40 |
$accept_tasks = (bool) ($permissions ? $permissions->get('accept_tasks', 0) : 0); |
| 41 |
|
| 42 |
// determine the month to load |
| 43 |
$fromYmd = $filters['calendar_month'] ?? date('Y-m-01'); |
| 44 |
$toYmd = date('Y-m-t', strtotime($fromYmd)); |
| 45 |
if (($filters['dates'] ?? null) && !($filters['calendar_month'] ?? null)) { |
| 46 |
// when no calendar month filter set, use the current dates filter |
| 47 |
list($fromDt, $toDt) = VBOTaskModelTask::getInstance()->getFilterDatesInterval((string) $filters['dates'], $local = true, $sql = false); |
| 48 |
if ($fromDt && $toDt) { |
| 49 |
$fromYmd = date('Y-m-01', strtotime($fromDt)); |
| 50 |
$toYmd = date('Y-m-t', strtotime($toDt)); |
| 51 |
} |
| 52 |
} |
| 53 |
|
| 54 |
// build the pool of month tasks |
| 55 |
$monthTasks = []; |
| 56 |
|
| 57 |
if (!$no_loading) { |
| 58 |
// save current filters in the user state |
| 59 |
JFactory::getApplication()->setUserState('vbo.tm.filters', $filters); |
| 60 |
|
| 61 |
// check from the permissions whether tasks can be accepted by the operator, |
| 62 |
// hence null assigness should be included - use a different filter otherwise |
| 63 |
$operatorTasksFilterName = $accept_tasks ? 'operator' : 'assignee'; |
| 64 |
|
| 65 |
// build filter options for fetching the tasks |
| 66 |
$fetchOptions = array_merge($filters, [ |
| 67 |
$operatorTasksFilterName => $operator['id'], |
| 68 |
'dates' => $fromYmd . ':' . $toYmd, |
| 69 |
]); |
| 70 |
|
| 71 |
// load tasks according to filters, by always forcing/injecting the dates and the assignee/operator filter |
| 72 |
foreach (VBOTaskModelTask::getInstance()->filterItems($fetchOptions, 0, 0) as $taskRecord) { |
| 73 |
// wrap task record into a registry |
| 74 |
$task = VBOTaskTaskregistry::getInstance((array) $taskRecord); |
| 75 |
|
| 76 |
// get task due date in Y-m-d format |
| 77 |
$due_date_key = $task->getDueDate($local = true, $format = 'Y-m-d'); |
| 78 |
|
| 79 |
if ($due_date_key) { |
| 80 |
// start a container for this date |
| 81 |
$monthTasks[$due_date_key] = $monthTasks[$due_date_key] ?? []; |
| 82 |
|
| 83 |
// push task registry for this day |
| 84 |
$monthTasks[$due_date_key][] = $task; |
| 85 |
} |
| 86 |
} |
| 87 |
} |
| 88 |
|
| 89 |
// obtain the current month information |
| 90 |
$monthDate = getdate(strtotime($fromYmd)); |
| 91 |
$monthIter = $monthDate; |
| 92 |
|
| 93 |
// get navigation dates |
| 94 |
$todayYmd = date('Y-m-d'); |
| 95 |
$monthBack = date('Y-m-01', strtotime('-1 month', $monthDate[0])); |
| 96 |
$monthNext = date('Y-m-01', strtotime('+1 month', $monthDate[0])); |
| 97 |
$monthToday = date('Y-m-01'); |
| 98 |
|
| 99 |
// build week-day indexes |
| 100 |
$firstwday = (int) VikBooking::getFirstWeekDay(); |
| 101 |
$days_indexes = []; |
| 102 |
$wday_labels = [ |
| 103 |
JText::translate('VBSUN'), |
| 104 |
JText::translate('VBMON'), |
| 105 |
JText::translate('VBTUE'), |
| 106 |
JText::translate('VBWED'), |
| 107 |
JText::translate('VBTHU'), |
| 108 |
JText::translate('VBFRI'), |
| 109 |
JText::translate('VBSAT'), |
| 110 |
]; |
| 111 |
|
| 112 |
for ($d = 0; $d < 7; $d++) { |
| 113 |
$days_indexes[$d] = (6-($firstwday-$d)+1)%7; |
| 114 |
} |
| 115 |
|
| 116 |
?> |
| 117 |
|
| 118 |
<div class="vbo-tm-calendar-wrap"> |
| 119 |
<div class="vbo-tm-calendar-head"> |
| 120 |
<div class="vbo-tm-calendar-info"> |
| 121 |
<span class="vbo-tm-calendar-month-name"><?php echo VikBooking::sayMonth($monthDate['mon']); ?></span> |
| 122 |
<span class="vbo-tm-calendar-month-year"><?php echo $monthDate['year']; ?></span> |
| 123 |
</div> |
| 124 |
|
| 125 |
<div class="vbo-tm-calendar-nav"> |
| 126 |
<div class="vbo-tm-calendar-nav-btns"> |
| 127 |
<span class="vbo-tm-calendar-nav-btn vbo-tm-calendar-nav-back" data-month="<?php echo $monthBack; ?>"><?php VikBookingIcons::e('chevron-left'); ?></span> |
| 128 |
<span class="vbo-tm-calendar-nav-btn vbo-tm-calendar-nav-today" data-month="<?php echo $monthToday; ?>"><?php echo JText::translate('VBTODAY'); ?></span> |
| 129 |
<span class="vbo-tm-calendar-nav-btn vbo-tm-calendar-nav-next" data-month="<?php echo $monthNext; ?>"><?php VikBookingIcons::e('chevron-right'); ?></span> |
| 130 |
</div> |
| 131 |
</div> |
| 132 |
</div> |
| 133 |
|
| 134 |
<div class="vbo-tm-calendar-month-container"> |
| 135 |
|
| 136 |
<div class="vbo-tm-calendar-month-row vbo-tm-calendar-month-weekdays"> |
| 137 |
<?php |
| 138 |
// display days of week |
| 139 |
for ($d = 0; $d < 7; $d++) { |
| 140 |
$d_ind = ($d + $firstwday) < 7 ? ($d + $firstwday) : ($d + $firstwday - 7); |
| 141 |
?> |
| 142 |
<div class="vbo-tm-calendar-month-day vbo-tm-calendar-month-weekday"> |
| 143 |
<span><?php echo $wday_labels[$d_ind]; ?></span> |
| 144 |
</div> |
| 145 |
<?php |
| 146 |
} |
| 147 |
?> |
| 148 |
</div> |
| 149 |
|
| 150 |
<div class="vbo-tm-calendar-month-row"> |
| 151 |
<?php |
| 152 |
// start month-day counter |
| 153 |
$d_count = 0; |
| 154 |
|
| 155 |
// display the initial empty week-days, if any |
| 156 |
for ($i = 0, $n = $days_indexes[$monthDate['wday']]; $i < $n; $i++, $d_count++) { |
| 157 |
?> |
| 158 |
<div class="vbo-tm-calendar-month-day vbo-tm-calendar-month-day-empty"></div> |
| 159 |
<?php |
| 160 |
} |
| 161 |
|
| 162 |
// loop through all dates of the current month |
| 163 |
while ($monthIter['mon'] == $monthDate['mon']) { |
| 164 |
if ($d_count > 6) { |
| 165 |
// week is over, reset counter and start a new row |
| 166 |
$d_count = 0; |
| 167 |
echo '</div>' . "\n" . '<div class="vbo-tm-calendar-month-row">' . "\n"; |
| 168 |
} |
| 169 |
|
| 170 |
// build date key |
| 171 |
$date_key = date('Y-m-d', $monthIter[0]); |
| 172 |
|
| 173 |
?> |
| 174 |
<div class="vbo-tm-calendar-month-day<?php echo $date_key == $todayYmd ? ' vbo-tm-calendar-month-today' : ''; ?>" data-date="<?php echo $date_key; ?>"> |
| 175 |
<span class="vbo-tm-calendar-mday"><?php echo $monthIter['mday']; ?></span> |
| 176 |
<?php |
| 177 |
if ($monthTasks[$date_key] ?? []) { |
| 178 |
?> |
| 179 |
<div class="vbo-tm-calendar-month-day-tasks"> |
| 180 |
<?php |
| 181 |
foreach ($monthTasks[$date_key] as $taskIndex => $task) { |
| 182 |
// get task status |
| 183 |
$statusColor = ''; |
| 184 |
$statusName = ''; |
| 185 |
if ($taskManager->statusTypeExists($task->getStatus())) { |
| 186 |
$status = $taskManager->getStatusTypeInstance($task->getStatus()); |
| 187 |
$statusColor = $status->getColor(); |
| 188 |
$statusName = $status->getName(); |
| 189 |
} |
| 190 |
|
| 191 |
// get task assignee details |
| 192 |
$assignees = $task->getAssigneeDetails(); |
| 193 |
?> |
| 194 |
<div class="vbo-tm-calendar-month-day-task vbo-tm-color <?php echo $statusColor ?: 'gray'; ?> <?php echo $statusName ? 'vbo-tooltip vbo-tooltip-top' : ''; ?>" data-task-id="<?php echo $task->getID(); ?>" data-area-id="<?php echo $task->getAreaID(); ?>" data-tooltiptext="<?php echo JHtml::fetch('esc_attr', $statusName); ?>"> |
| 195 |
<div class="vbo-tm-calendar-month-day-task-wrap"> |
| 196 |
<div class="vbo-tm-calendar-task-title"> |
| 197 |
<?php echo $task->getTitle(); ?> |
| 198 |
|
| 199 |
<?php if ($task->get('hasUnreadMessages', false)): ?> |
| 200 |
<span class="unread-message-dot"> |
| 201 |
<?php VikBookingIcons::e('comment'); ?> |
| 202 |
</span> |
| 203 |
<?php endif; ?> |
| 204 |
</div> |
| 205 |
<?php |
| 206 |
if ($assignees) { |
| 207 |
?> |
| 208 |
<span class="vbo-tm-calendar-task-assignees"> |
| 209 |
<?php |
| 210 |
foreach ($assignees as $operator) { |
| 211 |
?> |
| 212 |
<span class="vbo-tm-calendar-task-assignee vbo-tm-task-assignee"> |
| 213 |
<span class="vbo-tm-calendar-task-assignee-avatar vbo-tm-task-assignee-avatar" title="<?php echo JHtml::fetch('esc_attr', $operator['name']); ?>"> |
| 214 |
<?php |
| 215 |
if (!empty($operator['img_uri'])) { |
| 216 |
?> |
| 217 |
<img src="<?php echo $operator['img_uri']; ?>" alt="<?php echo JHtml::fetch('esc_attr', $operator['initials']); ?>" decoding="async" loading="lazy" /> |
| 218 |
<?php |
| 219 |
} else { |
| 220 |
?> |
| 221 |
<span><?php echo $operator['initials']; ?></span> |
| 222 |
<?php |
| 223 |
} |
| 224 |
?> |
| 225 |
</span> |
| 226 |
</span> |
| 227 |
<?php |
| 228 |
} |
| 229 |
?> |
| 230 |
</span> |
| 231 |
<?php |
| 232 |
} |
| 233 |
?> |
| 234 |
</div> |
| 235 |
</div> |
| 236 |
<?php |
| 237 |
if ($taskIndex > 1 && $exceeding = (count($monthTasks[$date_key]) - ++$taskIndex)) { |
| 238 |
// we display at most 3 tasks per day |
| 239 |
?> |
| 240 |
<div class="vbo-tm-calendar-month-day-task vbo-tm-calendar-month-day-more" data-date="<?php echo $date_key; ?>"> |
| 241 |
<span><?php echo sprintf('+%d %s', $exceeding, strtolower($exceeding > 1 ? JText::translate('VBO_TASKS') : JText::translate('VBO_TASK'))); ?></span> |
| 242 |
</div> |
| 243 |
<?php |
| 244 |
|
| 245 |
// break the execution of the tasks for this day |
| 246 |
break; |
| 247 |
} |
| 248 |
} |
| 249 |
?> |
| 250 |
</div> |
| 251 |
<?php |
| 252 |
} |
| 253 |
?> |
| 254 |
</div> |
| 255 |
<?php |
| 256 |
// increase month-day counter |
| 257 |
$d_count++; |
| 258 |
|
| 259 |
// go to next day |
| 260 |
$monthIter = getdate(mktime(0, 0, 0, $monthIter['mon'], $monthIter['mday'] + 1, $monthIter['year'])); |
| 261 |
} |
| 262 |
|
| 263 |
// display the ending empty week-days, if any |
| 264 |
for ($i = $d_count; $i <= 6; $i++) { |
| 265 |
?> |
| 266 |
<div class="vbo-tm-calendar-month-day vbo-tm-calendar-month-day-empty"></div> |
| 267 |
<?php |
| 268 |
} |
| 269 |
?> |
| 270 |
</div> |
| 271 |
|
| 272 |
</div> |
| 273 |
</div> |
| 274 |
|
| 275 |
<script type="text/javascript"> |
| 276 |
jQuery(function($) { |
| 277 |
|
| 278 |
/** |
| 279 |
* Register listeners for month navigation buttons. |
| 280 |
*/ |
| 281 |
document |
| 282 |
.querySelectorAll('.vbo-tm-calendar-nav-btn') |
| 283 |
.forEach((nav) => { |
| 284 |
// get the navigation month |
| 285 |
const month = nav.getAttribute('data-month'); |
| 286 |
|
| 287 |
nav.addEventListener('click', () => { |
| 288 |
VBOCore.emitEvent('vbo-tm-apply-filters', { |
| 289 |
filters: { |
| 290 |
calendar_month: month, |
| 291 |
}, |
| 292 |
}); |
| 293 |
}); |
| 294 |
}); |
| 295 |
|
| 296 |
/** |
| 297 |
* Register listeners for day calendar type. |
| 298 |
*/ |
| 299 |
document |
| 300 |
.querySelectorAll('.vbo-tm-calendar-month-day[data-date]') |
| 301 |
.forEach((day) => { |
| 302 |
// get the navigation month |
| 303 |
const ymd = day.getAttribute('data-date'); |
| 304 |
|
| 305 |
day |
| 306 |
.querySelector('.vbo-tm-calendar-mday') |
| 307 |
.addEventListener('click', (event) => { |
| 308 |
event.preventDefault(); |
| 309 |
event.stopPropagation(); |
| 310 |
|
| 311 |
VBOCore.emitEvent('vbo-tm-apply-filters', { |
| 312 |
filters: { |
| 313 |
calendar_type: 'day', |
| 314 |
calendar_day: ymd, |
| 315 |
}, |
| 316 |
}); |
| 317 |
}); |
| 318 |
}); |
| 319 |
|
| 320 |
/** |
| 321 |
* Register listeners for viewing an existing task. |
| 322 |
*/ |
| 323 |
document |
| 324 |
.querySelectorAll('.vbo-tm-calendar-month-day-task') |
| 325 |
.forEach((taskElement) => { |
| 326 |
if (taskElement.clickListener) { |
| 327 |
// listener added already |
| 328 |
return; |
| 329 |
} |
| 330 |
|
| 331 |
// get the clicked task and area IDs |
| 332 |
const taskId = taskElement.getAttribute('data-task-id'); |
| 333 |
const areaId = taskElement.getAttribute('data-area-id'); |
| 334 |
const date = taskElement.getAttribute('data-date'); |
| 335 |
|
| 336 |
if ((!taskId || !areaId) && !date) { |
| 337 |
// missing task details |
| 338 |
return; |
| 339 |
} |
| 340 |
|
| 341 |
if (!taskId && !areaId && date) { |
| 342 |
// see more day tasks |
| 343 |
taskElement.addEventListener('click', (event) => { |
| 344 |
event.preventDefault(); |
| 345 |
event.stopPropagation(); |
| 346 |
|
| 347 |
VBOCore.emitEvent('vbo-tm-apply-filters', { |
| 348 |
filters: { |
| 349 |
calendar_type: 'day', |
| 350 |
calendar_day: date, |
| 351 |
}, |
| 352 |
}); |
| 353 |
}); |
| 354 |
|
| 355 |
// abort |
| 356 |
return; |
| 357 |
} |
| 358 |
|
| 359 |
taskElement.addEventListener('click', (event) => { |
| 360 |
event.preventDefault(); |
| 361 |
event.stopPropagation(); |
| 362 |
|
| 363 |
VBOCore.emitEvent('vbo-tm-apply-filters', { |
| 364 |
filters: { |
| 365 |
calendar_back_type: 'month', |
| 366 |
calendar_month: '<?php echo $fromYmd; ?>', |
| 367 |
calendar_type: 'taskdetails', |
| 368 |
task_id: taskId, |
| 369 |
}, |
| 370 |
}); |
| 371 |
}); |
| 372 |
|
| 373 |
// turn flag on for listener set |
| 374 |
taskElement.clickListener = true; |
| 375 |
}); |
| 376 |
|
| 377 |
}); |
| 378 |
</script> |
| 379 |
|