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

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