PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.91.6
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.91.6
2.1.0 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 All 42 releases
fluent-boards / app / Models / Task.php

Task.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 1.91.6, at app/Models/Task.php

769 lines 24.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\Models;
4
5 use FluentBoards\App\Services\Constant;
6 use FluentBoards\App\Services\Helper;
7 use FluentBoards\App\Services\TaskService;
8 use FluentBoards\Framework\Database\Orm\Builder;
9 use FluentBoardsPro\App\Models\TaskAttachment;
10 use FluentBoardsPro\App\Models\CustomField;
11 use FluentBoardsPro\App\Services\Constant as ProConstant;
12 use FluentCrm\App\Models\Subscriber;
13
14 class Task extends Model
15 {
16 protected $table = 'fbs_tasks';
17
18 protected $guarded = ['id'];
19
20 protected $fillable = [
21 'title',
22 'slug',
23 'board_id',
24 'parent_id',
25 'crm_contact_id',
26 'type',
27 'lead_value',
28 'stage_id',
29 'status',
30 'reminder_type',
31 'priority',
32 'archived_at',
33 'remind_at',
34 'source',
35 'source_id',
36 'description',
37 'lead_value',
38 'events',
39 'settings',
40 'due_at',
41 'started_at',
42 'last_completed_at',
43 'position',
44 'comments_count',
45 'created_by',
46 ];
47
48 protected $appends = ['meta', 'repeat_task_meta'];
49
50 protected static $skipTaskCreatedEvent = false;
51
52 public static function withoutTaskCreatedEvent($callback)
53 {
54 static::$skipTaskCreatedEvent = true;
55 $result = $callback();
56 static::$skipTaskCreatedEvent = false;
57 return $result;
58 }
59
60 public static function boot()
61 {
62 parent::boot();
63 static::creating(function ($model) {
64 $board = Board::find($model->board_id);
65 $model->created_by = $model->created_by ?: get_current_user_id();
66 $model->type = $board->type === 'roadmap' ? 'roadmap' : 'task'; // default task type is task
67
68 if (empty($model->slug)) {
69 $model->slug = sanitize_title($model->title, 'idea-'.time());
70 }
71
72 $model->settings = $model->settings
73 ?: [
74 'cover' => [
75 'backgroundColor' => '',
76 ],
77 'subtask_count' => 0,
78 'attachment_count' => 0,
79 'subtask_completed_count' => 0,
80 ];
81 $model->position = $model->position
82 ?: (new TaskService())->getLastPositionOfTasks($model->stage_id);
83 });
84 static::created(function ($model) {
85 if (!$model->parent_id && !static::$skipTaskCreatedEvent) {
86 do_action('fluent_boards/task_created', $model);
87 if ($model->crm_contact_id) {
88 do_action('fluent_boards/contact_added_to_task', $model);
89 }
90 } else {
91 self::adjustSubtaskCount($model->parent_id);
92 }
93 });
94
95 /* global scope for task type which means only type = task will be fetched from everywhere in */
96 static::addGlobalScope('type', function (Builder $builder) {
97 $builder->where('type', '=', 'task')
98 ->orWhere('type', '=', 'roadmap');
99 });
100 }
101
102
103 public function scopeType($query, $type)
104 {
105 return $query->where('type', $type);
106 }
107
108 /**
109 * scope of getting past due not completed tasks
110 *
111 * @param $query \FluentBoards\Framework\Database\Query\Builder
112 *
113 * @return \FluentBoards\Framework\Database\Query\Builder
114 */
115 public function scopeOverdue($query)
116 {
117 return $query->whereNull('last_completed_at')
118 ->where('status', 'open')
119 ->where('due_at', '<=', current_time('mysql'));
120 }
121
122 /**
123 * scope of getting upcoming tasks
124 *
125 * @param $query \FluentBoards\Framework\Database\Query\Builder
126 *
127 * @return \FluentBoards\Framework\Database\Query\Builder
128 */
129 public function scopeUpcoming($query)
130 {
131 return $query->where('status', 'open')
132 ->where('due_at', '>=', current_time('mysql'));
133 }
134
135
136 public function setSettingsAttribute($settings)
137 {
138 $this->attributes['settings'] = \maybe_serialize($settings);
139 }
140
141 public function getSettingsAttribute($settings)
142 {
143 return \maybe_unserialize($settings);
144 }
145
146
147 /**
148 * One2Many: Task has many activities
149 *
150 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
151 */
152 public function activities()
153 {
154 return $this->hasMany(Activity::class, 'object_id', 'id')
155 ->where('object_type', Constant::ACTIVITY_TASK)
156 ->orderBy('id', 'DESC');
157 }
158
159 //->orderBy('id', 'DESC')
160
161 /**
162 * One2Many: Task has many activities
163 *
164 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
165 */
166 public function comments()
167 {
168 return $this->hasMany(Comment::class, 'task_id', 'id')
169 ->where('type', 'comment')
170 ->where('parent_id', null);
171 }
172
173 /**
174 * One2Many: Task has many notifications
175 *
176 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
177 */
178 public function notifications()
179 {
180 return $this->hasMany(Notification::class, 'task_id', 'id');
181 }
182
183 /**
184 * One2Many: Task has many activities
185 *
186 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
187 */
188 public function public_comments() // for primarily - roadmap plugin
189 {
190 return $this->hasMany(Comment::class, 'task_id', 'id')
191 ->where('privacy', 'public')
192 ->where('status', 'published');
193 }
194
195 public function board()
196 {
197 return $this->belongsTo(Board::class, 'board_id', 'id');
198 }
199
200 public function assignees()
201 {
202 return $this->belongsToMany(
203 User::class,
204 'fbs_relations',
205 'object_id',
206 'foreign_id'
207 )->withPivot('settings', 'preferences')
208 ->wherePivot('object_type',
209 Constant::OBJECT_TYPE_TASK_ASSIGNEE)
210 ->withTimestamps();
211 }
212
213 public function labels()
214 {
215 return $this->belongsToMany(
216 BoardTerm::class,
217 'fbs_relations',
218 'object_id',
219 'foreign_id'
220 )->withPivot('settings')
221 ->wherePivot('object_type',
222 Constant::OBJECT_TYPE_TASK_LABEL)
223 ->withTimestamps();
224 }
225
226 public function attachments() //may not need in future
227 {
228 return $this->hasMany(TaskAttachment::class,
229 'object_id', 'id')
230 ->where('object_type', Constant::OBJECT_TYPE_TASK);
231 }
232
233 public function watchers()
234 {
235 return $this->belongsToMany(
236 User::class,
237 'fbs_relations',
238 'object_id',
239 'foreign_id'
240 )->withPivot('settings')
241 ->wherePivot('object_type',
242 Constant::OBJECT_TYPE_USER_TASK_WATCH)
243 ->withTimestamps();
244 }
245
246 public function parentTask($id)
247 {
248 return self::find($id);
249 }
250
251 public function contact()
252 {
253 return $this->belongsTo(Subscriber::class, 'crm_contact_id', 'id');
254 }
255
256 public function taskMeta()
257 {
258 return $this->hasMany(TaskMeta::class, 'task_id', 'id');
259 }
260
261 public function getPopularCount()
262 {
263 $interactions = $this->taskMeta()->get()->pluck('value', 'key');
264 if($interactions) {
265 return ($interactions['upvote'] ?? 0) + ($interactions['comments_count'] ?? 0);
266 } else {
267 return 0;
268 }
269 }
270
271 public function getMetaAttribute()
272 {
273 return $this->taskMeta()->get()->pluck('value', 'key');
274 }
275
276 public function stage()
277 {
278 return $this->belongsTo(Stage::class, 'stage_id', 'id');
279 }
280
281 public function isOverdue()
282 {
283 return $this->last_completed_at == null && $this->due_at
284 && strtotime($this->due_at, current_time('timestamp'))
285 <= current_time('timestamp');
286 }
287
288 public function upcoming()
289 {
290 return $this->last_completed_at == null && $this->due_at
291 && strtotime($this->due_at, current_time('timestamp'))
292 >= current_time('timestamp');
293 }
294
295 public function isWatching()
296 {
297 $userId = get_current_user_id();
298 $is_watching = false;
299 foreach ($this->watchers as $watcher) {
300 if ($watcher->ID == $userId) {
301 $is_watching = true;
302 }
303 }
304
305 return $is_watching;
306 }
307
308 public function createTask($data)
309 {
310 $data = apply_filters('fluent_boards/before_task_create', $data);
311 $createdTask = Task::create($data);
312
313 if ( ! empty($data['assignees'])) {
314 $assignees = array_filter(array_map('intval', $data['assignees']));
315 if ($assignees) {
316 $assigneeData = array_fill_keys($assignees,
317 ['object_type' => Constant::OBJECT_TYPE_TASK_ASSIGNEE]);
318 $createdTask->assignees()->syncWithoutDetaching($assigneeData);
319
320 // Add assignees as watchers
321 foreach ($assignees as $assigneeId) {
322 $createdTask->watchers()->syncWithoutDetaching([
323 $assigneeId => ['object_type' => Constant::OBJECT_TYPE_USER_TASK_WATCH]
324 ]);
325 }
326 }
327 }
328
329 if ( ! empty($data['labels'])) {
330 $labels = array_filter(array_map('intval', $data['labels']));
331 if ($labels) {
332 foreach ($labels as $label) {
333 $createdTask->labels()->syncWithoutDetaching([$label => ['object_type' => Constant::OBJECT_TYPE_TASK_LABEL]]);
334 }
335 }
336 }
337
338 return $createdTask;
339 }
340
341 public function addOrRemoveAssignee($idToAddOrRemove)
342 {
343 $oldAssigneeIds = $this->assignees->pluck('ID')->toArray();
344 $IfAlreadyAssignee = in_array($idToAddOrRemove, $oldAssigneeIds);
345 $operation = 'added';
346
347 if ($IfAlreadyAssignee) { // if already an assignee then it is a remove operation
348 $this->assignees()->detach($idToAddOrRemove);
349 $this->watchers()->detach($idToAddOrRemove);
350 $operation = 'removed';
351 } else { //else an add operation
352 $this->assignees()
353 ->syncWithoutDetaching([$idToAddOrRemove => ['object_type' => Constant::OBJECT_TYPE_TASK_ASSIGNEE]]);
354 $this->watchers()
355 ->syncWithoutDetaching([$idToAddOrRemove => ['object_type' => Constant::OBJECT_TYPE_USER_TASK_WATCH]]);
356 }
357
358 return $operation;
359 }
360
361 public function getArchivedAttribute()
362 {
363 return (bool) $this->attributes['is_archived'];
364 }
365
366 public function setArchivedAttribute($value)
367 {
368 if (true === $value) {
369 $this->attributes['is_archived'] = 1;
370 } elseif (false === $value) {
371 $this->attributes['is_archived'] = 0;
372 } else {
373 $this->attributes['is_archived'] = in_array($value, [0, 1]) ? $value
374 : 0;
375 }
376 }
377
378 public function user($id)
379 {
380 return User::findOrFail($id);
381 }
382
383 /**
384 * from now(01/03/24) we will use Helper::crm_contact() method
385 */
386 public static function lead_contact($id)
387 {
388 if ( ! defined('FLUENTCRM')) {
389 return '';
390 }
391
392 $contact = \FluentCrm\App\Models\Subscriber::with(['tags', 'lists'])
393 ->find($id);
394
395 if ( ! $contact) {
396 return null;
397 }
398
399 return [
400 'id' => $contact->id,
401 'email' => $contact->email,
402 'first_name' => $contact->first_name,
403 'last_name' => $contact->last_name,
404 'full_name' => $contact->full_name,
405 'avatar' => $contact->avatar,
406 'photo' => $contact->photo,
407 'status' => $contact->status,
408 'contact_type' => $contact->contact_type,
409 'last_activity' => $contact->last_activity,
410 'life_time_value' => $contact->life_time_value,
411 'total_points' => $contact->total_points,
412 'user_id' => $contact->user_id,
413 'created_at' => $contact->created_at,
414 'tags' => Helper::getIdTitleArray($contact->tags),
415 'lists' => Helper::getIdTitleArray($contact->lists),
416
417 ];
418
419 }
420
421 public function getMeta($key, $default = null)
422 {
423 $exist = TaskMeta::where('task_id', $this->id)->where('key', $key)
424 ->first();
425 if ($exist) {
426 return $exist->value;
427 }
428
429 return $default;
430 }
431
432 public function updateMeta($key, $value)
433 {
434 $exist = TaskMeta::where('task_id', $this->id)->where('key', $key)
435 ->first();
436
437 if ($exist) {
438 $exist->value = $value;
439 $exist->save();
440 } else {
441 $exist = TaskMeta::create([
442 'task_id' => $this->id,
443 'key' => $key,
444 'value' => $value,
445 ]);
446 }
447
448 return $exist;
449 }
450
451 public function moveToNewPosition($newIndex)
452 {
453 $newIndex = (int) $newIndex;
454 if ($newIndex < 1) {
455 $newIndex = 1;
456 }
457
458 // Declaring query for subtask or task
459 if (isset($this->parent_id)) {
460 $taskQuery = self::where('parent_id', $this->parent_id)
461 ->whereNull('archived_at');
462 } else {
463 $taskQuery = self::where('stage_id', $this->stage_id)
464 ->whereNull('archived_at');
465 }
466
467
468 if ($newIndex == 1) {
469 $firstItem = $taskQuery->where('id', '!=', $this->id)
470 ->orderBy('position', 'asc')
471 ->first();
472
473 if ($firstItem) {
474 if ($firstItem->position < 0.02) {
475 self::reIndexTasksPositions($this->toArray());
476
477 return $this->moveToNewPosition($newIndex);
478 }
479 $index = round($firstItem->position / 2, 2);
480 } else {
481 $index = 1;
482 }
483
484 $this->position = $index;
485 $this->save();
486
487 return $this;
488 }
489
490 $prevTask = $taskQuery
491 ->offset($newIndex - 2)
492 ->where('id', '!=', $this->id)
493 ->orderBy('position', 'asc')
494 ->first();
495
496 if ( ! $prevTask) {
497 return $this->moveToNewPosition(1);
498 }
499
500 $nextItem = $taskQuery
501 ->offset($newIndex - 1)
502 ->where('id', '!=', $this->id)
503 ->orderBy('position', 'asc')
504 ->first();
505
506 if ( ! $nextItem) {
507 $this->position = $prevTask->position + 1;
508 $this->save();
509
510 return $this;
511 }
512
513 $newPosition = ($prevTask->position + $nextItem->position) / 2;
514
515 // check if new position is already taken
516 $exist = $taskQuery
517 ->where('position', $newPosition)
518 ->where('id', '!=', $this->id)
519 ->first();
520
521 if ($exist) {
522 self::reIndexTasksPositions($this->toArray());
523
524 return $this->moveToNewPosition($newIndex);
525 }
526
527 $this->position = $newPosition;
528 $this->save();
529
530 return $this;
531 }
532
533 public static function reIndexTasksPositions($task)
534 {
535 if (isset($task['parent_id'])) {
536 $tasksQuery = self::where('parent_id', $task['parent_id']);
537 } else {
538 $tasksQuery = self::where('stage_id', $task['stage_id']);
539 }
540 $allTasks = $tasksQuery->orderBy('position', 'asc')
541 ->whereNull('archived_at')->get();
542
543 foreach ($allTasks as $index => $task) {
544 $task->position = $index + 1;
545 $task->save();
546 }
547 }
548
549
550 public static function adjustSubtaskCount($subTaskParentId)
551 {
552 if ( ! $subTaskParentId) {
553 return;
554 }
555 $parentTask = Task::find($subTaskParentId);
556 if( !$parentTask) {
557 return;
558 }
559 $subtasks = $parentTask->subtasks;
560 $parentTaskSettings = $parentTask->settings;
561 $parentTaskSettings['subtask_count'] = $subtasks->count();
562 $parentTaskSettings['subtask_completed_count'] = $subtasks->filter(function ($subtask) {
563 return $subtask->status === 'closed';
564 })->count();
565 $parentTask->settings = $parentTaskSettings;
566 $parentTask->save();
567 }
568
569 public function close()
570 {
571 if ($this->status == 'closed') {
572 return $this;
573 }
574
575 $this->status = 'closed';
576 $this->last_completed_at = current_time('mysql');
577 $this->save();
578 if ($this->parent_id) {
579 self::adjustSubtaskCount($this->parent_id);
580 }
581 return $this;
582 }
583
584 public function reopen()
585 {
586 if ($this->status == 'open') {
587 return $this;
588 }
589
590 $this->status = 'open';
591 $this->last_completed_at = null;
592 $this->save();
593 if ($this->parent_id) {
594 self::adjustSubtaskCount($this->parent_id);
595 }
596 return $this;
597 }
598
599 /*
600 * Get all the fields that can be mapped
601 * to Task Creation Webhook or
602 * REST API Task Creation
603 * PHP API Task Creation
604 * @return array
605 */
606 public static function mappables()
607 {
608 return [
609 'task_title' => __('Task Title', 'fluent-boards'),
610 'slug' => __('Slug', 'fluent-boards'),
611 'board_title' => __('Board Title', 'fluent-boards'),
612 'status' => __('Status', 'fluent-boards'),
613 'type' => __('Type', 'fluent-boards'),
614 'description' => __('Description', 'fluent-boards'),
615 'priority' => __('Priority', 'fluent-boards'),
616 'due_at' => __('Due Date', 'fluent-boards'),
617 'started_at' => __('Start Date', 'fluent-boards'),
618 'archived_at' => __('Archive Date', 'fluent-boards'),
619 'stage' => __('Stage', 'fluent-boards'),
620 'board' => __('Board', 'fluent-boards'),
621 'source' => __('Source', 'fluent-boards'),
622 'position' => __('Position', 'fluent-boards'),
623 'subtasks' => __('Subtasks', 'fluent-boards'),
624 'completion' => __('Completion', 'fluent-boards'),
625 ];
626 }
627 public static function mappableFields()
628 {
629 $fields = [
630 'title' => [
631 'field' => __('Title', 'fluent-boards'),
632 'type' => 'text',
633 'rules' => 'required',
634 'description' => __('Title of the task.', 'fluent-boards'),
635 ],
636 'stage' => [
637 'field' => __('Stage', 'fluent-boards'),
638 'type' => 'int|text',
639 'rules' => 'optional',
640 'description' => __('The stage of the task, which can be an ID, title, or slug. Example: 1 | "open" | "Open"', 'fluent-boards'),
641 ],
642 'parent_id' => [
643 'field' => __('Parent Task', 'fluent-boards'),
644 'type' => 'int',
645 'rules' => 'optional',
646 'description' => __('Parent Task ID of the subtask. Example: 1', 'fluent-boards'),
647 ],
648 'status' => [
649 'field' => __('Status', 'fluent-boards'),
650 'type' => 'text',
651 'rules' => 'optional',
652 'description' => __('The status of the task (open | closed). Example: "closed"', 'fluent-boards'),
653 ],
654 'description' => [
655 'field' => __('Description', 'fluent-boards'),
656 'type' => 'textarea',
657 'rules' => 'optional',
658 'description' => __('Description of the task', 'fluent-boards'),
659 ],
660 'priority' => [
661 'field' => __('Priority', 'fluent-boards'),
662 'type' => 'text',
663 'rules' => 'optional',
664 'description' => __('Priority of the task (low | medium | high). Example: "medium" ', 'fluent-boards'),
665 ],
666 'due_at' => [
667 'field' => __('Due Date', 'fluent-boards'),
668 'type' => 'date',
669 'rules' => 'optional',
670 'description' => __('The due date of the task in the format YYYY-MM-DD hh:mm. Example: 2099-12-31 23:59:59', 'fluent-boards'),
671 ],
672 'started_at' => [
673 'field' => __('Start Date', 'fluent-boards'),
674 'type' => 'date',
675 'rules' => 'optional',
676 'description' => __('The start date of the task in the format YYYY-MM-DD. Example: 2099-12-31', 'fluent-boards'),
677 ],
678 'source' => [
679 'field' => __('Source', 'fluent-boards'),
680 'type' => 'text',
681 'rules' => 'optional',
682 'description' => __('The source of the task. Example: "jira"', 'fluent-boards'),
683 ],
684 'source_id' => [
685 'field' => __('Source Id', 'fluent-boards'),
686 'type' => 'text|int',
687 'rules' => 'optional',
688 'description' => __('The source Id of the task (if any). Example: "bcy664fh177"', 'fluent-boards'),
689 ],
690 'crm_contact_id' => [
691 'field' => __('CRM Contact Id', 'fluent-boards'),
692 'type' => 'int',
693 'rules' => 'optional',
694 'description' => __('The ID of the associated FluentCRM contact. Example: 6465', 'fluent-boards'),
695 ],
696 'contact_email' => [
697 'field' => __('CRM Contact Email', 'fluent-boards'),
698 'type' => 'text',
699 'rules' => 'optional',
700 'description' => __('The email of the associated CRM contact. Example: "john.doe@example.com"', 'fluent-boards'),
701 ],
702 'contact_first_name' => [
703 'field' => __('Contact First Name', 'fluent-boards'),
704 'type' => 'text',
705 'rules' => 'optional',
706 'description' => __('Associated CRM Contact First Name. Example: "John"', 'fluent-boards'),
707 ],
708 'contact_last_name' => [
709 'field' => __('Contact Last Name', 'fluent-boards'),
710 'type' => 'text',
711 'rules' => 'optional',
712 'description' => __('Associated CRM Contact Last Name', 'fluent-boards'),
713 ],
714 'labels' => [
715 'field' => __('Labels', 'fluent-boards'),
716 'type' => 'text|int',
717 'rules' => 'optional',
718 'description' => __('An array of label IDs or titles. Example: [1, "feature", 44]', 'fluent-boards'),
719 ],
720 'assignees' => [
721 'field' => __('Assignees', 'fluent-boards'),
722 'type' => 'text|int',
723 'rules' => 'optional',
724 'description' => __('An array of WP User IDs. Example: [1,2,44]', 'fluent-boards'),
725 ]
726 ];
727
728 return $fields;
729 }
730
731 public function customFields()
732 {
733 return $this->belongsToMany(
734 CustomField::class,
735 'fbs_relations',
736 'object_id',
737 'foreign_id'
738 )->withPivot('settings')
739 ->wherePivot('object_type', ProConstant::TASK_CUSTOM_FIELD)
740 ->withTimestamps();
741 }
742
743
744 public function repeatTaskMeta()
745 {
746 return $this->hasOne(Meta::class, 'object_id', 'id')->where('object_type', Constant::REPEAT_TASK_META);
747 }
748 public function getRepeatTaskMetaAttribute()
749 {
750 return $this->repeatTaskMeta()->first();
751 }
752 public function taskCustomFields()
753 {
754 return $this->hasMany(Relation::class, 'object_id', 'id')
755 ->where('object_type', Constant::TASK_CUSTOM_FIELD);
756 }
757
758 public function subtasks()
759 {
760 return $this->hasMany(Task::class, 'parent_id', 'id');
761 }
762
763 public function subtaskGroup()
764 {
765 return $this->hasMany(TaskMeta::class, 'task_id')->where('key', Constant::SUBTASK_GROUP_NAME);
766 }
767
768 }
769