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 / model / task.php

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

930 lines 33.8 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 * Task model task implementation.
16 *
17 * @since 1.18.0 (J) - 1.8.0 (WP)
18 */
19 final class VBOTaskModelTask
20 {
21 /**
22 * Proxy for immediately accessing the object.
23 *
24 * @return VBOTaskModelTask
25 */
26 public static function getInstance()
27 {
28 return new static;
29 }
30
31 /**
32 * Class constructor.
33 */
34 public function __construct()
35 {}
36
37 /**
38 * Item loading implementation.
39 *
40 * @param mixed $pk An optional primary key value to load the row by,
41 * or an associative array of fields to match.
42 *
43 * @return object|null The record object on success, null otherwise.
44 */
45 public function getItem($pk)
46 {
47 $dbo = JFactory::getDbo();
48
49 $q = $dbo->getQuery(true)
50 ->select('*')
51 ->from($dbo->qn('#__vikbooking_tm_tasks'));
52
53 if (is_array($pk)) {
54 foreach ($pk as $column => $value) {
55 $q->where($dbo->qn($column) . ' = ' . $dbo->q($value));
56 }
57 } else {
58 $q->where($dbo->qn('id') . ' = ' . (int) $pk);
59 }
60
61 $dbo->setQuery($q, 0, 1);
62
63 return $dbo->loadObject();
64 }
65
66 /**
67 * Items loading implementation.
68 *
69 * @param array $clauses List of associative columns to filter
70 * (column => [operator, value])
71 * @param int $start Query limit start.
72 * @param int $lim Query limit value.
73 * @param array $cols Optional list of columns to fetch.
74 *
75 * @return array List of record objects.
76 */
77 public function getItems(array $clauses = [], $start = 0, $lim = 0, array $cols = [])
78 {
79 $app = JFactory::getApplication();
80 $dbo = JFactory::getDbo();
81
82 // tell whether we are actually counting the items
83 $counting = $cols && substr($cols[0] ?? '', 0, 5) === 'COUNT' && !$start && $lim === 1;
84
85 // start query object
86 $q = $dbo->getQuery(true);
87
88 if (!$cols) {
89 $q->select($dbo->qn('t') . '.*');
90 } else {
91 $q->select(array_map(function($column) use ($dbo) {
92 if (preg_match('/^[A-Z]/', $column)) {
93 // no quoting needed when column name starts with an upper case letter (i.e "COUNT(*)")
94 return $column;
95 }
96 if (!preg_match('/^t\./', $column)) {
97 $column = 't.' . $column;
98 }
99 return $dbo->qn($column);
100 }, $cols));
101 }
102
103 $q->from($dbo->qn('#__vikbooking_tm_tasks', 't'));
104
105 if (($clauses['assignee'] ?? null) || ($clauses['assignees'] ?? null) || ($clauses['operator'] ?? null)) {
106 $q->leftJoin($dbo->qn('#__vikbooking_tm_task_assignees', 'ta') . ' ON ' . $dbo->qn('ta.id_task') . ' = ' . $dbo->qn('t.id'));
107 }
108
109 if (is_array($clauses['fulltext'] ?? null) && is_string($clauses['fulltext']['value'] ?? null)) {
110 // full-text special clause to search over task titles and notes
111 // select full-text match score (relevance)
112 $q->select('MATCH(' . $dbo->qn('title') . ', ' . $dbo->qn('notes') . ') AGAINST(' . $dbo->q($clauses['fulltext']['value']) . ') AS ' . $dbo->qn('relevance'));
113 // add where statement to only include matches
114 $q->where('MATCH(' . $dbo->qn('title') . ', ' . $dbo->qn('notes') . ') AGAINST(' . $dbo->q($clauses['fulltext']['value']) . ') > 0');
115 // add order by match relevance
116 $q->order($dbo->qn('relevance') . ' DESC');
117 // unset this special clause
118 unset($clauses['fulltext']);
119 }
120
121 foreach ($clauses as $column => $data) {
122 if (!is_array($data) || !array_key_exists('value', $data)) {
123 // null values are also accepted for "value"
124 continue;
125 }
126
127 if (in_array($column, ['assignee', 'assignees', 'operator'])) {
128 $column = 'ta.id_operator';
129 } elseif (!preg_match('/^t\./', $column)) {
130 $column = 't.' . $column;
131 }
132
133 if (is_array($data['value'])) {
134 if (preg_match('/[a-z]/i', ($data['value'][0] ?? '0'))) {
135 // use "IN" for a list of quoted strings
136 $q->where($dbo->qn($column) . ' IN (' . implode(', ', array_map([$dbo, 'q'], $data['value'])) . ')');
137 } else {
138 // default to "IN" for a list of integers
139 $q->where($dbo->qn($column) . ' IN (' . implode(', ', array_map('intval', $data['value'])) . ')');
140 }
141 } else {
142 // singular fetching value
143 if (is_null($data['value'])) {
144 // look for a null (or not null) value
145 $q->where($dbo->qn($column) . ' IS' . (($data['operator'] ?? '=') == '!=' ? ' NOT' : '') . ' NULL');
146 } else {
147 // look for a real value
148 if ($data['instruction'] ?? null) {
149 // raw clause instruction given
150 $q->where($data['instruction']);
151 } else {
152 // match value
153 $q->where($dbo->qn($column) . ' ' . ($data['operator'] ?? '=') . ' ' . $dbo->q($data['value']));
154 }
155 }
156 }
157 }
158
159 if (!isset($clauses['dueon'])) {
160 // default ordering is by current date to list the upcoming tasks
161 $q->order('IF(' . $dbo->qn('t.dueon') . ' >= ' . $dbo->q(JFactory::getDate('now', $app->get('offset'))->toSql()) . ', 1, 0)' . ' DESC');
162 }
163 $q->order($dbo->qn('t.dueon') . ' ASC');
164 $q->order($dbo->qn('t.id') . ' ASC');
165
166 $dbo->setQuery($q, $start, $lim);
167
168 if ($counting) {
169 // count items
170 return $dbo->loadResult();
171 }
172
173 // fetch items
174 $tasks = $dbo->loadObjectList();
175
176 try {
177 // take the latest 20 unread threads
178 $threads = VBOFactory::getChatMediator()->getMessages(
179 (new VBOChatSearch)
180 ->aggregate()
181 ->unread()
182 ->limit(20)
183 );
184 } catch (Exception $e) {
185 // silently catch any possible authentication error
186 $threads = [];
187 }
188
189 $threadsLookup = [];
190
191 // map threads by context ID
192 foreach ($threads as $message) {
193 $threadsLookup[$message->getContext()->getID()] = $message;
194 }
195
196 // check whether the loaded tasks have at least an unread message
197 foreach ($tasks as $task) {
198 $task->hasUnreadMessages = (bool) ($threadsLookup[$task->id] ?? null);
199 }
200
201 return $tasks;
202 }
203
204 /**
205 * Item IDs loading implementation.
206 *
207 * @param array $clauses List of associative columns to filter
208 * (column => [operator, value])
209 * @param int $start Query limit start.
210 * @param int $lim Query limit value.
211 *
212 * @return array List of record objects.
213 */
214 public function getItemIds(array $clauses = [], $start = 0, $lim = 0)
215 {
216 return $this->getItems($clauses, $start, $lim, ['id']);
217 }
218
219 /**
220 * Items loading through filtering implementation.
221 *
222 * @param array $filters Associative list filters to apply.
223 * @param int $start Query limit start.
224 * @param int $lim Query limit value.
225 * @param bool $count True for counting rather than fetching.
226 *
227 * @return array List of record objects.
228 */
229 public function filterItems(array $filters, $start = 0, $lim = 0, bool $count = false)
230 {
231 $app = JFactory::getApplication();
232 $dbo = JFactory::getDbo();
233
234 // filter out empty filters
235 $filters = array_filter($filters);
236
237 // build fetching clauses by normalizing filter names
238 $clauses = [];
239
240 if ($filters['id_area'] ?? null) {
241 // filter by area/project ID
242 $clauses['id_area'] = [
243 'value' => (int) $filters['id_area'],
244 ];
245 } elseif (is_array($filters['id_areas'] ?? null)) {
246 // filter by area/project IDs
247 $clauses['id_area'] = [
248 'value' => array_map('intval', $filters['id_areas']),
249 ];
250 }
251
252 if ($filters['statusId'] ?? null) {
253 // filter by status(es)
254 $clauses['status_enum'] = [
255 'value' => $filters['statusId'],
256 ];
257 } else {
258 // status not specified, ignore archived tasks by default
259 $clauses['archived'] = [
260 'value' => 0,
261 ];
262 }
263
264 if ($filters['tag'] ?? null) {
265 // filter by tag requires multiple conditions
266 $qpieces = [
267 $dbo->qn('t.tags') . ' = ' . $dbo->q('[' . $filters['tag'] . ']'),
268 $dbo->qn('t.tags') . ' LIKE ' . $dbo->q('[' . $filters['tag'] . ',%'),
269 $dbo->qn('t.tags') . ' LIKE ' . $dbo->q('%,' . $filters['tag'] . ']'),
270 $dbo->qn('t.tags') . ' LIKE ' . $dbo->q('%,' . $filters['tag'] . ',%'),
271 ];
272
273 $clauses['tags'] = [
274 'instruction' => '(' . implode(' OR ', $qpieces) . ')',
275 'value' => (int) $filters['tag'],
276 ];
277 }
278
279 if ($filters['assignee'] ?? null) {
280 // filter by a single assignee ID or null (-1) for tasks not assigned to any operator
281 $clauses['assignee'] = [
282 'value' => $filters['assignee'] == -1 ? null : intval($filters['assignee']),
283 ];
284 } elseif (is_array($filters['assignees'] ?? null)) {
285 // filter by assignee IDs
286 $clauses['assignees'] = [
287 'value' => $filters['assignees'],
288 ];
289 }
290
291 if (is_numeric($filters['operator'] ?? null)) {
292 // unlike the "assignee(s)" filter, this filter will get the tasks assigned
293 // to the given operator ID, OR, those who are not yet assigned to any operator
294 // by excluding the tasks that belong to private areas
295
296 // cast filter to integer
297 $filters['operator'] = (int) $filters['operator'];
298
299 // build SQL instruction for the operator assignments
300 $instructions = [
301 $dbo->qn('ta.id_operator') . ' = ' . $filters['operator'],
302 $dbo->qn('ta.id_operator') . ' IS NULL',
303 ];
304 $instruction = '(' . implode(' OR ', array_map(function($q) {
305 return '(' . $q . ')';
306 }, $instructions)) . ')';
307
308 // get a list of private area IDs, if any
309 $privateAreaIds = VBOFactory::getTaskManager()->getPrivateAreas();
310
311 // prepend SQL instruction to exclude the private areas
312 if ($privateAreaIds) {
313 $instruction = '(' . $dbo->qn('t.id_area') . ' NOT IN (' . implode(', ', $privateAreaIds) . ') AND ' . $instruction . ')';
314 }
315
316 // set final filter
317 $clauses['operator'] = [
318 'instruction' => $instruction,
319 'value' => $filters['operator'],
320 ];
321 }
322
323 if (is_numeric($filters['id_room'] ?? null)) {
324 // cast filter to integer
325 $filters['id_room'] = (int) $filters['id_room'];
326
327 // filter by room ID or category ID
328 if ($filters['id_room'] > 0) {
329 // room ID given
330 $clauses['id_room'] = [
331 'value' => $filters['id_room'],
332 ];
333 } else {
334 // category ID given
335 $room_ids = VikBooking::getAvailabilityInstance(true)->filterRoomCategories((array) $filters['id_room']);
336 if ($room_ids) {
337 // filter by multiple room IDs
338 $clauses['id_room'] = [
339 'value' => $room_ids,
340 ];
341 }
342 }
343 } elseif (is_array($filters['id_rooms'] ?? null) && ($id_rooms = array_filter($filters['id_rooms']))) {
344 // filter by multiple room IDs
345 $clauses['id_room'] = [
346 'value' => array_values($id_rooms),
347 ];
348 }
349
350 if ($filters['id_order'] ?? null) {
351 // filter by booking ID
352 $clauses['id_order'] = [
353 'value' => (int) $filters['id_order'],
354 ];
355 } elseif ($filters['with_order'] ?? null) {
356 // filter by tasks assigned to a booking ID (NOT NULL)
357 $clauses['id_order'] = [
358 'operator' => '!=',
359 'value' => null,
360 ];
361 }
362
363 if ($filters['dates'] ?? null) {
364 // filter by date(s) by converting the local date-time to UTC
365 list($fromDt, $toDt) = $this->getFilterDatesInterval((string) $filters['dates'], $local = false, $sql = true);
366
367 if ($fromDt) {
368 // build SQL instruction
369 $instruction = $dbo->qn('t.dueon') . ' BETWEEN ' . $dbo->q($fromDt) . ' AND ' . $dbo->q($toDt);
370
371 // check if the same dates filter should be applied on the begin date
372 if ($filters['calendar'] ?? false) {
373 // modify SQL instruction to include the begin date and the finish date
374 $instructions = [
375 $instruction,
376 $dbo->qn('t.beganon') . ' BETWEEN ' . $dbo->q($fromDt) . ' AND ' . $dbo->q($toDt),
377 $dbo->qn('t.finishedon') . ' IS NOT NULL AND (' . $dbo->q($fromDt) . ' BETWEEN IFNULL(' . $dbo->qn('t.beganon') . ', ' . $dbo->qn('t.dueon') . ') AND ' . $dbo->qn('t.finishedon') . ')',
378 ];
379 $instruction = '(' . implode(' OR ', array_map(function($q) {
380 return '(' . $q . ')';
381 }, $instructions)) . ')';
382 }
383
384 // add clause
385 $clauses['dueon'] = [
386 'instruction' => $instruction,
387 'value' => $filters['dates'],
388 ];
389 }
390 }
391
392 if ($filters['future'] ?? null) {
393 // filter by due date in the future
394 $today_midnight = JFactory::getDate('now', $app->get('offset'))->modify('00:00:00')->toSql();
395 $clauses['dueon'] = [
396 'instruction' => $dbo->qn('t.dueon') . ' >= ' . $dbo->q($today_midnight),
397 'value' => $today_midnight,
398 ];
399 }
400
401 if ($filters['search'] ?? null) {
402 if (preg_match('/^id:\s?[0-9]+$/i', $filters['search'])) {
403 // search task by ID
404 $clauses['id'] = [
405 'value' => (int) preg_replace('/[^0-9]/', '', $filters['search']),
406 ];
407 } else {
408 // full-text tasks search by title and notes
409 $clauses['fulltext'] = [
410 'value' => $filters['search'],
411 ];
412 }
413 }
414
415 if ($count) {
416 // count items
417 return $this->getItems($clauses, 0, 1, ['COUNT(*)']);
418 }
419
420 // fetch items
421 return $this->getItems($clauses, $start, $lim);
422 }
423
424 /**
425 * Stores a new task record.
426 *
427 * @param array|object $record The record to store.
428 *
429 * @return int|null The new record ID or null.
430 */
431 public function save($record)
432 {
433 $app = JFactory::getApplication();
434 $dbo = JFactory::getDbo();
435
436 $taskManager = VBOFactory::getTaskManager();
437
438 $record = (object) $record;
439
440 // normalize received notes HTML; if any
441 $this->normalizeNotesHtml($record);
442
443 if (is_array(($record->tags ?? null))) {
444 // parse all tags, even custom ones, into a list of IDs
445 $record->tags = json_encode(VBOTaskModelColortag::getInstance()->parseIds($record->tags));
446 }
447
448 if (empty($record->status_enum) && !empty($record->id_area)) {
449 // fallback to the first area status enumeration found
450 $statuses = $taskManager->getStatusGroupElements(VBOTaskArea::getRecordInstance($record->id_area)->getStatuses(), $flatten = true);
451 $record->status_enum = $statuses[0]['id'];
452 }
453
454 // check due date
455 if (empty($record->dueon)) {
456 // default to current date-time because the due date cannot be empty
457 $record->dueon = JFactory::getDate('now', $app->get('offset'))->toSql();
458 } else {
459 // convert the given (and expected) local date-time to UTC
460 $record->dueon = JFactory::getDate($record->dueon, $app->get('offset'))->toSql();
461 }
462
463 // force creation date
464 $record->createdon = JFactory::getDate('now', $app->get('offset'))->toSql();
465
466 // always attempt to get and unset the assignee IDs as they do not belong to the task record
467 $assigneesList = $record->assignees ?? [];
468 unset($record->assignees);
469
470 /**
471 * Trigger event to allow third-party plugins to manipulate the task payload before it gets saved
472 */
473 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSaveTaskManagerTask', [$record, $isNewTask = true]);
474
475 // store task record
476 $dbo->insertObject('#__vikbooking_tm_tasks', $record, 'id');
477
478 /**
479 * Trigger event to allow third-party plugins to operate once the task record has been saved
480 */
481 VBOFactory::getPlatform()->getDispatcher()->trigger('onAfterSaveTaskManagerTask', [$record, $isNewTask = true]);
482
483 $taskId = ($record->id ?? null) ?: null;
484
485 if ($taskId && $assigneesList) {
486 $assignees = array_filter(array_map('intval', (array) $assigneesList));
487 foreach ($assignees as $assigneeId) {
488 $relRecord = [
489 'id_task' => $taskId,
490 'id_operator' => $assigneeId,
491 ];
492 $relRecord = (object) $relRecord;
493 $dbo->insertObject('#__vikbooking_tm_task_assignees', $relRecord, 'id');
494 }
495 }
496
497 // track the task creation
498 (new VBOTaskHistoryTracker(
499 new VBOHistoryModelDatabase(
500 new VBOTaskHistoryContext($record->id)
501 )
502 ))->track(null, $record);
503
504 // make sure the new task exists
505 if ($taskManager->statusTypeExists($record->status_enum)) {
506 // execute the extra rules that the new status should apply
507 $taskManager->getStatusTypeInstance($record->status_enum)->apply((int) $record->id);
508 }
509
510 return $taskId;
511 }
512
513 /**
514 * Updates an existing task record.
515 *
516 * @param array|object $record The record details to update.
517 *
518 * @return bool
519 */
520 public function update($record)
521 {
522 $app = JFactory::getApplication();
523 $dbo = JFactory::getDbo();
524
525 $record = (object) $record;
526
527 if (empty($record->id)) {
528 return false;
529 }
530
531 // get previous item
532 $prev = $this->getItem($record->id);
533
534 if (!$prev) {
535 throw new UnexpectedValueException('The task [' . $record->id . '] you are trying to update does not exist.', 404);
536 }
537
538 // normalize received notes HTML; if any
539 $this->normalizeNotesHtml($record);
540
541 if (is_array(($record->tags ?? null))) {
542 // parse all tags, even custom ones, into a list of IDs
543 $record->tags = json_encode(VBOTaskModelColortag::getInstance()->parseIds($record->tags));
544 }
545
546 // check due date
547 if (!empty($record->dueon)) {
548 // convert the given (and expected) local date-time to UTC
549 $record->dueon = JFactory::getDate($record->dueon, $app->get('offset'))->toSql();
550 } else {
551 // prevent the system from saving NULL dates
552 unset($record->dueon);
553 }
554
555 // check begin date
556 if (!empty($record->beganon)) {
557 // convert the given (and expected) local date-time to UTC
558 $record->beganon = JFactory::getDate($record->beganon, $app->get('offset'))->toSql();
559 } else {
560 // prevent the system from saving NULL dates
561 unset($record->beganon);
562 }
563
564 // check finish date
565 if (!empty($record->finishedon)) {
566 // convert the given (and expected) local date-time to UTC
567 $record->finishedon = JFactory::getDate($record->finishedon, $app->get('offset'))->toSql();
568 } else {
569 // prevent the system from saving NULL dates
570 unset($record->finishedon);
571 }
572
573 // always unset the creation date-time and force the modification date-time
574 unset($record->createdon);
575 $record->modifiedon = JFactory::getDate('now', $app->get('offset'))->toSql();
576
577 // always attempt to get and unset the assignee IDs as they do not belong to the task record
578 $assigneesList = $record->assignees ?? null;
579 unset($record->assignees);
580
581 /**
582 * Trigger event to allow third-party plugins to manipulate the task payload before it gets saved
583 */
584 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSaveTaskManagerTask', [$record, $isNewTask = false]);
585
586 // update task record
587 $updated = (bool) $dbo->updateObject('#__vikbooking_tm_tasks', $record, 'id');
588
589 // inject assignees again
590 $record->assignees = $assigneesList;
591
592 /**
593 * Trigger event to allow third-party plugins to operate once the task record has been saved
594 */
595 VBOFactory::getPlatform()->getDispatcher()->trigger('onAfterSaveTaskManagerTask', [$record, $isNewTask = false]);
596
597 if ($assigneesList !== null) {
598 // sanitize the assignees list
599 $assigneesList = array_filter(array_map('intval', (array) $assigneesList));
600
601 if ($assigneesList) {
602 // update task-operator relations
603
604 // get the current task-operator relations
605 $dbo->setQuery(
606 $dbo->getQuery(true)
607 ->select($dbo->qn('id_operator'))
608 ->from($dbo->qn('#__vikbooking_tm_task_assignees'))
609 ->where($dbo->qn('id_task') . ' = ' . (int) $record->id)
610 );
611
612 $prev->assignees = array_filter(array_map('intval', $dbo->loadColumn()));
613
614 // find the relations to eventually add or delete
615 $addingOperators = array_diff($assigneesList, $prev->assignees);
616 $missingOperators = array_diff($prev->assignees, $assigneesList);
617
618 foreach ($missingOperators as $operatorId) {
619 // delete task-operator relation
620 $dbo->setQuery(
621 $dbo->getQuery(true)
622 ->delete($dbo->qn('#__vikbooking_tm_task_assignees'))
623 ->where($dbo->qn('id_task') . ' = ' . (int) $record->id)
624 ->where($dbo->qn('id_operator') . ' = ' . (int) $operatorId)
625 );
626 $dbo->execute();
627 }
628
629 foreach ($addingOperators as $operatorId) {
630 // add task-operator relation
631 $relRecord = [
632 'id_task' => (int) $record->id,
633 'id_operator' => (int) $operatorId,
634 ];
635 $relRecord = (object) $relRecord;
636 $dbo->insertObject('#__vikbooking_tm_task_assignees', $relRecord, 'id');
637 }
638 } else {
639 // delete all previous task-operator relations, if any
640 $dbo->setQuery(
641 $dbo->getQuery(true)
642 ->delete($dbo->qn('#__vikbooking_tm_task_assignees'))
643 ->where($dbo->qn('id_task') . ' = ' . (int) $record->id)
644 );
645 $dbo->execute();
646 }
647 }
648
649 // track any changes
650 (new VBOTaskHistoryTracker(
651 new VBOHistoryModelDatabase(
652 new VBOTaskHistoryContext($record->id)
653 )
654 ))->track($prev, $record);
655
656 // check whether the status has changed
657 if ((new VBOTaskHistoryDetectorStatus)->hasChanged((object) $prev, (object) $record)) {
658 $taskManager = VBOFactory::getTaskManager();
659
660 // make sure the new task exists
661 if ($taskManager->statusTypeExists($record->status_enum)) {
662 // execute the extra rules that the new status should apply
663 $taskManager->getStatusTypeInstance($record->status_enum)->apply((int) $record->id);
664 }
665 }
666
667 return $updated;
668 }
669
670 /**
671 * Deletes a task record.
672 *
673 * @param array|int $id The record(s) to delete.
674 *
675 * @return bool
676 */
677 public function delete($id)
678 {
679 $dbo = JFactory::getDbo();
680
681 if (!is_array($id)) {
682 $id = (array) $id;
683 }
684
685 $id = array_map('intval', $id);
686
687 if (!$id) {
688 return false;
689 }
690
691 $dbo->setQuery(
692 $dbo->getQuery(true)
693 ->delete($dbo->qn('#__vikbooking_tm_tasks'))
694 ->where($dbo->qn('id') . ' IN (' . implode(', ', $id) . ')')
695 );
696
697 $dbo->execute();
698 $result = (bool) $dbo->getAffectedRows();
699
700 if ($result) {
701 // delete the task-operator relations
702 $dbo->setQuery(
703 $dbo->getQuery(true)
704 ->delete($dbo->qn('#__vikbooking_tm_task_assignees'))
705 ->where($dbo->qn('id_task') . ' IN (' . implode(', ', $id) . ')')
706 );
707 $dbo->execute();
708 }
709
710 return $result;
711 }
712
713 /**
714 * Given a dates filter identifier, returns the interval of dates in "Y-m-d H:i:s" or "SQL" format.
715 *
716 * @param string $dates The dates filter identifier.
717 * @param bool $local Whether to obtain dates in local or UTC timezone.
718 * @param bool $sql Whether to obtain dates in "SQL" or "Y-m-d H:i:s" format.
719 *
720 * @return array List of dates, from-to, for the interval, or array of null values.
721 */
722 public function getFilterDatesInterval(string $dates, bool $local = true, bool $sql = true)
723 {
724 $useTz = $local ? date_default_timezone_get() : JFactory::getApplication()->get('offset');
725
726 if (!strcasecmp($dates, 'today')) {
727 // filter by today's date
728 $fromDt = JFactory::getDate(date('Y-m-d 00:00:00'), $useTz);
729 $toDt = JFactory::getDate(date('Y-m-d 23:59:59'), $useTz);
730 if ($sql) {
731 return [
732 $fromDt->toSql(),
733 $toDt->toSql(),
734 ];
735 }
736 return [
737 $fromDt->format('Y-m-d H:i:s'),
738 $toDt->format('Y-m-d H:i:s'),
739 ];
740 }
741
742 if (!strcasecmp($dates, 'tomorrow')) {
743 // filter by tomorrow's date
744 $tomorrowTs = strtotime('+1 day');
745 $fromDt = JFactory::getDate(date('Y-m-d 00:00:00', $tomorrowTs), $useTz);
746 $toDt = JFactory::getDate(date('Y-m-d 23:59:59', $tomorrowTs), $useTz);
747 if ($sql) {
748 return [
749 $fromDt->toSql(),
750 $toDt->toSql(),
751 ];
752 }
753 return [
754 $fromDt->format('Y-m-d H:i:s'),
755 $toDt->format('Y-m-d H:i:s'),
756 ];
757 }
758
759 if (!strcasecmp($dates, 'yesterday')) {
760 // filter by yesterday's date
761 $yesterdayTs = strtotime('-1 day');
762 $fromDt = JFactory::getDate(date('Y-m-d 00:00:00', $yesterdayTs), $useTz);
763 $toDt = JFactory::getDate(date('Y-m-d 23:59:59', $yesterdayTs), $useTz);
764 if ($sql) {
765 return [
766 $fromDt->toSql(),
767 $toDt->toSql(),
768 ];
769 }
770 return [
771 $fromDt->format('Y-m-d H:i:s'),
772 $toDt->format('Y-m-d H:i:s'),
773 ];
774 }
775
776 if (!strcasecmp($dates, 'week')) {
777 // filter by this week's date
778 $fromDt = JFactory::getDate(date('Y-m-d 00:00:00'), $useTz);
779 $toDt = JFactory::getDate(date('Y-m-d 23:59:59', strtotime('+1 week')), $useTz);
780 if ($sql) {
781 return [
782 $fromDt->toSql(),
783 $toDt->toSql(),
784 ];
785 }
786 return [
787 $fromDt->format('Y-m-d H:i:s'),
788 $toDt->format('Y-m-d H:i:s'),
789 ];
790 }
791
792 if (!strcasecmp($dates, 'month')) {
793 // filter by this month's date
794 $fromDt = JFactory::getDate(date('Y-m-01 00:00:00'), $useTz);
795 $toDt = JFactory::getDate(date('Y-m-t 23:59:59'), $useTz);
796 if ($sql) {
797 return [
798 $fromDt->toSql(),
799 $toDt->toSql(),
800 ];
801 }
802 return [
803 $fromDt->format('Y-m-d H:i:s'),
804 $toDt->format('Y-m-d H:i:s'),
805 ];
806 }
807
808 if (preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}\s?:\s?[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $dates)) {
809 // filter by custom range of dates
810 $parts = explode(':', $dates);
811 $fromDt = JFactory::getDate(date('Y-m-d 00:00:00', strtotime(trim($parts[0]))), $useTz);
812 $toDt = JFactory::getDate(date('Y-m-d 23:59:59', strtotime(trim($parts[1]))), $useTz);
813 if ($sql) {
814 return [
815 $fromDt->toSql(),
816 $toDt->toSql(),
817 ];
818 }
819 return [
820 $fromDt->format('Y-m-d H:i:s'),
821 $toDt->format('Y-m-d H:i:s'),
822 ];
823 }
824
825 // unrecognized dates filter
826 return [null, null];
827 }
828
829 /**
830 * Updates a checklist element of a specific task.
831 *
832 * @param int $taskId The ID of the task to update.
833 * @param int $n The N-th checkbox to update.
834 * @param bool|null $status The status to assign. Null to toggle the current status.
835 *
836 * @return void
837 */
838 public function updateChecklist(int $taskId, int $n, ?bool $status = null)
839 {
840 // get updated item
841 $task = $this->getItem($taskId);
842
843 if (!$task) {
844 throw new UnexpectedValueException('The task [' . $taskId . '] you are trying to update does not exist.', 404);
845 }
846
847 $index = 0;
848
849 // scan the notes HTML in search of the element to update
850 $task->notes = preg_replace_callback(
851 // take all the ULs holding the data-checked attribute
852 "/<ul[^>]+data-checked=\"(true|false)\"[^>]*>(.*?)<\/ul>/s", function($matches) use ($n, &$index, $status) {
853 // make sure the matches the requested index
854 if (++$index === $n) {
855 if ($status === null) {
856 // toggle the current status
857 $status = $matches[1] !== 'true';
858 }
859
860 // replace the current status with the new one
861 $matches[0] = preg_replace("/data-checked=\"(true|false)\"/", 'data-checked="' . ($status ? 'true' : 'false') . '"', $matches[0]);
862 }
863
864 return $matches[0];
865 },
866 $task->notes
867 );
868
869 // finally update the task
870 $this->update([
871 'id' => $task->id,
872 'notes' => $task->notes,
873 ]);
874 }
875
876 /**
877 * Normalizes the HTML content generated by the preferred WYSIWYG editor.
878 *
879 * @param object $record
880 *
881 * @return void
882 */
883 private function normalizeNotesHtml(object $task) {
884 if (empty($task->notes)) {
885 // task missing, nothing to normalize
886 return;
887 }
888
889 /**
890 * Quill editor supports the checklist feature. However, instead of having the checked status
891 * on the LIs, Quill groups the elements per status under the same UL. Therefore we need to
892 * refactor the following structure:
893 *
894 * ```html
895 * <ul data-checked="true"><li>a</li><li>b</li></ul>
896 * <ul data-checked="false"><li>c</li></ul>
897 * <ul data-checked="true"><li>d</li></ul>
898 * ```
899 *
900 * into this one:
901 *
902 * ```html
903 * <ul data-checked="true"><li>a</li></ul>
904 * <ul data-checked="true"><li>b</li></ul>
905 * <ul data-checked="false"><li>c</li></ul>
906 * <ul data-checked="true"><li>d</li></ul>
907 * ```
908 */
909 $task->notes = preg_replace_callback(
910 // take all the ULs holding the data-checked attribute
911 "/<ul[^>]+data-checked=\"(true|false)\"[^>]*>(.*?)<\/ul>\s*/s",
912 function($ulMatches) {
913 // extract the current status and all the LIs
914 $checked = $ulMatches[1];
915 $lis = $ulMatches[2];
916
917 // wrap all the LIs into different ULs
918 return preg_replace_callback(
919 "/\s*<li[^>]*>(.*?)<\/li>\s*/s",
920 function($liMatches) use ($checked) {
921 return '<ul data-checked="' . $checked . '"><li>' . $liMatches[1] . '</li></ul>';
922 },
923 $lis
924 );
925 },
926 $task->notes
927 );
928 }
929 }
930