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

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