PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.40
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.40
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 / Models / Task.php

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

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