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

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