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

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