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

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