PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.13
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.13
2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 1.45 All 41 releases
fluent-boards / app / Services / TaskService.php

TaskService.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 1.13, at app/Services/TaskService.php

722 lines 23.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Services;
4
5 use FluentBoards\App\Services\Constant;
6 use FluentBoardsPro\App\Models\Attachment;
7 use FluentBoards\App\Models\Stage;
8 use FluentBoards\App\Models\Task;
9 use FluentBoards\App\Models\Board;
10 use FluentBoards\App\Models\TaskMeta;
11 use FluentBoards\App\Models\Activity;
12 use FluentBoards\App\Models\BoardTerm;
13 use FluentBoards\Framework\Support\Arr;
14 use FluentRoadmap\App\Models\IdeaReaction;
15
16 class TaskService
17 {
18 public function createTask($data, $boardId)
19 {
20 $board = Board::select('id', 'type')->find($boardId);
21
22 if (!$board) {
23 throw new \Exception(__("Board doesn't exists", 'fluent-boards'));
24 }
25
26 $stage = Stage::find($data['stage_id']);
27 if (!$stage) {
28 throw new \Exception(__("Stage doesn't exists", 'fluent-boards'));
29 }
30
31 $data['status'] = $stage->defaultTaskStatus();
32
33 if ($board->type == 'roadmap') {
34 $current_user = wp_get_current_user();
35 $settingData = array(
36 'integration_type' => 'feature',
37 'logo' => '',
38 'author' => [
39 'email' => $current_user->user_email // email of who posted this feature
40 ],
41 );
42 $data['settings'] = $settingData;
43 $data['type'] = 'roadmap';
44 }
45
46 $providerPosition = Arr::get($data, 'position');
47
48 $data['position'] = $this->getLastPositionOfTasks($stage->id);
49
50 $data['board_id'] = $boardId;
51
52 $data = array_filter($data);
53 $task = (new Task())->createTask($data);
54 if (isset($data['is_template']) && $data['is_template'] == 'yes') {
55 $task->updateMeta(Constant::IS_TASK_TEMPLATE, $data['is_template']);
56 }
57
58 if ($providerPosition) {
59 $task->moveToNewPosition($providerPosition);
60 }
61
62 // $this->taskCreatedAction($task);
63 $this->loadWithRelations($task, ['assignees', 'labels', 'board']);
64
65 return $task;
66 }
67
68 public function loadWithRelations($task, $relations)
69 {
70 if (!is_array($relations)) {
71 return $task;
72 }
73 $task->load($relations); // $relations = ['assignees', 'board'] in this case
74 $task->isOverdue = $task->isOverdue();
75
76 return $task;
77 }
78
79 public function getTasksForBoards($filters = ['overdue', 'upcoming'], $limit = 5, $task_ids = [])
80 {
81 $overDue = $this->getTasksForBoardsByCategory('overdue', $limit, $task_ids);
82 $completed = $this->getTasksForBoardsByCategory('completed', $limit, $task_ids);
83 $upcoming = $this->getTasksForBoardsByCategory('upcoming', $limit, $task_ids);
84
85
86 return [
87 'overdue' => $overDue ?? [],
88 'upcoming' => $upcoming ?? [],
89 'completed' => $completed ?? []
90 ];
91 }
92
93 public function getTasksForBoardsByCategory($category, $limit, $taskIds)
94 {
95 unset($taskQuery);
96 $taskQuery = Task::whereIn('id', $taskIds)
97 ->with(['assignees', 'board', 'stage'])
98 ->whereNull('archived_at')
99 ->where('parent_id', null)
100 ->orderBy('due_at', 'ASC');
101
102 if ('overdue' == $category) {
103 $taskQuery->overdue();
104 } elseif ('upcoming' == $category) {
105 $taskQuery->upcoming();
106 } elseif ('upcoming_no_duedate' == $category) {
107 $taskQuery->whereNull('due_at');
108 } elseif ('completed' == $category) {
109 $taskQuery->where('status', 'closed');
110 } else {
111 return [];
112 }
113
114 $tasks = $taskQuery->take($limit)->get();
115
116 return $tasks->toArray();
117 }
118
119 /*
120 * TODO: Refactor this function - For me.
121 */
122 public function updateTaskProperty($col, $value, $task)
123 {
124 $oldTask = clone $task; // normal assigning won't work here. because objects are passed by reference in php
125 $validColumns = [
126 'board_id',
127 'task_type',
128 'reminder_type',
129 'remind_at',
130 'log_minutes',
131 'settings'
132 ];
133
134 if (in_array($col, $validColumns) && $task->{$col} != $value) {
135 $task->{$col} = $value;
136 $task->save();
137 // do_action('fluent_boards/task_prop_changed', $col, $task, $oldTask);
138 } elseif ('assignees' == $col) {
139 if (is_array($value)) {
140 foreach ($value as $id) {
141 $this->updateAssignee($id, $task);
142 }
143 } else {
144 $this->updateAssignee($value, $task);
145 }
146
147 } elseif ('crm_contact_id' == $col) {
148 $this->updateAssociate($value, $task);
149 } elseif ('archived_at' == $col) {
150 $this->updateArchive($value, $task);
151 } elseif ('status' == $col) {
152 $this->updateStatus($value, $task);
153 } elseif ('parent_id' == $col) {
154 $this->updateParent($value, $task);
155 } elseif ('title' == $col) {
156 $this->updateTitle($col, $value, $task, $oldTask);
157 } elseif ('description' == $col) {
158 $this->updateDescription($col, $value, $task, $oldTask);
159 } elseif ($col == 'due_at') {
160 $this->updateDueDate($value, $task);
161 } elseif ($col == 'started_at') {
162 $this->updateStartedDate($value, $task);
163 } elseif ($col == 'priority') {
164 $this->updatePriority($value, $task);
165 } elseif ($col == 'is_watching') {
166 $this->updateObservationOfCurrentUser($value, $task);
167 } elseif ($col == 'last_completed_at') {
168 $isClosed = $value == 'true' || $value === true;
169 if ($isClosed) {
170 $task = $task->close();
171 } else {
172 $task = $task->reopen();
173 }
174 $task->save();
175 } elseif ($col == 'attachment_count') {
176 $settings = $task->settings;
177 $settings['attachment_count'] = $task->attachments()->count();
178 $task->settings = $settings;
179 $task->save();
180 } elseif ($col == 'subtask_count') {
181 $settings = $task->settings;
182 $subtasksCount = Task::where('parent_id', $task->id)->count();
183 $settings['subtask_count'] = $subtasksCount;
184 $task->settings = $settings;
185 $task->save();
186 } elseif ($col == 'is_template') {
187 if (defined('FLUENT_BOARDS_PRO')) {
188 $task->updateMeta(Constant::IS_TASK_TEMPLATE, $value);
189 }
190 }
191
192 return $task;
193 }
194
195 public function updateAssignee($payloadAssigneeId, $task)
196 {
197 $operation = $task->addOrRemoveAssignee($payloadAssigneeId);
198 $task->load('assignees');
199 $task->updated_at = current_time('mysql');
200
201 $task->save();
202
203 if ($operation == 'added') {
204 if ((new NotificationService())->checkIfEmailEnable($payloadAssigneeId, Constant::BOARD_EMAIL_TASK_ASSIGN, $task->board_id)) {
205 $this->sendMailAfterTaskModify('add_assignee', $payloadAssigneeId, $task->id);
206 }
207 // $assigneeIdsToSendEmail = $this->filterAssigneeToSendEmail($task, $idArray, Constant::BOARD_EMAIL_TASK_ASSIGN);
208 // $this->sendMailAfterAddAssignees($assigneeIdsToSendEmail, $task->id);
209 do_action('fluent_boards/task_assignee_changed', $task, $payloadAssigneeId, $operation);
210 } else {
211 if ((new NotificationService())->checkIfEmailEnable($payloadAssigneeId, Constant::BOARD_EMAIL_REMOVE_FROM_TASK, $task->board_id)) {
212 $this->sendMailAfterTaskModify('remove_assignee', $payloadAssigneeId, $task->id);
213 }
214 do_action('fluent_boards/task_assignee_changed', $task, $payloadAssigneeId, $operation);
215 }
216
217 }
218
219 // public function filterAssigneeToSendEmail($task, $newAssigneeIds, $purpose)
220 // {
221 // $toSendEmail = array();
222 // foreach ($newAssigneeIds as $assigneeId) {
223 // if ((new NotificationService())->checkIfEmailEnabled($task->board_id, $assigneeId, $purpose)) {
224 // $toSendEmail[] = $assigneeId;
225 // }
226 // }
227 // return $toSendEmail;
228 // }
229
230 // public function defaultWatchingTaskByNewUsers($task, $newIds)
231 // {
232 // foreach ($newIds as $newId) {
233 // if (!$task->watchers->contains($newId)) {
234 // $task->watchers()->attach(
235 // $newId,
236 // [
237 // 'object_type' => Constant::OBJECT_TYPE_USER_TASK_WATCH,
238 // ]
239 // );
240 // }
241 // }
242 // }
243
244 // public function checkIfAnybodyRemovedFromTask($newAssigneeIds, $oldAssigneeIds, $task)
245 // {
246 // $removedAssignees = array_diff($oldAssigneeIds, $newAssigneeIds);
247 // $this->sendMailAfterTaskModify('removed_from_task', $removedAssignees, $task->id);
248 // dd($removedAssignees);
249 // }
250
251 private function updateAssociate($value, $task)
252 {
253 // if task has no crm contact and got value null then return current task
254 if (($task->crm_contact_id == null || $task->crm_contact_id == 0) && $value == null) {
255 return $task;
256 }
257
258 $oldAssociateId = $task->crm_contact_id;
259 $task->crm_contact_id = $value;
260 $task->save();
261 $task->contact = Task::lead_contact($task->crm_contact_id);
262 do_action('fluent_boards/contact_added_to_task', $task);
263 do_action('fluent_boards/associate_user_add_change_remove_activity', $oldAssociateId, $task->crm_contact_id, $task->id);
264 }
265
266 private function updateArchive($value, $task)
267 {
268 if ($value != null) {
269 $task->position = 0;
270 } else {
271 $task->moveToNewPosition(1);
272 }
273 $task->archived_at = $value == null ? null : current_time('mysql');
274 $task->save();
275 do_action('fluent_boards/board_task_archived', $task);
276 $wathersToSendEmail = (new NotificationService())->filterAssigneeToSendEmail($task->id, Constant::BOARD_EMAIL_TASK_ARCHIVE);
277 $this->sendMailAfterTaskModify('task_archived', $wathersToSendEmail, $task->id);
278 }
279
280 private function updateStatus($value, $task)
281 {
282 if ($value == 'closed') {
283 $task = $task->close();
284 } else {
285 $task = $task->reopen();
286 }
287
288 do_action('fluent_boards/task_completed_activity', $task, $value);
289 }
290
291 private function updateParent($value, $task)
292 {
293 $task->parent_id = $value;
294 $task->save();
295 }
296
297 private function updateTitle($col, $value, $task, $oldTask)
298 {
299 $task->title = $value;
300 $task->save();
301 do_action('fluent_boards/task_content_updated', $task, $col, $oldTask);
302 }
303
304 private function updateDescription($col, $value, $task, $oldTask)
305 {
306 $task->description = $value;
307 $task->save();
308 do_action('fluent_boards/task_content_updated', $task, $col, $oldTask);
309 }
310
311 private function updateDueDate($value, $task)
312 {
313 $oldValue = $task->due_at;
314 $value = $this->filterNullDate($value);
315 $task->due_at = $value;
316 $task->save();
317
318 $task = $task->reopen();
319
320 do_action('fluent_boards/task_date_changed', $task, $oldValue, 'Due Date');
321
322 $wathersToSendEmail = (new NotificationService())->filterAssigneeToSendEmail($task->id, Constant::BOARD_EMAIL_DUE_DATE_CHANGE);
323 $this->sendMailAfterTaskModify('due_date_update', $wathersToSendEmail, $task->id);
324 }
325
326 private function updateStartedDate($value, $task)
327 {
328 $oldValue = $task->started_at;
329 $value = $this->filterNullDate($value);
330 $task->started_at = $value;
331 $task->save();
332
333 do_action('fluent_boards/task_date_changed', $task, $oldValue, 'Start Date');
334
335 }
336
337 private function updatePriority($value, $task)
338 {
339 $oldPriority = $task->priority;
340 $task->priority = $value;
341 $task->save();
342 do_action('fluent_boards/task_priority_changed', $task, $oldPriority);
343 }
344
345 public function updateObservationOfCurrentUser($value, $task)
346 {
347 $currentUserId = get_current_user_id();
348
349 if ($value == 'stop') {
350 $task->watchers()->detach($currentUserId);
351 } else {
352 $task->watchers()->syncWithoutDetaching([$currentUserId => ['object_type' => Constant::OBJECT_TYPE_USER_TASK_WATCH]]);
353 }
354 $task->updated_at = current_time('mysql');
355 $task->save();
356
357 if ($value == 'stop') {
358 $task->is_watching = false;
359 } else {
360 $task->is_watching = true;
361 }
362 }
363
364 public function taskCoverPhotoUpdate($taskId, $imagePath)
365 {
366 $task = Task::find($taskId);
367 if (!$task) {
368 return null;
369 }
370
371 $settings = unserialize($task->settings);
372
373 $settings['logo'] = $imagePath;
374 $task->settings = serialize($settings);
375 $task->save();
376
377 return $task;
378 }
379
380 public function taskStatusUpdate($taskId, $integrationType)
381 {
382 $task = Task::find($taskId);
383 if (!$task) {
384 return null;
385 }
386
387 $settings = $task->settings;
388 $settings['integration_type'] = $integrationType;
389 $task->settings = serialize($settings);
390 $task->save();
391
392 return $task;
393 }
394
395 public function assignYourselfInTask($boardId, $taskId)
396 {
397 $task = Task::find($taskId);
398 $authUserId = get_current_user_id();
399
400 $boardService = new BoardService();
401 if (!$boardService->isAlreadyMember($boardId, $authUserId)) {
402 $boardService->addMembersInBoard($boardId, $authUserId);
403 }
404
405 $task->addOrRemoveAssignee($authUserId);
406
407 $task->load('assignees');
408
409 return $task;
410 }
411
412 public function detachYourselfFromTask($boardId, $taskId)
413 {
414 $task = Task::find($taskId);
415 $task->addOrRemoveAssignee(get_current_user_id());
416 $task->load('assignees');
417
418 return $task;
419 }
420
421 public function deleteTask($task)
422 {
423 $deleted = $task->delete();
424
425 if ($deleted) {
426
427 //task assignees watchers removed
428 $task->watchers()->detach();
429 $task->assignees()->detach();
430
431 //removing all task related notifications
432 $task->notifications()->delete();
433
434 //task labels removed
435 $task->labels()->detach();
436
437 do_action('fluent_boards/task_deleted', $task);
438 TaskMeta::where('task_id', $task->id)->delete();
439 }
440 }
441
442 public function filterNullDate($date)
443 {
444 if ('0000-00-00 00:00:00' == $date || false === strtotime($date)) {
445 return null;
446 }
447 return $date;
448 }
449
450 // this is invoked when task is moved to another board
451
452 /**
453 * @throws \Exception
454 */
455 public function changeBoardByTask($task, $targetBoardId)
456 {
457 if ($task->board_id == $targetBoardId) {
458 return $task;
459 }
460
461 $oldBoard = Board::find($task->board_id);
462
463 $newBoard = Board::find($targetBoardId);
464 if (!$newBoard) {
465 throw new \Exception('Invalid board id', 400);
466 }
467 $task->board_id = $targetBoardId;
468 $task->save();
469 //delete labels of that task because labels have board dependencies
470 $task->labels()->detach();
471
472 do_action('fluent_boards/task_moved_from_board', $task, $oldBoard, $newBoard);
473
474 return $task;
475 }
476
477
478 public function getIdeaVoteStatistics($taskId)
479 {
480 $reactionTypes = [
481 [
482 'label' => 'Upvote',
483 'type' => 'upvote'
484 ],
485 [
486 'label' => 'Downvote',
487 'type' => 'downvote'
488 ]
489 ];
490
491 $reactionCounts = [];
492
493 foreach ($reactionTypes as $reactionType) {
494 $count = IdeaReaction::where('object_id', $taskId)
495 ->where('object_type', 'idea')
496 ->where('type', $reactionType['type'])
497 ->count();
498
499 $reactionCounts[] = [
500 'label' => $reactionType['label'],
501 'type' => $reactionType['type'],
502 'count' => $count
503 ];
504 }
505
506 return $reactionCounts;
507 }
508
509
510 /**
511 * Summary of getArchivedOrCompletedTasks
512 * this function will return completd tasks or archived tasks based on users input and also can search by name
513 * @param mixed $data
514 * @param mixed $taskType
515 * @return mixed
516 * @throws \Exception
517 */
518 public function getArchivedTasks($data, $boardId)
519 {
520 $per_page = isset($data['per_page']) ? $data['per_page'] : 25;
521 $page = isset($data['page']) ? $data['page'] : 1;
522 $tasksQuery = Task::where('board_id', $boardId)->whereNotNull('archived_at');
523 if (isset($data['searchInput'])) {
524 $tasksQuery = $tasksQuery->where('title', 'LIKE', '%' . $data['searchInput'] . '%');
525 }
526
527 // if board_id is not passed then throw an exception
528 if (!$boardId) {
529 throw new \Exception('Board id is required', 'fluent-boards');
530 }
531
532 return $tasksQuery->orderBy('created_at', 'DESC')->with('assignees')->paginate($per_page, ['*'], 'page', $page);
533 }
534
535 public function sendMailAfterTaskModify($column, $assigneeIds, $taskId)
536 {
537 $current_user_id = get_current_user_id();
538 /* this will run in background as soon as possible */
539 /* sending Model or Model Instance won't work here */
540
541 as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_'.$column, [$taskId, $assigneeIds, $current_user_id], 'fluent-boards');
542 }
543
544 public function getStageByTask($task_id)
545 {
546 $task = Task::find($task_id);
547 return $task->stage;
548 }
549
550 public function moveTaskToNextStage($task_id)
551 {
552 $task = Task::findOrFail($task_id);
553
554 $oldStage = $task->stage;
555
556 $nextStage = Stage::where('board_id', $task->board_id)
557 ->where('position', '>', $oldStage->position)
558 ->orderBy('position', 'ASC')
559 ->first();
560
561 if (!$nextStage) {
562 return $task;
563 }
564
565 if ($nextStage->defaultTaskStatus() == 'closed' && $task->status != 'closed') {
566 $task->status = 'closed';
567 if (!$task->last_completed_at) {
568 $task->last_completed_at = current_time('mysql');
569 }
570 }
571
572 $task->stage_id = $nextStage->id;
573 $task->save();
574
575 $task->load(['board', 'stage', 'attachments']);
576
577 $task = $this->loadNextStage($task);
578
579 return $task;
580 }
581
582 public function loadNextStage($task)
583 {
584 $stage = $task->stage;
585 $nextStage = Stage::where('board_id', $task->board_id)
586 ->where('position', '>', $stage->position)
587 ->orderBy('position', 'ASC')
588 ->first();
589
590 $task->nextStage = $nextStage ? $nextStage->title : null;
591 return $task;
592 }
593
594 public function getActivities($taskId, $perPage, $filter = 'newest')
595 {
596 $activityQuery = Activity::where('object_id', $taskId)
597 ->where('object_type', Constant::ACTIVITY_TASK);
598 if ($filter == 'newest') {
599 $activityQuery = $activityQuery->latest();
600 } else if ($filter == 'oldest') {
601 $activityQuery = $activityQuery->oldest();
602 }
603 return $activityQuery->with('user')->paginate($perPage);
604 }
605
606 public function getLastOneMinuteUpdatedTasks($boardId, $lastUpdated = null)
607 {
608 if (!$lastUpdated) {
609 $lastUpdated = gmdate('Y-m-d H:i:s', current_time('timestamp') - 60);
610 }
611
612 $tasks = Task::query()
613 ->where([
614 'board_id' => $boardId,
615 'parent_id' => null,
616 ])
617 ->where('updated_at', '>', $lastUpdated)
618 ->with(['assignees', 'labels', 'watchers'])
619 ->orderBy('due_at', 'ASC')
620 ->get();
621
622 foreach ($tasks as $task) {
623 $task->isOverdue = $task->isOverdue();
624 $task->isUpcoming = $task->upcoming();
625 $task->is_watching = $task->isWatching();
626 $task->contact = Task::lead_contact($task->crm_contact_id);
627 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
628 $task->watchers = Helper::sanitizeUserCollections($task->watchers);
629 }
630 return $tasks;
631 }
632
633 public function getLastPositionOfTasks($stage_id)
634 {
635 $lastPosition = Task::query()
636 ->where('stage_id', $stage_id)
637 ->where('parent_id', null)
638 ->whereNull('archived_at')
639 ->orderBy('position', 'desc')
640 ->pluck('position')
641 ->first();
642
643 return $lastPosition + 1;
644 }
645
646 public function getAssociatedTasks($associatedId)
647 {
648 $tasks = Task::query()
649 ->where('crm_contact_id', $associatedId)
650 ->with(['board', 'stage', 'assignees', 'labels', 'watchers',])
651 ->orderBy('due_at', 'ASC')
652 ->get();
653
654 foreach ($tasks as $task) {
655 $task->isOverdue = $task->isOverdue();
656 $task->isUpcoming = $task->upcoming();
657 $task->contact = Task::lead_contact($task->crm_contact_id);
658 $task->is_watching = $task->isWatching();
659
660 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
661 $task->watchers = Helper::sanitizeUserCollections($task->watchers);
662
663 $subTasks = Task::query()
664 ->where('parent_id', $task->id)
665 ->with(['assignees'])
666 ->whereNull('archived_at')
667 ->orderBy('position', 'ASC')
668 ->get();
669
670 foreach ($subTasks as $subTask) {
671 $subTask->assignees = Helper::sanitizeUserCollections($subTask->assignees);
672 }
673
674 $task->subtasks = $subTasks;
675 }
676
677 return $tasks;
678 }
679
680 public function copyTasks($boardId, $stageMap, $newBoard)
681 {
682 $allActiveTasks = Task::where('board_id', $boardId)->whereNull('archived_at')->get();
683 $taskMap = [];
684 foreach ($allActiveTasks as $task) {
685 $newTask = array();
686 $newTask['title'] = $task->title;
687 $newTask['parent_id'] = $task->parent_id ? $taskMap[$task->parent_id] : null;
688 $newTask['description'] = $task->description;
689 $newTask['board_id'] = $newBoard->id;
690 $newTask['stage_id'] = $stageMap[$task->stage_id];
691 $newTask['status'] = $task->status;
692 $newTask['priority'] = $task->priority;
693 $newTask['position'] = $task->position;
694 $newTask['due_at'] = $task->due_at;
695 $newTask = Task::create($newTask);
696 if(!$task->parent_id){
697 $taskMap[$task['id']] = $newTask->id;
698 } else {
699 $this->subtaskCountUpdate($newTask->parent_id);
700 }
701 }
702
703 //initiate task count
704 $totalTasks = sizeof($allActiveTasks);
705 $board = Board::findOrFail($newBoard->id);
706 $settings = [];
707 $settings['tasks_count'] = $totalTasks;
708 $board->settings = $settings;
709 $board->save();
710 }
711
712 private function subtaskCountUpdate($taskId){
713 $parentTask = Task::findOrFail($taskId);
714 $settings = $parentTask->settings;
715 $settings['subtask_count'] = (int)($settings['subtask_count'] ?? 0) + 1;
716 $parentTask->settings = $settings;
717 $parentTask->save();
718 }
719
720
721 }
722