PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.11
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.11
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.11, at app/Services/TaskService.php

716 lines 23.1 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 FluentBoardsPro\App\Models\Attachment;
6 use FluentBoards\App\Models\Stage;
7 use FluentBoards\App\Models\Task;
8 use FluentBoards\App\Models\Board;
9 use FluentBoards\App\Models\TaskMeta;
10 use FluentBoards\App\Models\Activity;
11 use FluentBoards\App\Models\BoardTerm;
12 use FluentBoards\App\Services\Constant;
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);
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 $task->updateMeta(Constant::IS_TASK_TEMPLATE, $value);
188 }
189
190 return $task;
191 }
192
193 public function updateAssignee($payloadAssigneeId, $task)
194 {
195 $operation = $task->addOrRemoveAssignee($payloadAssigneeId);
196 $task->load('assignees');
197 $task->updated_at = current_time('mysql');
198
199 $task->save();
200
201 if ($operation == 'added') {
202 if ((new NotificationService())->checkIfEmailEnable($payloadAssigneeId, Constant::BOARD_EMAIL_TASK_ASSIGN, $task->board_id)) {
203 $this->sendMailAfterTaskModify('add_assignee', $payloadAssigneeId, $task->id);
204 }
205 // $assigneeIdsToSendEmail = $this->filterAssigneeToSendEmail($task, $idArray, Constant::BOARD_EMAIL_TASK_ASSIGN);
206 // $this->sendMailAfterAddAssignees($assigneeIdsToSendEmail, $task->id);
207 do_action('fluent_boards/task_assignee_changed', $task, $payloadAssigneeId, $operation);
208 } else {
209 if ((new NotificationService())->checkIfEmailEnable($payloadAssigneeId, Constant::BOARD_EMAIL_REMOVE_FROM_TASK, $task->board_id)) {
210 $this->sendMailAfterTaskModify('remove_assignee', $payloadAssigneeId, $task->id);
211 }
212 do_action('fluent_boards/task_assignee_changed', $task, $payloadAssigneeId, $operation);
213 }
214
215 }
216
217 // public function filterAssigneeToSendEmail($task, $newAssigneeIds, $purpose)
218 // {
219 // $toSendEmail = array();
220 // foreach ($newAssigneeIds as $assigneeId) {
221 // if ((new NotificationService())->checkIfEmailEnabled($task->board_id, $assigneeId, $purpose)) {
222 // $toSendEmail[] = $assigneeId;
223 // }
224 // }
225 // return $toSendEmail;
226 // }
227
228 // public function defaultWatchingTaskByNewUsers($task, $newIds)
229 // {
230 // foreach ($newIds as $newId) {
231 // if (!$task->watchers->contains($newId)) {
232 // $task->watchers()->attach(
233 // $newId,
234 // [
235 // 'object_type' => Constant::OBJECT_TYPE_USER_TASK_WATCH,
236 // ]
237 // );
238 // }
239 // }
240 // }
241
242 // public function checkIfAnybodyRemovedFromTask($newAssigneeIds, $oldAssigneeIds, $task)
243 // {
244 // $removedAssignees = array_diff($oldAssigneeIds, $newAssigneeIds);
245 // $this->sendMailAfterTaskModify('removed_from_task', $removedAssignees, $task->id);
246 // dd($removedAssignees);
247 // }
248
249 private function updateAssociate($value, $task)
250 {
251 // if task has no crm contact and got value null then return current task
252 if (($task->crm_contact_id == null || $task->crm_contact_id == 0) && $value == null) {
253 return $task;
254 }
255
256 $oldAssociateId = $task->crm_contact_id;
257 $task->crm_contact_id = $value;
258 $task->save();
259 $task->contact = Task::lead_contact($task->crm_contact_id);
260 do_action('fluent_boards/contact_added_to_task', $task);
261 do_action('fluent_boards/associate_user_add_change_remove_activity', $oldAssociateId, $task->crm_contact_id, $task->id);
262 }
263
264 private function updateArchive($value, $task)
265 {
266 if ($value != null) {
267 $task->position = 0;
268 } else {
269 $task->moveToNewPosition(1);
270 }
271 $task->archived_at = $value == null ? null : current_time('mysql');
272 $task->save();
273 do_action('fluent_boards/board_task_archived', $task);
274 $wathersToSendEmail = (new NotificationService())->filterAssigneeToSendEmail($task->id, Constant::BOARD_EMAIL_TASK_ARCHIVE);
275 $this->sendMailAfterTaskModify('task_archived', $wathersToSendEmail, $task->id);
276 }
277
278 private function updateStatus($value, $task)
279 {
280 if ($value == 'closed') {
281 $task = $task->close();
282 } else {
283 $task = $task->reopen();
284 }
285
286 do_action('fluent_boards/task_completed_activity', $task, $value);
287 }
288
289 private function updateParent($value, $task)
290 {
291 $task->parent_id = $value;
292 $task->save();
293 }
294
295 private function updateTitle($col, $value, $task, $oldTask)
296 {
297 $task->title = $value;
298 $task->save();
299 do_action('fluent_boards/task_content_updated', $task, $col, $oldTask);
300 }
301
302 private function updateDescription($col, $value, $task)
303 {
304 $task->description = $value;
305 $task->save();
306 do_action('fluent_boards/task_content_updated', $task, $col, null);
307 }
308
309 private function updateDueDate($value, $task)
310 {
311 $oldValue = $task->due_at;
312 $value = $this->filterNullDate($value);
313 $task->due_at = $value;
314 $task->save();
315
316 $task = $task->reopen();
317
318 do_action('fluent_boards/task_date_changed', $task, $oldValue, 'Due Date');
319
320 $wathersToSendEmail = (new NotificationService())->filterAssigneeToSendEmail($task->id, Constant::BOARD_EMAIL_DUE_DATE_CHANGE);
321 $this->sendMailAfterTaskModify('due_date_update', $wathersToSendEmail, $task->id);
322 }
323
324 private function updateStartedDate($value, $task)
325 {
326 $oldValue = $task->started_at;
327 $value = $this->filterNullDate($value);
328 $task->started_at = $value;
329 $task->save();
330
331 do_action('fluent_boards/task_date_changed', $task, $oldValue, 'Start Date');
332
333 }
334
335 private function updatePriority($value, $task)
336 {
337 $oldPriority = $task->priority;
338 $task->priority = $value;
339 $task->save();
340 do_action('fluent_boards/task_priority_changed', $task, $oldPriority);
341 }
342
343 public function updateObservationOfCurrentUser($value, $task)
344 {
345 $currentUserId = get_current_user_id();
346
347 if ($value == 'stop') {
348 $task->watchers()->detach($currentUserId);
349 } else {
350 $task->watchers()->syncWithoutDetaching([$currentUserId => ['object_type' => Constant::OBJECT_TYPE_USER_TASK_WATCH]]);
351 }
352 $task->updated_at = current_time('mysql');
353 $task->save();
354
355 if ($value == 'stop') {
356 $task->is_watching = false;
357 } else {
358 $task->is_watching = true;
359 }
360 }
361
362 public function taskCoverPhotoUpdate($taskId, $imagePath)
363 {
364 $task = Task::find($taskId);
365 if (!$task) {
366 return null;
367 }
368
369 $settings = unserialize($task->settings);
370
371 $settings['logo'] = $imagePath;
372 $task->settings = serialize($settings);
373 $task->save();
374
375 return $task;
376 }
377
378 public function taskStatusUpdate($taskId, $integrationType)
379 {
380 $task = Task::find($taskId);
381 if (!$task) {
382 return null;
383 }
384
385 $settings = $task->settings;
386 $settings['integration_type'] = $integrationType;
387 $task->settings = serialize($settings);
388 $task->save();
389
390 return $task;
391 }
392
393 public function assignYourselfInTask($boardId, $taskId)
394 {
395 $task = Task::find($taskId);
396 $authUserId = get_current_user_id();
397
398 $boardService = new BoardService();
399 if (!$boardService->isAlreadyMember($boardId, $authUserId)) {
400 $boardService->addMembersInBoard($boardId, $authUserId);
401 }
402
403 $task->addOrRemoveAssignee($authUserId);
404
405 $task->load('assignees');
406
407 return $task;
408 }
409
410 public function detachYourselfFromTask($boardId, $taskId)
411 {
412 $task = Task::find($taskId);
413 $task->addOrRemoveAssignee(get_current_user_id());
414 $task->load('assignees');
415
416 return $task;
417 }
418
419 public function deleteTask($task)
420 {
421 $deleted = $task->delete();
422
423 if ($deleted) {
424
425 //task assignees watchers removed
426 $task->watchers()->detach();
427 $task->assignees()->detach();
428
429 //removing all task related notifications
430 $task->notifications()->delete();
431
432 //task labels removed
433 $task->labels()->detach();
434
435 do_action('fluent_boards/task_deleted', $task);
436 TaskMeta::where('task_id', $task->id)->delete();
437 }
438 }
439
440 public function filterNullDate($date)
441 {
442 if ('0000-00-00 00:00:00' == $date || false === strtotime($date)) {
443 return null;
444 }
445 return $date;
446 }
447
448 // this is invoked when task is moved to another board
449 public function changeBoardByTask($task, $targetBoardId)
450 {
451 if ($task->board_id == $targetBoardId) {
452 return $task;
453 }
454
455 $oldBoard = Board::find($task->board_id);
456
457 $newBoard = Board::find($targetBoardId);
458 if (!$newBoard) {
459 throw new \Exception('Invalid board id', 400);
460 }
461 $task->board_id = $targetBoardId;
462 $task->save();
463 //delete labels of that task because labels have board dependencies
464 $task->labels()->detach();
465
466 do_action('fluent_boards/task_moved_from_board', $task, $oldBoard, $newBoard);
467
468 return $task;
469 }
470
471
472 public function getIdeaVoteStatistics($taskId)
473 {
474 $reactionTypes = [
475 [
476 'label' => 'Upvote',
477 'type' => 'upvote'
478 ],
479 [
480 'label' => 'Downvote',
481 'type' => 'downvote'
482 ]
483 ];
484
485 $reactionCounts = [];
486
487 foreach ($reactionTypes as $reactionType) {
488 $count = IdeaReaction::where('object_id', $taskId)
489 ->where('object_type', 'idea')
490 ->where('type', $reactionType['type'])
491 ->count();
492
493 $reactionCounts[] = [
494 'label' => $reactionType['label'],
495 'type' => $reactionType['type'],
496 'count' => $count
497 ];
498 }
499
500 return $reactionCounts;
501 }
502
503
504 /**
505 * Summary of getArchivedOrCompletedTasks
506 * this function will return completd tasks or archived tasks based on users input and also can search by name
507 * @param mixed $data
508 * @param mixed $taskType
509 * @return mixed
510 * @throws \Exception
511 */
512 public function getArchivedTasks($data, $boardId)
513 {
514 $per_page = isset($data['per_page']) ? $data['per_page'] : 25;
515 $page = isset($data['page']) ? $data['page'] : 1;
516 $tasksQuery = Task::where('board_id', $boardId)->whereNotNull('archived_at');
517 if (isset($data['searchInput'])) {
518 $tasksQuery = $tasksQuery->where('title', 'LIKE', '%' . $data['searchInput'] . '%');
519 }
520
521 // if board_id is not passed then throw an exception
522 if (!$boardId) {
523 throw new \Exception('Board id is required', 'fluent-boards');
524 }
525
526 return $tasksQuery->orderBy('created_at', 'DESC')->with('assignees')->paginate($per_page, ['*'], 'page', $page);
527 }
528
529 public function sendMailAfterTaskModify($column, $assigneeIds, $taskId)
530 {
531 $current_user_id = get_current_user_id();
532 /* this will run in background as soon as possible */
533 /* sending Model or Model Instance won't work here */
534
535 as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_'.$column, [$taskId, $assigneeIds, $current_user_id], 'fluent-boards');
536 }
537
538 public function getStageByTask($task_id)
539 {
540 $task = Task::find($task_id);
541 return $task->stage;
542 }
543
544 public function moveTaskToNextStage($task_id)
545 {
546 $task = Task::findOrFail($task_id);
547
548 $oldStage = $task->stage;
549
550 $nextStage = Stage::where('board_id', $task->board_id)
551 ->where('position', '>', $oldStage->position)
552 ->orderBy('position', 'ASC')
553 ->first();
554
555 if (!$nextStage) {
556 return $task;
557 }
558
559 if ($nextStage->defaultTaskStatus() == 'closed' && $task->status != 'closed') {
560 $task->status = 'closed';
561 if (!$task->last_completed_at) {
562 $task->last_completed_at = current_time('mysql');
563 }
564 }
565
566 $task->stage_id = $nextStage->id;
567 $task->save();
568
569 $task->load(['board', 'stage', 'attachments']);
570
571 $task = $this->loadNextStage($task);
572
573 return $task;
574 }
575
576 public function loadNextStage($task)
577 {
578 $stage = $task->stage;
579 $nextStage = Stage::where('board_id', $task->board_id)
580 ->where('position', '>', $stage->position)
581 ->orderBy('position', 'ASC')
582 ->first();
583
584 $task->nextStage = $nextStage ? $nextStage->title : null;
585 return $task;
586 }
587
588 public function getActivities($taskId, $perPage, $filter = 'newest')
589 {
590 $activityQuery = Activity::where('object_id', $taskId)
591 ->where('object_type', Constant::ACTIVITY_TASK);
592 if ($filter == 'newest') {
593 $activityQuery = $activityQuery->latest();
594 } else if ($filter == 'oldest') {
595 $activityQuery = $activityQuery->oldest();
596 }
597 return $activityQuery->with('user')->paginate($perPage);
598 }
599
600 public function getLastOneMinuteUpdatedTasks($boardId, $lastUpdated = null)
601 {
602 if (!$lastUpdated) {
603 $lastUpdated = gmdate('Y-m-d H:i:s', current_time('timestamp') - 60);
604 }
605
606 $tasks = Task::query()
607 ->where([
608 'board_id' => $boardId,
609 'parent_id' => null,
610 ])
611 ->where('updated_at', '>', $lastUpdated)
612 ->with(['assignees', 'labels', 'watchers'])
613 ->orderBy('due_at', 'ASC')
614 ->get();
615
616 foreach ($tasks as $task) {
617 $task->isOverdue = $task->isOverdue();
618 $task->isUpcoming = $task->upcoming();
619 $task->is_watching = $task->isWatching();
620 $task->contact = Task::lead_contact($task->crm_contact_id);
621 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
622 $task->watchers = Helper::sanitizeUserCollections($task->watchers);
623 }
624 return $tasks;
625 }
626
627 public function getLastPositionOfTasks($stage_id)
628 {
629 $lastPosition = Task::query()
630 ->where('stage_id', $stage_id)
631 ->where('parent_id', null)
632 ->whereNull('archived_at')
633 ->orderBy('position', 'desc')
634 ->pluck('position')
635 ->first();
636
637 return $lastPosition + 1;
638 }
639
640 public function getAssociatedTasks($associatedId)
641 {
642 $tasks = Task::query()
643 ->where('crm_contact_id', $associatedId)
644 ->with(['board', 'stage', 'assignees', 'labels', 'watchers',])
645 ->orderBy('due_at', 'ASC')
646 ->get();
647
648 foreach ($tasks as $task) {
649 $task->isOverdue = $task->isOverdue();
650 $task->isUpcoming = $task->upcoming();
651 $task->contact = Task::lead_contact($task->crm_contact_id);
652 $task->is_watching = $task->isWatching();
653
654 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
655 $task->watchers = Helper::sanitizeUserCollections($task->watchers);
656
657 $subTasks = Task::query()
658 ->where('parent_id', $task->id)
659 ->with(['assignees'])
660 ->whereNull('archived_at')
661 ->orderBy('position', 'ASC')
662 ->get();
663
664 foreach ($subTasks as $subTask) {
665 $subTask->assignees = Helper::sanitizeUserCollections($subTask->assignees);
666 }
667
668 $task->subtasks = $subTasks;
669 }
670
671 return $tasks;
672 }
673
674 public function copyTasks($boardId, $stageMap, $newBoard)
675 {
676 $allActiveTasks = Task::where('board_id', $boardId)->whereNull('archived_at')->get();
677 $taskMap = [];
678 foreach ($allActiveTasks as $task) {
679 $newTask = array();
680 $newTask['title'] = $task->title;
681 $newTask['parent_id'] = $task->parent_id ? $taskMap[$task->parent_id] : null;
682 $newTask['description'] = $task->description;
683 $newTask['board_id'] = $newBoard->id;
684 $newTask['stage_id'] = $stageMap[$task->stage_id];
685 $newTask['status'] = $task->status;
686 $newTask['priority'] = $task->priority;
687 $newTask['position'] = $task->position;
688 $newTask['due_at'] = $task->due_at;
689 $newTask = Task::create($newTask);
690 if(!$task->parent_id){
691 $taskMap[$task['id']] = $newTask->id;
692 } else {
693 $this->subtaskCountUpdate($newTask->parent_id);
694 }
695 }
696
697 //initiate task count
698 $totalTasks = sizeof($allActiveTasks);
699 $board = Board::findOrFail($newBoard->id);
700 $settings = [];
701 $settings['tasks_count'] = $totalTasks;
702 $board->settings = $settings;
703 $board->save();
704 }
705
706 private function subtaskCountUpdate($taskId){
707 $parentTask = Task::findOrFail($taskId);
708 $settings = $parentTask->settings;
709 $settings['subtask_count'] = (int)($settings['subtask_count'] ?? 0) + 1;
710 $parentTask->settings = $settings;
711 $parentTask->save();
712 }
713
714
715 }
716