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

682 lines 21.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 FluentCrm\App\Models\Subscriber;
10
11 class Task extends Model
12 {
13 protected $table = 'fbs_tasks';
14
15 protected $guarded = ['id'];
16
17 protected $fillable
18 = [
19 'title',
20 'slug',
21 'board_id',
22 'parent_id',
23 'crm_contact_id',
24 'type',
25 'lead_value',
26 'stage_id',
27 'status',
28 'reminder_type',
29 'priority',
30 'archived_at',
31 'remind_at',
32 'source',
33 'source_id',
34 'description',
35 'lead_value',
36 'events',
37 'settings',
38 'due_at',
39 'started_at',
40 'last_completed_at',
41 'position',
42 'comments_count',
43 'created_by',
44 ];
45
46 protected $appends = ['meta'];
47
48 public static function boot()
49 {
50 parent::boot();
51 static::creating(function ($model) {
52 $model->created_by = $model->created_by ?: get_current_user_id();
53 $model->type = $model->type
54 ?: 'task'; // default task type is task
55
56 if (empty($model->slug)) {
57 $model->slug = sanitize_title($model->title, 'idea-'.time());
58 }
59
60 $model->settings = $model->settings
61 ?: [
62 'cover' => [
63 'backgroundColor' => '',
64 ],
65 'subtask_count' => 0,
66 'attachment_count' => 0,
67 ];
68 $model->position = $model->position
69 ?: (new TaskService())->getLastPositionOfTasks($model->stage_id);
70 });
71 static::created(function ($model) {
72 if ( ! $model->parent_id) {
73 do_action('fluent_boards/task_created', $model);
74 if ($model->crm_contact_id) {
75 do_action('fluent_boards/contact_added_to_task', $model);
76 }
77 }
78 });
79
80 /* global scope for task type which means only task_type = task will be fetched from everywhere in */
81 static::addGlobalScope('type', function (Builder $builder) {
82 $builder->where('type', '=', 'task')
83 ->orWhere('type', '=', 'roadmap');
84 });
85 }
86
87
88 public function scopeType($query, $type)
89 {
90 return $query->where('type', $type);
91 }
92
93 /**
94 * scope of getting past due not completed tasks
95 *
96 * @param $query \FluentBoards\Framework\Database\Query\Builder
97 *
98 * @return \FluentBoards\Framework\Database\Query\Builder
99 */
100 public function scopeOverdue($query)
101 {
102 return $query->whereNull('last_completed_at')
103 ->where('status', 'open')
104 ->where('due_at', '<=', current_time('mysql'));
105 }
106
107 /**
108 * scope of getting upcoming tasks
109 *
110 * @param $query \FluentBoards\Framework\Database\Query\Builder
111 *
112 * @return \FluentBoards\Framework\Database\Query\Builder
113 */
114 public function scopeUpcoming($query)
115 {
116 return $query->where('status', 'open')
117 ->where('due_at', '>=', current_time('mysql'));
118 }
119
120
121 public function setSettingsAttribute($settings)
122 {
123 $this->attributes['settings'] = \maybe_serialize($settings);
124 }
125
126 public function getSettingsAttribute($settings)
127 {
128 return \maybe_unserialize($settings);
129 }
130
131
132 /**
133 * One2Many: Task has many activities
134 *
135 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
136 */
137 public function activities()
138 {
139 return $this->hasMany(Activity::class, 'object_id', 'id')
140 ->where('object_type', Constant::ACTIVITY_TASK)
141 ->orderBy('id', 'DESC');
142 }
143
144 //->orderBy('id', 'DESC')
145
146 /**
147 * One2Many: Task has many activities
148 *
149 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
150 */
151 public function comments()
152 {
153 return $this->hasMany(Comment::class, 'task_id', 'id');
154 }
155
156 /**
157 * One2Many: Task has many notifications
158 *
159 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
160 */
161 public function notifications()
162 {
163 return $this->hasMany(Notification::class, 'task_id', 'id');
164 }
165
166 /**
167 * One2Many: Task has many activities
168 *
169 * @return \FluentBoards\Framework\Database\Orm\Relations\hasMany
170 */
171 public function public_comments() // for primarily - roadmap plugin
172 {
173 return $this->hasMany(Comment::class, 'task_id', 'id')
174 ->where('privacy', 'public')
175 ->where('status', 'published');
176 }
177
178 public function board()
179 {
180 return $this->belongsTo(Board::class, 'board_id', 'id');
181 }
182
183 public function assignees()
184 {
185 return $this->belongsToMany(
186 User::class,
187 'fbs_relations',
188 'object_id',
189 'foreign_id'
190 )->withPivot('settings', 'preferences')
191 ->wherePivot('object_type',
192 Constant::OBJECT_TYPE_TASK_ASSIGNEE)
193 ->withTimestamps();
194 }
195
196 public function labels()
197 {
198 return $this->belongsToMany(
199 BoardTerm::class,
200 'fbs_relations',
201 'object_id',
202 'foreign_id'
203 )->withPivot('settings')
204 ->wherePivot('object_type',
205 Constant::OBJECT_TYPE_TASK_LABEL)
206 ->withTimestamps();
207 }
208
209 public function attachments() //may not need in future
210 {
211 return $this->hasMany(\FluentBoardsPro\App\Models\Attachment::class,
212 'object_id', 'id')
213 ->where('object_type', Constant::OBJECT_TYPE_TASK);
214 }
215
216 public function watchers()
217 {
218 return $this->belongsToMany(
219 User::class,
220 'fbs_relations',
221 'object_id',
222 'foreign_id'
223 )->withPivot('settings')
224 ->wherePivot('object_type',
225 Constant::OBJECT_TYPE_USER_TASK_WATCH)
226 ->withTimestamps();
227 }
228
229 public function parentTask($id)
230 {
231 return self::find($id);
232 }
233
234 public function contact()
235 {
236 return $this->belongsTo(Subscriber::class, 'crm_contact_id', 'id');
237 }
238
239 public function taskMeta()
240 {
241 return $this->hasMany(TaskMeta::class, 'task_id', 'id');
242 }
243
244 public function getMetaAttribute()
245 {
246 return $this->taskMeta()->get()->pluck('value', 'key');
247 }
248
249 public function stage()
250 {
251 return $this->belongsTo(Stage::class, 'stage_id', 'id');
252 }
253
254 public function isOverdue()
255 {
256 return $this->last_completed_at == null && $this->due_at
257 && strtotime($this->due_at, current_time('timestamp'))
258 <= current_time('timestamp');
259 }
260
261 public function upcoming()
262 {
263 return $this->last_completed_at == null && $this->due_at
264 && strtotime($this->due_at, current_time('timestamp'))
265 >= current_time('timestamp');
266 }
267
268 public function isWatching()
269 {
270 $userId = get_current_user_id();
271 $is_watching = false;
272 foreach ($this->watchers as $watcher) {
273 if ($watcher->ID == $userId) {
274 $is_watching = true;
275 }
276 }
277
278 return $is_watching;
279 }
280
281 public function createTask($data)
282 {
283 $createdTask = Task::create($data);
284
285 if ( ! empty($data['assignees'])) {
286 $assignees = array_filter(array_map('intval', $data['assignees']));
287 if ($assignees) {
288 $assigneeData = array_fill_keys($assignees,
289 ['object_type' => Constant::OBJECT_TYPE_TASK_ASSIGNEE]);
290 $createdTask->assignees()->syncWithoutDetaching($assigneeData);
291 }
292 }
293
294 if ( ! empty($data['labels'])) {
295 $labels = array_filter(array_map('intval', $data['labels']));
296 if ($labels) {
297 foreach ($labels as $label) {
298 $createdTask->labels()->syncWithoutDetaching([$label => ['object_type' => Constant::OBJECT_TYPE_TASK_LABEL]]);
299 }
300 }
301 }
302
303 return $createdTask;
304 }
305
306 public function addOrRemoveAssignee($idToAddOrRemove)
307 {
308 $oldAssigneeIds = $this->assignees->pluck('ID')->toArray();
309 $IfAlreadyAssignee = in_array($idToAddOrRemove, $oldAssigneeIds);
310 $operation = 'added';
311
312 if ($IfAlreadyAssignee) { // if already an assignee then it is a remove operation
313 $this->assignees()->detach($idToAddOrRemove);
314 $this->watchers()->detach($idToAddOrRemove);
315 $operation = 'removed';
316 } else { //else an add operation
317 $this->assignees()
318 ->syncWithoutDetaching([$idToAddOrRemove => ['object_type' => Constant::OBJECT_TYPE_TASK_ASSIGNEE]]);
319 $this->watchers()
320 ->syncWithoutDetaching([$idToAddOrRemove => ['object_type' => Constant::OBJECT_TYPE_USER_TASK_WATCH]]);
321 }
322
323 return $operation;
324 }
325
326 public function getArchivedAttribute()
327 {
328 return (bool) $this->attributes['is_archived'];
329 }
330
331 public function setArchivedAttribute($value)
332 {
333 if (true === $value) {
334 $this->attributes['is_archived'] = 1;
335 } elseif (false === $value) {
336 $this->attributes['is_archived'] = 0;
337 } else {
338 $this->attributes['is_archived'] = in_array($value, [0, 1]) ? $value
339 : 0;
340 }
341 }
342
343 public function user($id)
344 {
345 return User::findOrFail($id);
346 }
347
348 /**
349 * from now(01/03/24) we will use Helper::crm_contact() method
350 */
351 public static function lead_contact($id)
352 {
353 if ( ! defined('FLUENTCRM')) {
354 return '';
355 }
356
357 $contact = \FluentCrm\App\Models\Subscriber::with(['tags', 'lists'])
358 ->find($id);
359
360 if ( ! $contact) {
361 return null;
362 }
363
364 return [
365 'id' => $contact->id,
366 'email' => $contact->email,
367 'first_name' => $contact->first_name,
368 'last_name' => $contact->last_name,
369 'full_name' => $contact->full_name,
370 'avatar' => $contact->avatar,
371 'photo' => $contact->photo,
372 'status' => $contact->status,
373 'contact_type' => $contact->contact_type,
374 'last_activity' => $contact->last_activity,
375 'life_time_value' => $contact->life_time_value,
376 'total_points' => $contact->total_points,
377 'user_id' => $contact->user_id,
378 'created_at' => $contact->created_at,
379 'tags' => Helper::getIdTitleArray($contact->tags),
380 'lists' => Helper::getIdTitleArray($contact->lists),
381
382 ];
383
384 }
385
386 public function getMeta($key, $default = null)
387 {
388 $exist = TaskMeta::where('task_id', $this->id)->where('key', $key)
389 ->first();
390 if ($exist) {
391 return $exist->value;
392 }
393
394 return $default;
395 }
396
397 public function updateMeta($key, $value)
398 {
399 $exist = TaskMeta::where('task_id', $this->id)->where('key', $key)
400 ->first();
401
402 if ($exist) {
403 $exist->value = $value;
404 $exist->save();
405 } else {
406 $exist = TaskMeta::create([
407 'task_id' => $this->id,
408 'key' => $key,
409 'value' => $value,
410 ]);
411 }
412
413 return $exist;
414 }
415
416 public function moveToNewPosition($newIndex)
417 {
418 $newIndex = (int) $newIndex;
419 if ($newIndex < 1) {
420 $newIndex = 1;
421 }
422
423 // Declaring query for subtask or task
424 if (isset($this->parent_id)) {
425 $taskQuery = self::where('parent_id', $this->parent_id)
426 ->whereNull('archived_at');
427 } else {
428 $taskQuery = self::where('stage_id', $this->stage_id)
429 ->whereNull('archived_at');
430 }
431
432
433 if ($newIndex == 1) {
434 $firstItem = $taskQuery->where('id', '!=', $this->id)
435 ->orderBy('position', 'asc')
436 ->first();
437
438 if ($firstItem) {
439 if ($firstItem->position < 0.02) {
440 self::reIndexTasksPositions($this->toArray());
441
442 return $this->moveToNewPosition($newIndex);
443 }
444 $index = round($firstItem->position / 2, 2);
445 } else {
446 $index = 1;
447 }
448
449 $this->position = $index;
450 $this->save();
451
452 return $this;
453 }
454
455 $prevTask = $taskQuery
456 ->offset($newIndex - 2)
457 ->where('id', '!=', $this->id)
458 ->orderBy('position', 'asc')
459 ->first();
460
461 if ( ! $prevTask) {
462 return $this->moveToNewPosition(1);
463 }
464
465 $nextItem = $taskQuery
466 ->offset($newIndex - 1)
467 ->where('id', '!=', $this->id)
468 ->orderBy('position', 'asc')
469 ->first();
470
471 if ( ! $nextItem) {
472 $this->position = $prevTask->position + 1;
473 $this->save();
474
475 return $this;
476 }
477
478 $newPosition = ($prevTask->position + $nextItem->position) / 2;
479
480 // check if new position is already taken
481 $exist = $taskQuery
482 ->where('position', $newPosition)
483 ->where('id', '!=', $this->id)
484 ->first();
485
486 if ($exist) {
487 self::reIndexTasksPositions($this->toArray());
488
489 return $this->moveToNewPosition($newIndex);
490 }
491
492 $this->position = $newPosition;
493 $this->save();
494
495 return $this;
496 }
497
498 public static function reIndexTasksPositions($task)
499 {
500 if (isset($task['parent_id'])) {
501 $tasksQuery = self::where('parent_id', $task['parent_id']);
502 } else {
503 $tasksQuery = self::where('stage_id', $task['stage_id']);
504 }
505 $allTasks = $tasksQuery->orderBy('position', 'asc')
506 ->whereNull('archived_at')->get();
507
508 foreach ($allTasks as $index => $task) {
509 $task->position = $index + 1;
510 $task->save();
511 }
512 }
513
514
515 public static function adjustSubtaskCount($subTaskParentId)
516 {
517 if ( ! $subTaskParentId) {
518 return;
519 }
520 $parentTask = Task::findOrFail($subTaskParentId);
521
522 if ($parentTask->settings
523 && array_key_exists('subtask_count', $parentTask->settings)
524 ) {
525 $parentTaskSettings = $parentTask->settings;
526 $count = $parentTaskSettings['subtask_count'] - 1 > 0 ?? 0;
527 $parentTaskSettings['subtask_count'] = $count;
528 $parentTask->settings = $parentTaskSettings;
529 $parentTask->save();
530 }
531 }
532
533 public function close()
534 {
535 if ($this->status == 'closed') {
536 return $this;
537 }
538
539 $this->status = 'closed';
540 $this->last_completed_at = current_time('mysql');
541 $this->save();
542
543 return $this;
544 }
545
546 public function reopen()
547 {
548 if ($this->status == 'open') {
549 return $this;
550 }
551
552 $this->status = 'open';
553 $this->last_completed_at = null;
554 $this->save();
555
556 return $this;
557 }
558
559 /*
560 * Get all the fields that can be mapped
561 * to Task Creation Webhook or
562 * REST API Task Creation
563 * PHP API Task Creation
564 * @return array
565 */
566 public static function mappables()
567 {
568 return [
569 'title' => __('Title', 'fluent-boards'),
570 'description' => __('Description', 'fluent-boards'),
571 'priority' => __('Priority', 'fluent-boards'),
572 'due_at' => __('Due Date', 'fluent-boards'),
573 'stage' => __('Stage', 'fluent-boards'),
574 'board' => __('Board', 'fluent-boards'),
575 'source' => __('Source', 'fluent-boards'),
576 ];
577 }
578 public static function mappableFields()
579 {
580 $fields = [
581 'title' => [
582 'field' => __('Title', 'fluent-boards'),
583 'type' => 'text',
584 'rules' => 'required',
585 'description' => __('Title of the task.', 'fluent-boards'),
586 ],
587 'stage' => [
588 'field' => __('Stage', 'fluent-boards'),
589 'type' => 'int|text',
590 'rules' => 'optional',
591 'description' => __('The stage of the task, which can be an ID, title, or slug. Example: 1 | "open" | "Open"', 'fluent-boards'),
592 ],
593 'parent_id' => [
594 'field' => __('Parent Task', 'fluent-boards'),
595 'type' => 'int',
596 'rules' => 'optional',
597 'description' => __('Parent Task ID of the subtask. Example: 1', 'fluent-boards'),
598 ],
599 'status' => [
600 'field' => __('Status', 'fluent-boards'),
601 'type' => 'text',
602 'rules' => 'optional',
603 'description' => __('The status of the task (open | closed). Example: "closed"', 'fluent-boards'),
604 ],
605 'description' => [
606 'field' => __('Description', 'fluent-boards'),
607 'type' => 'textarea',
608 'rules' => 'optional',
609 'description' => __('Description of the task', 'fluent-boards'),
610 ],
611 'priority' => [
612 'field' => __('Priority', 'fluent-boards'),
613 'type' => 'text',
614 'rules' => 'optional',
615 'description' => __('Priority of the task (low | medium | high). Example: "medium" ', 'fluent-boards'),
616 ],
617 'due_at' => [
618 'field' => __('Due Date', 'fluent-boards'),
619 'type' => 'date',
620 'rules' => 'optional',
621 'description' => __('The due date of the task in the format YYYY-MM-DD hh:mm. Example: 2099-12-31 23:59:59', 'fluent-boards'),
622 ],
623 'started_at' => [
624 'field' => __('Start Date', 'fluent-boards'),
625 'type' => 'date',
626 'rules' => 'optional',
627 'description' => __('The start date of the task in the format YYYY-MM-DD. Example: 2099-12-31', 'fluent-boards'),
628 ],
629 'source' => [
630 'field' => __('Source', 'fluent-boards'),
631 'type' => 'text',
632 'rules' => 'optional',
633 'description' => __('The source of the task. Example: "jira"', 'fluent-boards'),
634 ],
635 'source_id' => [
636 'field' => __('Source Id', 'fluent-boards'),
637 'type' => 'text|int',
638 'rules' => 'optional',
639 'description' => __('The source Id of the task (if any). Example: "bcy664fh177"', 'fluent-boards'),
640 ],
641 'crm_contact_id' => [
642 'field' => __('CRM Contact Id', 'fluent-boards'),
643 'type' => 'int',
644 'rules' => 'optional',
645 'description' => __('The ID of the associated FluentCRM contact. Example: 6465', 'fluent-boards'),
646 ],
647 'contact_email' => [
648 'field' => __('CRM Contact Email', 'fluent-boards'),
649 'type' => 'text',
650 'rules' => 'optional',
651 'description' => __('The email of the associated CRM contact. Example: "john.doe@example.com"', 'fluent-boards'),
652 ],
653 'contact_first_name' => [
654 'field' => __('Contact First Name', 'fluent-boards'),
655 'type' => 'text',
656 'rules' => 'optional',
657 'description' => __('Associated CRM Contact First Name. Example: "John"', 'fluent-boards'),
658 ],
659 'contact_last_name' => [
660 'field' => __('Contact Last Name', 'fluent-boards'),
661 'type' => 'text',
662 'rules' => 'optional',
663 'description' => __('Associated CRM Contact Last Name', 'fluent-boards'),
664 ],
665 'labels' => [
666 'field' => __('Labels', 'fluent-boards'),
667 'type' => 'text|int',
668 'rules' => 'optional',
669 'description' => __('An array of label IDs or titles. Example: [1, "feature", 44]', 'fluent-boards'),
670 ],
671 'assignees' => [
672 'field' => __('Assignees', 'fluent-boards'),
673 'type' => 'text|int',
674 'rules' => 'optional',
675 'description' => __('An array of WP User IDs. Example: [1,2,44]', 'fluent-boards'),
676 ]
677 ];
678
679 return $fields;
680 }
681 }
682