PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
2.2.0 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 All 43 releases
fluent-boards / app / Models / Task.php

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

1,011 lines 32.1 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 FluentBoards\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
17 protected $table = 'fbs_tasks';
18
19 protected $guarded = ['id'];
20
21 protected $fillable = [
22 'title',
23 'slug',
24 'board_id',
25 'parent_id',
26 'crm_contact_id',
27 'type',
28 'lead_value',
29 'stage_id',
30 'status',
31 'reminder_type',
32 'priority',
33 'archived_at',
34 'remind_at',
35 'source',
36 'source_id',
37 'description',
38 'lead_value',
39 'events',
40 'settings',
41 'due_at',
42 'started_at',
43 'last_completed_at',
44 'position',
45 'comments_count',
46 'created_by',
47 ];
48
49 protected $appends = ['meta', 'repeat_task_meta', 'is_pinned'];
50
51 protected $nullableTimestampAttributes = [
52 'due_at',
53 'started_at',
54 'last_completed_at',
55 'archived_at',
56 'remind_at',
57 ];
58
59 protected static $skipTaskCreatedEvent = false;
60
61 public static function withoutTaskCreatedEvent($callback)
62 {
63 static::$skipTaskCreatedEvent = true;
64 $result = $callback();
65 static::$skipTaskCreatedEvent = false;
66 return $result;
67 }
68
69 public static function boot()
70 {
71 parent::boot();
72 static::creating(function ($model) {
73 $board = Board::find($model->board_id);
74 $model->created_by = $model->created_by ?: get_current_user_id();
75 $model->type = $board->type === 'roadmap' ? 'roadmap' : 'task'; // default task type is task
76
77 if (empty($model->slug)) {
78 $model->slug = sanitize_title($model->title, 'idea-'.time());
79 }
80
81 $model->settings = $model->settings
82 ?: [
83 'cover' => [
84 'backgroundColor' => '',
85 ],
86 'subtask_count' => 0,
87 'attachment_count' => 0,
88 'subtask_completed_count' => 0,
89 ];
90 $model->position = $model->position
91 ?: (new TaskService())->getLastPositionOfTasks($model->stage_id);
92 });
93 static::created(function ($model) {
94 if (!$model->parent_id && !static::$skipTaskCreatedEvent) {
95 do_action('fluent_boards/task_created', $model);
96 if ($model->crm_contact_id) {
97 do_action('fluent_boards/contact_added_to_task', $model);
98 }
99 } else {
100 self::adjustSubtaskCount($model->parent_id);
101 }
102 });
103
104 static::updated(function ($model) {
105 if ($model->isDirty('status') && $model->getOriginal('status') === 'open' && $model->status === 'closed') {
106 // Use persisted attributes without stale stage/board relations from the move.
107 $closedTask = $model->fresh();
108 if ($closedTask && $closedTask->status === 'closed') {
109 do_action('fluent_boards/task_closed', $closedTask);
110 }
111 }
112 });
113
114 /* global scope for task type which means only type = task will be fetched from everywhere in */
115 static::addGlobalScope('type', function (Builder $builder) {
116 $builder->where('type', '=', 'task')
117 ->orWhere('type', '=', 'roadmap');
118 });
119 }
120
121
122 public function scopeType($query, $type)
123 {
124 return $query->where('type', $type);
125 }
126
127 /**
128 * scope of getting past due not completed tasks
129 *
130 * @param $query \FluentBoards\Framework\Database\Query\Builder
131 *
132 * @return \FluentBoards\Framework\Database\Query\Builder
133 */
134 public function scopeOverdue($query)
135 {
136 return $query->whereNull('last_completed_at')
137 ->where('status', 'open')
138 ->where('due_at', '<=', current_time('mysql'));
139 }
140
141 /**
142 * scope of getting upcoming tasks
143 *
144 * @param $query \FluentBoards\Framework\Database\Query\Builder
145 *
146 * @return \FluentBoards\Framework\Database\Query\Builder
147 */
148 public function scopeUpcoming($query)
149 {
150 return $query->where('status', 'open')
151 ->where('due_at', '>=', current_time('mysql'));
152 }
153
154 /**
155 * scope of getting open tasks due today
156 *
157 * @param $query \FluentBoards\Framework\Database\Query\Builder
158 *
159 * @return \FluentBoards\Framework\Database\Query\Builder
160 */
161 public function scopeDueToday($query)
162 {
163 $todayTimestamp = current_time('timestamp');
164 $startOfToday = gmdate('Y-m-d 00:00:00', $todayTimestamp);
165 $endOfToday = gmdate('Y-m-d 23:59:59', $todayTimestamp);
166
167 return $query->whereNull('last_completed_at')
168 ->where('status', 'open')
169 ->whereBetween('due_at', [$startOfToday, $endOfToday]);
170 }
171
172
173 public function setSettingsAttribute($settings)
174 {
175 $this->attributes['settings'] = \maybe_serialize($settings);
176 }
177
178 public function getSettingsAttribute($settings)
179 {
180 return \maybe_unserialize($settings);
181 }
182
183
184 /**
185 * One2Many: Task has many activities
186 *
187 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
188 */
189 public function activities()
190 {
191 return $this->hasMany(Activity::class, 'object_id', 'id')
192 ->where('object_type', Constant::ACTIVITY_TASK)
193 ->orderBy('id', 'DESC');
194 }
195
196 //->orderBy('id', 'DESC')
197
198 /**
199 * One2Many: Task has many activities
200 *
201 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
202 */
203 public function comments()
204 {
205 return $this->hasMany(Comment::class, 'task_id', 'id')
206 ->where('type', 'comment')
207 ->where('parent_id', null);
208 }
209
210 /**
211 * One2Many: Task has many notifications
212 *
213 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
214 */
215 public function notifications()
216 {
217 return $this->hasMany(Notification::class, 'task_id', 'id');
218 }
219
220 /**
221 * One2Many: Task has many activities
222 *
223 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
224 */
225 public function public_comments() // for primarily - roadmap plugin
226 {
227 return $this->hasMany(Comment::class, 'task_id', 'id')
228 ->where('privacy', 'public')
229 ->where('status', 'published');
230 }
231
232 public function board()
233 {
234 return $this->belongsTo(Board::class, 'board_id', 'id');
235 }
236
237 /**
238 * Exclude tasks that belong to template boards.
239 */
240 public function scopeExcludeTemplateBoards($query)
241 {
242 return $query->whereHas('board', function ($boardQuery) {
243 $boardQuery->excludeTemplates();
244 });
245 }
246
247 /**
248 * Limit tasks to active, non-template boards available in this install.
249 */
250 public function scopeOnActiveAvailableBoards($query)
251 {
252 return $query->whereHas('board', function ($boardQuery) {
253 $boardQuery->whereNull('archived_at')
254 ->excludeTemplates()
255 ->availableInCurrentInstall();
256 });
257 }
258
259 public function assignees()
260 {
261 return $this->belongsToMany(
262 User::class,
263 'fbs_relations',
264 'object_id',
265 'foreign_id'
266 )->withPivot('settings', 'preferences')
267 ->wherePivot('object_type',
268 Constant::OBJECT_TYPE_TASK_ASSIGNEE)
269 ->withTimestamps();
270 }
271
272 public function labels()
273 {
274 return $this->belongsToMany(
275 BoardTerm::class,
276 'fbs_relations',
277 'object_id',
278 'foreign_id'
279 )->withPivot('settings')
280 ->wherePivot('object_type',
281 Constant::OBJECT_TYPE_TASK_LABEL)
282 ->withTimestamps();
283 }
284
285 public function attachments() //may not need in future
286 {
287 if (!defined('FLUENT_BOARDS_PRO_VERSION')) {
288 return $this->hasMany(Attachment::class, 'id', 'id')
289 ->where('id', 0);
290 }
291
292 return $this->hasMany(TaskAttachment::class,
293 'object_id', 'id')
294 ->where('object_type', Constant::OBJECT_TYPE_TASK);
295 }
296
297 public function watchers()
298 {
299 return $this->belongsToMany(
300 User::class,
301 'fbs_relations',
302 'object_id',
303 'foreign_id'
304 )->withPivot('settings')
305 ->wherePivot('object_type',
306 Constant::OBJECT_TYPE_USER_TASK_WATCH)
307 ->withTimestamps();
308 }
309
310 /**
311 * Tasks that must finish before this task can start (this task is the successor/blocked).
312 * In fbs_relations: object_id = predecessor, foreign_id = this task (successor).
313 *
314 * @return \FluentBoards\Framework\Database\Orm\Relations\BelongsToMany
315 */
316 public function predecessors()
317 {
318 return $this->belongsToMany(
319 Task::class,
320 'fbs_relations',
321 'foreign_id',
322 'object_id'
323 )->wherePivot('object_type', Constant::OBJECT_TYPE_TASK_DEPENDENCY)
324 ->withPivot('settings')
325 ->withTimestamps();
326 }
327
328 /**
329 * Tasks that are waiting on this task to finish (this task is the predecessor/blocker).
330 * In fbs_relations: object_id = this task (predecessor), foreign_id = successor.
331 *
332 * @return \FluentBoards\Framework\Database\Orm\Relations\BelongsToMany
333 */
334 public function successors()
335 {
336 return $this->belongsToMany(
337 Task::class,
338 'fbs_relations',
339 'object_id',
340 'foreign_id'
341 )->wherePivot('object_type', Constant::OBJECT_TYPE_TASK_DEPENDENCY)
342 ->withPivot('settings')
343 ->withTimestamps();
344 }
345
346 public function parentTask($id)
347 {
348 return self::find($id);
349 }
350
351 public function contact()
352 {
353 return $this->belongsTo(Subscriber::class, 'crm_contact_id', 'id');
354 }
355
356 public function taskMeta()
357 {
358 return $this->hasMany(TaskMeta::class, 'task_id', 'id');
359 }
360
361 public function getPopularCount()
362 {
363 $interactions = $this->taskMeta()->get()->pluck('value', 'key');
364 if($interactions) {
365 return ($interactions['upvote'] ?? 0) + ($interactions['comments_count'] ?? 0);
366 } else {
367 return 0;
368 }
369 }
370
371 /** Use batch-loaded metadata when preparing task-list responses. */
372 public function getMetaAttribute()
373 {
374 if ($this->relationLoaded('taskMeta')) {
375 return $this->getRelation('taskMeta')->pluck('value', 'key');
376 }
377
378 return $this->taskMeta()->get()->pluck('value', 'key');
379 }
380
381 public function stage()
382 {
383 return $this->belongsTo(Stage::class, 'stage_id', 'id');
384 }
385
386 public function isOverdue()
387 {
388 return $this->last_completed_at == null && $this->due_at
389 && strtotime($this->due_at, current_time('timestamp'))
390 <= current_time('timestamp');
391 }
392
393 public function upcoming()
394 {
395 return $this->last_completed_at == null && $this->due_at
396 && strtotime($this->due_at, current_time('timestamp'))
397 >= current_time('timestamp');
398 }
399
400 public function isWatching()
401 {
402 $userId = get_current_user_id();
403 $is_watching = false;
404 foreach ($this->watchers as $watcher) {
405 if ($watcher->ID == $userId) {
406 $is_watching = true;
407 }
408 }
409
410 return $is_watching;
411 }
412
413 public function createTask($data)
414 {
415 $data = apply_filters('fluent_boards/before_task_create', $data);
416
417 // Keep newly-created tasks unprioritized unless a priority is explicitly selected.
418 if (!array_key_exists('priority', $data)) {
419 $data['priority'] = '';
420 }
421
422 $createdTask = Task::create($data);
423
424 if ( ! empty($data['assignees'])) {
425 $assignees = array_filter(array_map('intval', $data['assignees']));
426 if ($assignees) {
427 $assigneeData = array_fill_keys($assignees,
428 ['object_type' => Constant::OBJECT_TYPE_TASK_ASSIGNEE]);
429 $createdTask->assignees()->syncWithoutDetaching($assigneeData);
430
431 // Add assignees as watchers
432 foreach ($assignees as $assigneeId) {
433 $createdTask->watchers()->syncWithoutDetaching([
434 $assigneeId => ['object_type' => Constant::OBJECT_TYPE_USER_TASK_WATCH]
435 ]);
436 }
437 }
438 }
439
440 if ( ! empty($data['labels'])) {
441 $labels = array_filter(array_map('intval', $data['labels']));
442 if ($labels) {
443 foreach ($labels as $label) {
444 $createdTask->labels()->syncWithoutDetaching([$label => ['object_type' => Constant::OBJECT_TYPE_TASK_LABEL]]);
445 }
446 }
447 }
448
449 return $createdTask;
450 }
451
452 public function addOrRemoveAssignee($idToAddOrRemove)
453 {
454 $oldAssigneeIds = $this->assignees->pluck('ID')->toArray();
455 $IfAlreadyAssignee = in_array($idToAddOrRemove, $oldAssigneeIds);
456 $operation = 'added';
457
458 if ($IfAlreadyAssignee) { // if already an assignee then it is a remove operation
459 $this->assignees()->detach($idToAddOrRemove);
460 $this->watchers()->detach($idToAddOrRemove);
461 $operation = 'removed';
462 } else { //else an add operation
463 $this->assignees()
464 ->syncWithoutDetaching([$idToAddOrRemove => ['object_type' => Constant::OBJECT_TYPE_TASK_ASSIGNEE]]);
465 $this->watchers()
466 ->syncWithoutDetaching([$idToAddOrRemove => ['object_type' => Constant::OBJECT_TYPE_USER_TASK_WATCH]]);
467 }
468
469 return $operation;
470 }
471
472 // public function getArchivedAttribute()
473 // {
474 // return (bool) $this->attributes['is_archived'];
475 // }
476
477 public function setArchivedAttribute($value)
478 {
479 if (true === $value) {
480 $this->attributes['is_archived'] = 1;
481 } elseif (false === $value) {
482 $this->attributes['is_archived'] = 0;
483 } else {
484 $this->attributes['is_archived'] = in_array($value, [0, 1]) ? $value
485 : 0;
486 }
487 }
488
489 public function user($id)
490 {
491 return User::findOrFail($id);
492 }
493
494 /**
495 * from now(01/03/24) we will use Helper::crm_contact() method
496 */
497 public static function lead_contact($id)
498 {
499 if ( ! defined('FLUENTCRM')) {
500 return '';
501 }
502
503 $contact = \FluentCrm\App\Models\Subscriber::with(['tags', 'lists'])
504 ->find($id);
505
506 if ( ! $contact) {
507 return null;
508 }
509
510 return [
511 'id' => $contact->id,
512 'email' => $contact->email,
513 'first_name' => $contact->first_name,
514 'last_name' => $contact->last_name,
515 'full_name' => $contact->full_name,
516 'avatar' => $contact->avatar,
517 'photo' => $contact->photo,
518 'status' => $contact->status,
519 'contact_type' => $contact->contact_type,
520 'last_activity' => $contact->last_activity,
521 'life_time_value' => $contact->life_time_value,
522 'total_points' => $contact->total_points,
523 'user_id' => $contact->user_id,
524 'created_at' => $contact->created_at,
525 'tags' => Helper::getIdTitleArray($contact->tags),
526 'lists' => Helper::getIdTitleArray($contact->lists),
527
528 ];
529
530 }
531
532 public function getMeta($key, $default = null)
533 {
534 $exist = TaskMeta::where('task_id', $this->id)->where('key', $key)
535 ->first();
536 if ($exist) {
537 return $exist->value;
538 }
539
540 return $default;
541 }
542
543 /** Persist metadata and invalidate any snapshot loaded for serialization. */
544 public function updateMeta($key, $value)
545 {
546 $exist = TaskMeta::where('task_id', $this->id)->where('key', $key)
547 ->first();
548
549 if ($exist) {
550 $exist->value = $value;
551 $exist->save();
552 } else {
553 $exist = TaskMeta::create([
554 'task_id' => $this->id,
555 'key' => $key,
556 'value' => $value,
557 ]);
558 }
559
560 $this->unsetRelation('taskMeta');
561
562 return $exist;
563 }
564
565 /**
566 * Virtual attribute: pinned state from task meta (for API/frontend).
567 * Tasks without is_pinned meta are treated as unpinned (0).
568 * Uses the batch-loaded metadata relation when preparing task lists.
569 *
570 * @return int 1 if pinned, 0 otherwise
571 */
572 public function getIsPinnedAttribute()
573 {
574 $meta = $this->meta;
575
576 return (int) ($meta[Constant::IS_TASK_PINNED] ?? 0);
577 }
578
579 public function moveToNewPosition($newIndex)
580 {
581 $newIndex = (int) $newIndex;
582 if ($newIndex < 1) {
583 $newIndex = 1;
584 }
585
586 // Declaring query for subtask or task
587 if (isset($this->parent_id)) {
588 $taskQuery = self::where('parent_id', $this->parent_id)
589 ->whereNull('archived_at');
590 } else {
591 $taskQuery = self::where('stage_id', $this->stage_id)
592 ->whereNull('archived_at');
593 }
594
595
596 if ($newIndex == 1) {
597 $firstItem = $taskQuery->where('id', '!=', $this->id)
598 ->orderBy('position', 'asc')
599 ->first();
600
601 if ($firstItem) {
602 if ($firstItem->position < 0.02) {
603 self::reIndexTasksPositions($this->toArray());
604
605 return $this->moveToNewPosition($newIndex);
606 }
607 $index = round($firstItem->position / 2, 2);
608 } else {
609 $index = 1;
610 }
611
612 $this->position = $index;
613 $this->save();
614
615 return $this;
616 }
617
618 $prevTask = $taskQuery
619 ->offset($newIndex - 2)
620 ->where('id', '!=', $this->id)
621 ->orderBy('position', 'asc')
622 ->first();
623
624 if ( ! $prevTask) {
625 return $this->moveToNewPosition(1);
626 }
627
628 $nextItem = $taskQuery
629 ->offset($newIndex - 1)
630 ->where('id', '!=', $this->id)
631 ->orderBy('position', 'asc')
632 ->first();
633
634 if ( ! $nextItem) {
635 $this->position = $prevTask->position + 1;
636 $this->save();
637
638 return $this;
639 }
640
641 $newPosition = ($prevTask->position + $nextItem->position) / 2;
642
643 // check if new position is already taken
644 $exist = $taskQuery
645 ->where('position', $newPosition)
646 ->where('id', '!=', $this->id)
647 ->first();
648
649 if ($exist) {
650 self::reIndexTasksPositions($this->toArray());
651
652 return $this->moveToNewPosition($newIndex);
653 }
654
655 $this->position = $newPosition;
656 $this->save();
657
658 return $this;
659 }
660
661 public function moveBetweenTasks($prevTaskId = null, $nextTaskId = null)
662 {
663 $prevTaskId = absint($prevTaskId);
664 $nextTaskId = absint($nextTaskId);
665 // Exclude the current task so cross-stage moves can reuse the same
666 // neighbour lookup logic after the stage_id has already been reassigned.
667 $taskQuery = $this->getTaskOrderingQuery();
668
669 $prevTask = null;
670 if ($prevTaskId) {
671 $prevTask = (clone $taskQuery)
672 ->where('id', $prevTaskId)
673 ->first();
674 }
675
676 $nextTask = null;
677 if ($nextTaskId) {
678 $nextTask = (clone $taskQuery)
679 ->where('id', $nextTaskId)
680 ->first();
681 }
682
683 if (!$prevTask && !$nextTask) {
684 $firstItem = (clone $taskQuery)
685 ->orderBy('position', 'asc')
686 ->first();
687
688 if (!$firstItem) {
689 $this->position = 1;
690 $this->save();
691
692 return $this;
693 }
694
695 if ($firstItem->position < 0.02) {
696 self::reIndexTasksPositions($this->toArray());
697
698 return $this->moveBetweenTasks($prevTaskId, $nextTaskId);
699 }
700
701 $this->position = round($firstItem->position / 2, 2);
702 $this->save();
703
704 return $this;
705 }
706
707 if (!$prevTask && $nextTask) {
708 // Insert before the first visible neighbour by splitting the leading gap.
709 if ($nextTask->position < 0.02) {
710 self::reIndexTasksPositions($this->toArray());
711
712 return $this->moveBetweenTasks($prevTaskId, $nextTaskId);
713 }
714
715 $this->position = round($nextTask->position / 2, 2);
716 $this->save();
717
718 return $this;
719 }
720
721 if ($prevTask && !$nextTask) {
722 // Insert after the last visible neighbour without touching the rest
723 // of the stage unless the sparse ordering needs a later reindex.
724 $this->position = $prevTask->position + 1;
725 $this->save();
726
727 return $this;
728 }
729
730 if ($prevTask->position >= $nextTask->position) {
731 self::reIndexTasksPositions($this->toArray());
732
733 return $this->moveBetweenTasks($prevTaskId, $nextTaskId);
734 }
735
736 // Middle inserts keep reordering cheap by taking the midpoint between
737 // the two neighbour positions instead of renumbering the whole stage.
738 $newPosition = ($prevTask->position + $nextTask->position) / 2;
739
740 $exists = (clone $taskQuery)
741 ->where('position', $newPosition)
742 ->first();
743
744 if ($exists) {
745 self::reIndexTasksPositions($this->toArray());
746
747 return $this->moveBetweenTasks($prevTaskId, $nextTaskId);
748 }
749
750 $this->position = $newPosition;
751 $this->save();
752
753 return $this;
754 }
755
756 private function getTaskOrderingQuery()
757 {
758 if (isset($this->parent_id)) {
759 return self::where('parent_id', $this->parent_id)
760 ->whereNull('archived_at')
761 ->where('id', '!=', $this->id);
762 }
763
764 return self::where('stage_id', $this->stage_id)
765 ->whereNull('archived_at')
766 ->where('id', '!=', $this->id);
767 }
768
769 public static function reIndexTasksPositions($task)
770 {
771 if (isset($task['parent_id'])) {
772 $tasksQuery = self::where('parent_id', $task['parent_id']);
773 } else {
774 $tasksQuery = self::where('stage_id', $task['stage_id']);
775 }
776 $allTasks = $tasksQuery->orderBy('position', 'asc')
777 ->whereNull('archived_at')->get();
778
779 foreach ($allTasks as $index => $task) {
780 $task->position = $index + 1;
781 $task->save();
782 }
783 }
784
785
786 public static function adjustSubtaskCount($subTaskParentId)
787 {
788 if ( ! $subTaskParentId) {
789 return;
790 }
791 $parentTask = Task::find($subTaskParentId);
792 if( !$parentTask) {
793 return;
794 }
795 $subtasks = $parentTask->subtasks;
796 $parentTaskSettings = $parentTask->settings;
797 $parentTaskSettings['subtask_count'] = $subtasks->count();
798 $parentTaskSettings['subtask_completed_count'] = $subtasks->filter(function ($subtask) {
799 return $subtask->status === 'closed';
800 })->count();
801 $parentTask->settings = $parentTaskSettings;
802 $parentTask->save();
803 }
804
805 public function close()
806 {
807 if ($this->status == 'closed' && $this->last_completed_at) {
808 return $this;
809 }
810
811 $this->status = 'closed';
812 $this->last_completed_at = current_time('mysql');
813 $this->save();
814 if ($this->parent_id) {
815 self::adjustSubtaskCount($this->parent_id);
816 }
817 return $this;
818 }
819
820 public function reopen()
821 {
822 if ($this->status == 'open' && $this->last_completed_at == null) {
823 return $this;
824 }
825
826 $this->status = 'open';
827 $this->last_completed_at = NULL;
828 $this->save();
829 if ($this->parent_id) {
830 self::adjustSubtaskCount($this->parent_id);
831 }
832 return $this;
833 }
834
835 /*
836 * Get all the fields that can be mapped
837 * to Task Creation Webhook or
838 * REST API Task Creation
839 * PHP API Task Creation
840 * @return array
841 */
842 public static function mappables()
843 {
844 return [
845 'task_title' => __('Task Title', 'fluent-boards'),
846 'slug' => __('Slug', 'fluent-boards'),
847 'board_title' => __('Board Title', 'fluent-boards'),
848 'status' => __('Status', 'fluent-boards'),
849 'type' => __('Type', 'fluent-boards'),
850 'description' => __('Description', 'fluent-boards'),
851 'priority' => __('Priority', 'fluent-boards'),
852 'due_at' => __('Due Date', 'fluent-boards'),
853 'started_at' => __('Start Date', 'fluent-boards'),
854 'archived_at' => __('Archive Date', 'fluent-boards'),
855 'stage' => __('Stage', 'fluent-boards'),
856 'board' => __('Board', 'fluent-boards'),
857 'source' => __('Source', 'fluent-boards'),
858 'position' => __('Position', 'fluent-boards'),
859 'subtasks' => __('Subtasks', 'fluent-boards'),
860 'completion' => __('Completion', 'fluent-boards'),
861 ];
862 }
863 public static function mappableFields()
864 {
865 $fields = [
866 'title' => [
867 'field' => __('Title', 'fluent-boards'),
868 'type' => 'text',
869 'rules' => 'required',
870 'description' => __('Title of the task.', 'fluent-boards'),
871 ],
872 'stage' => [
873 'field' => __('Stage', 'fluent-boards'),
874 'type' => 'int|text',
875 'rules' => 'optional',
876 'description' => __('The stage of the task, which can be an ID, title, or slug. Example: 1 | "open" | "Open"', 'fluent-boards'),
877 ],
878 'parent_id' => [
879 'field' => __('Parent Task', 'fluent-boards'),
880 'type' => 'int',
881 'rules' => 'optional',
882 'description' => __('Parent Task ID of the subtask. Example: 1', 'fluent-boards'),
883 ],
884 'status' => [
885 'field' => __('Status', 'fluent-boards'),
886 'type' => 'text',
887 'rules' => 'optional',
888 'description' => __('The status of the task (open | closed). Example: "closed"', 'fluent-boards'),
889 ],
890 'description' => [
891 'field' => __('Description', 'fluent-boards'),
892 'type' => 'textarea',
893 'rules' => 'optional',
894 'description' => __('Description of the task', 'fluent-boards'),
895 ],
896 'priority' => [
897 'field' => __('Priority', 'fluent-boards'),
898 'type' => 'text',
899 'rules' => 'optional',
900 'description' => __('Priority of the task (urgent | high | medium | low). Leave empty for no priority. Example: "medium"', 'fluent-boards'),
901 ],
902 'due_at' => [
903 'field' => __('Due Date', 'fluent-boards'),
904 'type' => 'date',
905 'rules' => 'optional',
906 'description' => __('The due date of the task in the format YYYY-MM-DD hh:mm. Example: 2099-12-31 23:59:59', 'fluent-boards'),
907 ],
908 'started_at' => [
909 'field' => __('Start Date', 'fluent-boards'),
910 'type' => 'date',
911 'rules' => 'optional',
912 'description' => __('The start date of the task in the format YYYY-MM-DD. Example: 2099-12-31', 'fluent-boards'),
913 ],
914 'source' => [
915 'field' => __('Source', 'fluent-boards'),
916 'type' => 'text',
917 'rules' => 'optional',
918 'description' => __('The source of the task. Example: "jira"', 'fluent-boards'),
919 ],
920 'source_id' => [
921 'field' => __('Source Id', 'fluent-boards'),
922 'type' => 'text|int',
923 'rules' => 'optional',
924 'description' => __('The source Id of the task (if any). Example: "bcy664fh177"', 'fluent-boards'),
925 ],
926 'crm_contact_id' => [
927 'field' => __('CRM Contact Id', 'fluent-boards'),
928 'type' => 'int',
929 'rules' => 'optional',
930 'description' => __('The ID of the associated FluentCRM contact. Example: 6465', 'fluent-boards'),
931 ],
932 'contact_email' => [
933 'field' => __('CRM Contact Email', 'fluent-boards'),
934 'type' => 'text',
935 'rules' => 'optional',
936 'description' => __('The email of the associated CRM contact. Example: "[email protected]"', 'fluent-boards'),
937 ],
938 'contact_first_name' => [
939 'field' => __('Contact First Name', 'fluent-boards'),
940 'type' => 'text',
941 'rules' => 'optional',
942 'description' => __('Associated CRM Contact First Name. Example: "John"', 'fluent-boards'),
943 ],
944 'contact_last_name' => [
945 'field' => __('Contact Last Name', 'fluent-boards'),
946 'type' => 'text',
947 'rules' => 'optional',
948 'description' => __('Associated CRM Contact Last Name', 'fluent-boards'),
949 ],
950 'labels' => [
951 'field' => __('Labels', 'fluent-boards'),
952 'type' => 'text|int',
953 'rules' => 'optional',
954 'description' => __('An array of label IDs or titles. Example: [1, "feature", 44]', 'fluent-boards'),
955 ],
956 'assignees' => [
957 'field' => __('Assignees', 'fluent-boards'),
958 'type' => 'text|int',
959 'rules' => 'optional',
960 'description' => __('An array of WP User IDs. Example: [1,2,44]', 'fluent-boards'),
961 ]
962 ];
963
964 return $fields;
965 }
966
967 public function customFields()
968 {
969 return $this->belongsToMany(
970 CustomField::class,
971 'fbs_relations',
972 'object_id',
973 'foreign_id'
974 )->withPivot('settings')
975 ->wherePivot('object_type', ProConstant::TASK_CUSTOM_FIELD)
976 ->withTimestamps();
977 }
978
979
980 public function repeatTaskMeta()
981 {
982 return $this->hasOne(Meta::class, 'object_id', 'id')->where('object_type', Constant::REPEAT_TASK_META);
983 }
984
985 /** Reuse batch-loaded repeat settings, including a known missing relation. */
986 public function getRepeatTaskMetaAttribute()
987 {
988 if ($this->relationLoaded('repeatTaskMeta')) {
989 return $this->getRelation('repeatTaskMeta');
990 }
991
992 return $this->repeatTaskMeta()->first();
993 }
994 public function taskCustomFields()
995 {
996 return $this->hasMany(Relation::class, 'object_id', 'id')
997 ->where('object_type', Constant::TASK_CUSTOM_FIELD);
998 }
999
1000 public function subtasks()
1001 {
1002 return $this->hasMany(Task::class, 'parent_id', 'id');
1003 }
1004
1005 public function subtaskGroup()
1006 {
1007 return $this->hasMany(TaskMeta::class, 'task_id')->where('key', Constant::SUBTASK_GROUP_NAME);
1008 }
1009
1010 }
1011