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

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