PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.95.3
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.95.3
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 / Services / BoardService.php

BoardService.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 1.95.3, at app/Services/BoardService.php

1,371 lines 45.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\Services;
4
5 use FluentBoards\App\Models\Activity;
6 use FluentBoards\App\Models\Board;
7 use FluentBoards\App\Models\Comment;
8 use FluentBoards\App\Models\Label;
9 use FluentBoards\App\Models\Meta;
10 use FluentBoards\App\Models\Relation;
11 use FluentBoards\App\Models\Stage;
12 use FluentBoards\App\Models\Task;
13 use FluentBoards\App\Models\TaskMeta;
14 use FluentBoards\App\Models\User;
15 use FluentBoards\App\Services\Libs\FileSystem;
16 use FluentBoardsPro\App\Models\Folder;
17
18 class BoardService
19 {
20 public function getBoardsByType($type)
21 {
22 return Board::where('type', sanitize_text_field($type))
23 ->whereNull('archived_at')
24 ->byAccessUser(get_current_user_id())
25 ->orderBy('created_at', 'ASC')
26 ->get();
27 }
28
29 public function deleteBoard($boardId)
30 {
31 $board = Board::findOrFail($boardId);
32
33 $options = null;
34 //if we need to do something before a board is deleted
35 do_action('fluent_boards/before_board_deleted', $board, $options);
36
37 //related task delete, task related relations delete
38 $allTaskIdsInBoard = $board->tasks->pluck('id');
39 $taskRelatedRelations = Relation::whereIn('object_id', $allTaskIdsInBoard);
40 $taskRelatedRelations->delete();
41 TaskMeta::whereIn('task_id', $allTaskIdsInBoard)->delete();
42
43 // Delete time tracking records for all tasks in the board
44 (new TaskService())->deleteTimeTrackingRecords($allTaskIdsInBoard->toArray());
45
46 Task::whereIn('id', $allTaskIdsInBoard)->delete();
47
48 // delete all activities
49 Activity::whereIn('object_id', $allTaskIdsInBoard)->where('object_type', Constant::ACTIVITY_TASK)->delete();
50 $board->activities()->delete();
51
52 //removing all Board Settings
53 $board->boardUserEmailNotificationSettings()->detach();
54 $board->boardUserNotificationSettings()->detach();
55 //removing all Board users
56 $board->users()->detach();
57
58 //removing add board stages
59 $board->stages()->delete();
60
61 //removing add board labels
62 $board->labels()->delete();
63
64 //removing all board comments (delete individually to fire model events and clean up images)
65 $comments = $board->comments()->get();
66 foreach ($comments as $comment) {
67 $comment->delete();
68 }
69
70 //removing add board custom fields
71 if (defined('FLUENT_BOARDS_PRO')) {
72 $board->customFields()->delete();
73 }
74
75
76 foreach ($board->notifications as $notification) {
77 $notification->users()->detach();
78 }
79 $board->notifications()->delete();
80 $board->removeBoardFromFolder();
81
82 //delete board related meta
83 $this->deleteBoardMeta($boardId);
84
85 //delete from recently viewed
86 $this->deleteFromRecentlyViewed($boardId);
87
88 //delete webhook data
89 $this->deleteWebhookData($boardId);
90
91 $board->delete();
92 FileSystem::deleteDir('board_'.$boardId);
93 }
94
95 public function fetchBoardMeta($boardId)
96 {
97 $boardMeta = Meta::where('object_id', $boardId)
98 ->where('object_type', 'board')
99 ->where('key', 'is_auth_require')
100 ->orderBy('id', 'desc')->first();
101
102 if ($boardMeta) {
103 $boardMeta->value = maybe_unserialize($boardMeta->value);
104 return $boardMeta;
105 } else {
106 $meta = new Meta();
107 $settingData = array(
108 'is_auth_require_idea_submit' => '',
109 'is_auth_require_voting_commenting' => '',
110 'is_auth_require_reaction' => '',
111 'is_allow_email_along_with_auth' => '',
112 'is_allow_unauthentication_reaction_along_with_auth' => ''
113 );
114 $meta->object_id = $boardId;
115 $meta->object_type = 'board';
116 $meta->key = 'is_auth_require';
117 $meta->value = \maybe_serialize($settingData);
118 $meta->save();
119 $meta->value = $settingData;
120 return $meta;
121 }
122 }
123
124 public function modifyAuthenticationPermission($data, $boardId)
125 {
126 $boardMeta = Meta::where('object_id', $boardId)
127 ->where('object_type', 'board')
128 ->where('key', 'is_auth_require')
129 ->orderBy('id', 'desc')->first();
130
131 if ($boardMeta) {
132 $settings = array(
133 'is_auth_require_idea_submit' => $data['is_auth_require_idea_submit'],
134 'is_auth_require_voting_commenting' => $data['is_auth_require_voting_commenting'],
135 'is_auth_require_reaction' => $data['is_auth_require_reaction'],
136 'is_allow_email_along_with_auth' => $data['is_allow_email_along_with_auth'],
137 'is_allow_unauthentication_reaction_along_with_auth' => $data['is_allow_unauthentication_reaction_along_with_auth']
138 );
139 $boardMeta->value = \maybe_serialize($settings);
140 $boardMeta->save();
141 }
142 return $boardMeta;
143 }
144
145 public function createBoard($boardData)
146 {
147 $boardData = [
148 'title' => $boardData['title'],
149 'type' => $boardData['type'] ? $boardData['type'] : 'to-do',
150 'description' => $boardData['description'],
151 'currency' => isset($boardData['currency']) ? $boardData['currency'] : 'USD',
152 'background' => isset($boardData['background']) ? $boardData['background'] : '',
153 'created_by' => isset($boardData['created_by']) ? $boardData['created_by'] : get_current_user_id()
154 ];
155
156 $boardData = apply_filters('fluent_boards/before_create_board', $boardData);
157
158 $board = Board::create($boardData);
159
160 $this->setCurrentUserPreferencesOnBoardCreate($board);
161
162 return $board;
163 }
164
165 public function setCurrentUserPreferencesOnBoardCreate($board)
166 {
167 $board->users()->attach(
168 $board->created_by,
169 [
170 'object_type' => Constant::OBJECT_TYPE_BOARD_USER,
171 'settings' => maybe_serialize([
172 Constant::IS_BOARD_ADMIN => true
173 ]),
174 'preferences' => maybe_serialize(Constant::BOARD_NOTIFICATION_TYPES)
175 ]
176 );
177 }
178
179 public function removeUserFromBoard($boardId, $userId)
180 {
181
182 $board = Board::findOrFail($boardId);
183 $user = User::findOrFail($userId);
184
185 $board->users()->detach($userId);
186 $board->boardUserNotificationSettings()->detach($userId); //removing notification settings of user in that board
187 $board->boardUserEmailNotificationSettings()->detach($userId); //removing email notification settings of user in that board
188
189 //detacing all tasks of this board from user
190 $taskIdsToDetach = $user->tasks()->where('board_id', $boardId)->get()->pluck('id');
191
192 $user->tasks()->detach($taskIdsToDetach);
193 $user->watchingTasks()->detach($taskIdsToDetach);
194
195 }
196
197 private function removeFromDefaultAssignee($boardId, $user)
198 {
199 $stages = Stage::where('board_id', $boardId)->get();
200 foreach ($stages as $stage) {
201 if (isset($stage->settings['default_task_assignees'])) {
202 if (($key = array_search($user, $stage->settings['default_task_assignees'])) !== false) {
203 unset($stage->settings['default_task_assignees'][$key]);
204 }
205 }
206 }
207 }
208
209 public function removeFromRecentlyOpened($boardId, $userId)
210 {
211 $recentlyOpened = Meta::where('object_id', $userId)
212 ->where('object_type', Constant::OBJECT_TYPE_USER)
213 ->where('key', Constant::USER_RECENT_BOARDS)
214 ->first();
215 if ($recentlyOpened) {
216 $recentBoardIds = $recentlyOpened->value;
217
218 $index = array_search($boardId, $recentBoardIds);
219 array_splice($recentBoardIds, $index, 1);
220
221 $recentlyOpened->value = $recentBoardIds;
222 $recentlyOpened->save();
223 }
224
225 }
226
227 public function updateBoard($board, $data)
228 {
229 if ($data['title']) {
230 $data['title'] = $data['title'];
231 } else {
232 throw new \Exception(esc_html__('Title cannot be empty', 'fluent-boards'));
233 }
234 if (isset($data['description'])) {
235 $data['description'] = $data['description'];
236 }
237 $board->fill($data);
238 $board->save();
239 // do_action('fluent_boards/board_updated', $board);
240 return $board;
241 }
242
243 public function defaultStages()
244 {
245 $stages = [
246 (object)[
247 'group' => 'open',
248 'label' => 'Open',
249 ],
250 (object)[
251 'group' => 'in_progress',
252 'label' => 'In Progress',
253 ],
254 (object)[
255 'group' => 'completed',
256 'label' => 'Completed',
257 ],
258 ];
259
260 return serialize($this->processStages($stages));
261 }
262
263
264 public function repositionStages($boardId, $incomingList)
265 {
266 $oldList = Stage::where('board_id', $boardId)->where('type', 'stage')->whereNull('archived_at')->orderBy('position')->pluck('id');
267
268 foreach ($incomingList as $key => $stage_id) {
269 $stage = Stage::findOrFail($stage_id);
270 $stage->moveToNewPosition($key + 1);
271 }
272 do_action('fluent_boards/board_stages_reordered', $boardId, $oldList);
273 }
274
275 public function processStages($stages)
276 {
277 $processedStages = [];
278 foreach ($stages as $stage) {
279 if (is_object($stage)) {
280 $processedStages[] = (object)[
281 'group' => Helper::snake_case($stage->slug),
282 'label' => sanitize_text_field($stage->label)
283 ];
284 } else {
285 $processedStages[] = (object)[
286 'group' => Helper::snake_case($stage['group']),
287 'label' => sanitize_text_field($stage['label'])
288 ];
289 }
290 }
291 return $processedStages;
292 }
293
294 public function archiveStage($boardId, $stage)
295 {
296 $stage->archived_at = current_time('mysql');
297 $stage->position = 0;
298 $stage->save();
299
300 do_action('fluent_boards/stage_archived', $boardId, $stage); // Old hook
301 do_action('fluent_boards/stage_archived_with_tasks', $boardId, $stage); // New hook
302 return $stage;
303 }
304
305 public function restoreStage($boardId, $stage)
306 {
307 $stageService = new StageService();
308 $lastStagePosition = $stageService->getLastPositionOfStagesOfBoard($stage->board_id);
309 $stage->archived_at = null;
310 $stage->position = $lastStagePosition ? $lastStagePosition->position + 1 : 1;
311 $stage->save();
312 do_action('fluent_boards/board_stage_restored', $boardId, $stage->title); // Old hook
313 do_action('fluent_boards/stage_restored_with_tasks', $boardId, $stage); // New hook
314 return $stage;
315 }
316
317 public function getActivities($id, $data)
318 {
319 $per_page = isset($data['per_page']) ? $data['per_page'] : 40;
320 $page = isset($data['page']) ? $data['page'] : 1;
321 $activities = Activity::where('object_id', $id)->where('object_type', Constant::ACTIVITY_BOARD)->with(['user'])
322 ->orderBy('id', 'DESC')
323 ->paginate($per_page, ['*'], 'page', $page);
324
325 Helper::translateActivities($activities);
326
327 return $activities;
328 }
329
330 public function isAlreadyMember($boardId, $memberId)
331 {
332 $isAlreadyMember = Relation::where('object_id', $boardId)
333 ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
334 ->where('foreign_id', $memberId)->first();
335
336 return $isAlreadyMember ?? false;
337 }
338
339 public function addMembersInBoard($boardId, $memberId, $isViewerOnly = null)
340 {
341 $board = Board::find($boardId);
342
343 if (!$board) {
344 return false;
345 }
346 $isAlreadyMember = $this->isAlreadyMember($boardId, $memberId);
347 if($isAlreadyMember) {
348 return false;
349 }
350 $settings = Constant::BOARD_USER_SETTINGS;
351
352 if($isViewerOnly === 'yes') {
353 $settings = Constant::BOARD_USER_VIEWER_ONLY_SETTINGS;
354 }
355
356
357
358 $board->users()->attach(
359 $memberId,
360 [
361 'object_type' => Constant::OBJECT_TYPE_BOARD_USER,
362 'settings' => maybe_serialize($settings),
363 'preferences' => maybe_serialize(Constant::BOARD_NOTIFICATION_TYPES)
364 ]
365 );
366 $boardMember = User::find($memberId);
367 if(!$isViewerOnly) {
368 do_action('fluent_boards/board_member_added', $boardId, $boardMember);
369 } else {
370 do_action('fluent_boards/board_viewer_added', $boardId, $boardMember);
371 }
372 return $boardMember;
373 }
374
375 public function makeAdminOfBoard($boardId, $userId)
376 {
377 $boardUser = Relation::where('object_id', $boardId)
378 ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
379 ->where('foreign_id', $userId)->first();
380 $boardUser->settings = [
381 'is_admin' => true
382 ];
383 $boardUser->save();
384
385 $user = User::findOrFail($userId);
386 do_action('fluent_boards/board_admin_added', $boardId, $userId);
387 $user['is_admin'] = true;
388 $user['is_board_admin'] = true;
389 return $user;
390 }
391
392 public function removeAdminFromBoard($boardId, $userId)
393 {
394 $boardUser = Relation::where('object_id', $boardId)
395 ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
396 ->where('foreign_id', $userId)->first();
397
398 $boardUser->settings = [
399 'is_admin' => false
400 ];
401
402 $boardUser->save();
403 $user = User::findOrFail($userId);
404 do_action('fluent_boards/board_admin_removed', $boardId, $userId);
405 $user['is_admin'] = false;
406 $user['is_board_admin'] = false;
407 return $user;
408 }
409
410 public function getUsersOfBoards()
411 {
412 $userBoards = Relation::whereNotNull('board_id')
413 ->where('user_id', get_current_user_id())
414 ->where('status', 'ACTIVE')->get();
415
416 return $userBoards;
417 }
418
419 /**
420 * change board background
421 * @param mixed $backgroundData
422 * @return string
423 */
424
425 public function setBoardBackground($backgroundData, $board_id)
426 {
427 $board = Board::find($board_id);
428 $oldBackground = $board->background;
429 $background = $board->background;
430
431 // if board background has color
432 if (isset($backgroundData['color'])) {
433 $background['color'] = $backgroundData['color'];
434 $background['image_url'] = null;
435 $background['is_image'] = false;
436 }
437
438 // if board background has image
439 if (isset($backgroundData['image_url'])) {
440 $background['image_url'] = $backgroundData['image_url'];
441 $background['is_image'] = true;
442 $background['color'] = null;
443 }
444 $background['id'] = $backgroundData['id'];
445
446 $board->background = $background;
447 $board->save();
448 do_action('fluent_boards/board_background_updated', $board_id, $oldBackground);
449
450 return $board->background;
451 }
452
453
454 /**
455 * Summary of getStageTaskAvailablePositions
456 * @param mixed $board_id
457 * @param mixed $stage_slug
458 * @return array of available positions of the stage with one increased value because if the stage has 10 tasks than it will have 10 position and +1 as last position of the stage
459 */
460 public function getStageTaskAvailablePositions($board_id, $stage_id, $task_id = null)
461 {
462 $task_id = absint($task_id);
463 $task = $task_id ? Task::find($task_id) : null;
464 $isCurrentStage = $task
465 && (int) $task->board_id === (int) $board_id
466 && (int) $task->stage_id === (int) $stage_id;
467
468 $stageTasks = Task::query()
469 ->where('board_id', $board_id)
470 ->where('parent_id', null)
471 ->where('stage_id', $stage_id)
472 ->whereNull('archived_at')
473 ->orderBy('position', 'asc')
474 ->get(['id', 'position']);
475
476 if ($isCurrentStage) {
477 $stageTasks = $stageTasks->filter(function ($stageTask) use ($task_id) {
478 return (int) $stageTask->id !== $task_id;
479 })->values();
480 }
481
482 $availablePositions = [];
483 $moveTargets = [];
484 $currentMoveTargetKey = null;
485 $totalSlots = $stageTasks->count() + 1;
486 $currentSlot = $this->getCurrentStageSlotIndex($task, $stageTasks, $isCurrentStage);
487
488 for ($slotIndex = 0; $slotIndex < $totalSlots; $slotIndex++) {
489 $slotNumber = $slotIndex + 1;
490 // Each slot represents a drop target between two ordered tasks, so the
491 // modal can send exact neighbour ids instead of a fragile display index.
492 $prevTask = $slotIndex > 0 ? $stageTasks->get($slotIndex - 1) : null;
493 $nextTask = $slotIndex < $stageTasks->count() ? $stageTasks->get($slotIndex) : null;
494 $slotKey = 'slot_' . $slotNumber;
495
496 $availablePositions[] = $slotNumber;
497 $moveTargets[] = [
498 'key' => $slotKey,
499 'label' => $slotNumber,
500 'prevTaskId' => $prevTask ? (int) $prevTask->id : null,
501 'nextTaskId' => $nextTask ? (int) $nextTask->id : null,
502 'isCurrent' => $isCurrentStage && $currentSlot === $slotNumber,
503 ];
504
505 if ($isCurrentStage && $currentSlot === $slotNumber) {
506 $currentMoveTargetKey = $slotKey;
507 }
508 }
509
510 return [
511 'availablePositions' => $availablePositions,
512 'moveTargets' => $moveTargets,
513 'currentMoveTargetKey' => $currentMoveTargetKey,
514 'defaultMoveTargetKey' => 'slot_' . $totalSlots,
515 ];
516 }
517
518 private function getCurrentStageSlotIndex($task, $stageTasks, $isCurrentStage)
519 {
520 if (!$isCurrentStage || !$task) {
521 return null;
522 }
523
524 $slotNumber = 1;
525 foreach ($stageTasks as $stageTask) {
526 if ((float) $task->position > (float) $stageTask->position) {
527 $slotNumber++;
528 continue;
529 }
530
531 break;
532 }
533
534 return $slotNumber;
535 }
536
537 public function getAssigneesByBoard($board_id, $search = '')
538 {
539 $assignees = [];
540 $boardUsers = [];
541 $board = Board::with('users')->find($board_id);
542
543 if ($board) {
544 if ($search) {
545 $boardUsers = $board->users->filter(
546 function ($user) use ($search) {
547 return strpos($user->display_name, $search) !== false || strpos($user->user_email, $search) !== false;
548 }
549 );
550 } else {
551 $boardUsers = $board->users;
552 }
553 };
554 foreach ($boardUsers as $user) {
555 $taskAssignee = Relation::where('foreign_id', $user->ID)->where('object_type', 'task_assignee')->exists();
556 if ($taskAssignee) {
557 $assignees[] = $user;
558 }
559 }
560 return $assignees;
561 }
562
563 private function deleteFromRecentlyViewed($boardId)
564 {
565 $recentlyOpened = $this->recentlyViewedByUserQuery()->first();
566 if ($recentlyOpened) {
567 $recentBoardIds = $recentlyOpened->value;
568 if (in_array($boardId, $recentBoardIds)) {
569 $index = array_search($boardId, $recentBoardIds);
570 unset($recentBoardIds[$index]);
571 $recentlyOpened->value = $recentBoardIds;
572 $recentlyOpened->save();
573 }
574 }
575 }
576
577 public function updateRecentBoards($boardId)
578 {
579 $userId = get_current_user_id();
580 $recentlyOpened = $this->recentlyViewedByUserQuery($userId)->first();
581 if (!$recentlyOpened) {
582 $openedBoards = [$boardId];
583 $userMeta = new Meta();
584 $userMeta->object_id = $userId;
585 $userMeta->object_type = Constant::OBJECT_TYPE_USER;
586 $userMeta->key = Constant::USER_RECENT_BOARDS;
587 $userMeta->value = $openedBoards;
588 $userMeta->save();
589 } else {
590 $recentBoardIds = $recentlyOpened->value;
591 // Ensure the value is an array
592 if (!is_array($recentBoardIds)) {
593 $recentBoardIds = [];
594 }
595
596 // Check if the board is already in the list
597 if (!in_array($boardId, $recentBoardIds)) {
598 // Keep the 4 most recently opened boards for the dashboard view.
599 if (count($recentBoardIds) >= 4) {
600 array_pop($recentBoardIds);
601 }
602 } else {
603 // Remove the existing board id to move it to the front
604 $index = array_search($boardId, $recentBoardIds);
605 unset($recentBoardIds[$index]);
606 }
607 // Add the board to the beginning of the list
608 array_unshift($recentBoardIds, $boardId);
609
610 // Update the meta value and save it
611 $recentlyOpened->value = $recentBoardIds;
612 $recentlyOpened->save();
613 }
614 }
615
616 public function recentlyViewedByUserQuery($userId = null)
617 {
618 if (!$userId) {
619 $userId = get_current_user_id();
620 }
621
622 return Meta::query()->where('object_id', $userId)
623 ->where('object_type', Constant::OBJECT_TYPE_USER)
624 ->where('key', Constant::USER_RECENT_BOARDS);
625 }
626
627 public function getRecentBoards()
628 {
629 $userId = get_current_user_id();
630
631 $recentBoardIds = $this->recentlyViewedByUserQuery($userId)->value('value');
632
633 if (!$recentBoardIds) {
634 return [];
635 }
636
637 if (!is_array($recentBoardIds)) {
638 $recentBoardIds = [];
639 }
640
641 $currentUser = User::find($userId);
642
643 if (!PermissionManager::isAdmin($userId)){
644 $recentBoardIds = array_intersect($recentBoardIds, $currentUser->whichBoards->pluck('id')->toArray());
645 }
646
647 $recentBoardIds = array_values(array_slice($recentBoardIds, 0, 4));
648
649 // This is for checking if that board is exists
650 // TODO: we will remove this code in future version
651 if (!$this->recentBoardBackwardCompatibilityCheck()) {
652 foreach ($recentBoardIds as $index => $boardId) {
653 $board = Board::find($boardId);
654 if (!$board) {
655 $this->deleteFromRecentlyViewed($boardId);
656 unset($recentBoardIds[$index]);
657 }
658 }
659
660 $this->updateRecentBoardCheckMeta();
661 }
662
663 return Board::whereIn('id', $recentBoardIds)->withCount('completedTasks')->with(['stages', 'users'])->get();
664 }
665
666 public function getRecentBoardCheckMeta($userId = null){
667 if (!$userId) {
668 $userId = get_current_user_id();
669 }
670
671 return Meta::where('object_id', $userId)
672 ->where('object_type', Constant::OBJECT_TYPE_USER)
673 ->where('key', Constant::FBS_RECENTLY_VIEWED_CHECK)
674 ->first();
675 }
676
677 private function recentBoardBackwardCompatibilityCheck() {
678 $userId = get_current_user_id();
679
680 $checkedMeta = $this->getRecentBoardCheckMeta($userId);
681
682 if (!$checkedMeta) {
683 $recentBoardCheck = new Meta();
684 $recentBoardCheck->object_id = $userId;
685 $recentBoardCheck->object_type = Constant::OBJECT_TYPE_USER;
686 $recentBoardCheck->key = Constant::FBS_RECENTLY_VIEWED_CHECK;
687 $recentBoardCheck->value = 'no';
688 $recentBoardCheck->save();
689
690 return false;
691 } else {
692 if ($checkedMeta->value == 'yes') {
693 return true;
694 } else {
695 return false;
696 }
697 }
698 }
699
700 private function updateRecentBoardCheckMeta()
701 {
702 $checkedMeta = $this->getRecentBoardCheckMeta();
703
704 if ($checkedMeta) {
705 $checkedMeta->value = 'yes';
706 $checkedMeta->save();
707 }
708 }
709
710 public function updateAssociateMember($contactId, $boardId)
711 {
712 $contactOfBoard = $this->getAssociateMember($boardId, true);
713
714 if ($contactOfBoard) {
715 $contactOfBoard->value = $contactId;
716 $contactOfBoard->save();
717 } else {
718 $contactOfBoard = new Meta();
719 $contactOfBoard->object_id = $boardId;
720 $contactOfBoard->object_type = Constant::OBJECT_TYPE_BOARD;
721 $contactOfBoard->key = Constant::BOARD_ASSOCIATED_CRM_CONTACT;
722 $contactOfBoard->value = $contactId;
723 $contactOfBoard->save();
724 }
725
726 $board = Board::findOrFail($boardId);
727 do_action('fluent_boards/contact_added_to_board', $board, $contactId);
728
729 }
730
731 public function getAssociateMember($boardId, $fromUpdateMethod = false)
732 {
733
734 $contactOfBoard = Meta::query()->where('object_id', $boardId)
735 ->where('object_type', Constant::OBJECT_TYPE_BOARD)
736 ->where('key', Constant::BOARD_ASSOCIATED_CRM_CONTACT)
737 ->first();
738
739 if ($fromUpdateMethod) {
740 return $contactOfBoard;
741 }
742
743 if (!$contactOfBoard) {
744 return null;
745 }
746
747 return Helper::crm_contact($contactOfBoard->value);
748
749 // return \FluentCrm\App\Models\Subscriber::find($contactOfBoard->value);
750 }
751
752 public function deleteAssociateMember($boardId, $contact_id)
753 {
754 $contactOfBoard = Meta::query()->where('object_id', $boardId)
755 ->where('object_type', Constant::OBJECT_TYPE_BOARD)
756 ->where('key', Constant::BOARD_ASSOCIATED_CRM_CONTACT)
757 ->where('value', $contact_id)
758 ->first();
759
760 $contactOfBoard->delete();
761 }
762
763 public function sendInvitationToBoard($boardId, $email)
764 {
765 $user = User::query()->where('user_email', $email)->first();
766
767 if ($user) {
768 return $user;
769 }
770
771 $current_user_id = get_current_user_id();
772
773 do_action('fluent_boards/send_invitation', $boardId, $email, $current_user_id);
774
775 return;
776
777 }
778
779 public function getInvitations($boardId)
780 {
781 return Meta::query()->where('object_id', $boardId)
782 ->where('object_type', Constant::OBJECT_TYPE_BOARD)
783 ->where('key', Constant::BOARD_INVITATION)
784 ->get();
785 }
786
787 public function deleteInvitation($invitationId)
788 {
789 Meta::findOrFail($invitationId)->delete();
790 }
791
792 public function hasDataChanged($boardId, $includeArchived = false, $since = null)
793 {
794 $stages = [];
795 $labels = [];
796 $tasks = [];
797 $syncStartedAt = current_time('mysql');
798 $isCursorRequest = !empty($since);
799 $forceFullSync = false;
800
801 if ($isCursorRequest) {
802 $lastUpdated = $this->normalizeSyncCursor($since, $syncStartedAt);
803 $forceFullSync = !$lastUpdated;
804 } else {
805 $oneMinuteAgoTimestamp = current_time('timestamp') - 60;
806 $lastUpdated = date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp);
807 }
808
809 $board = Board::find($boardId);
810 if (!$board) {
811 throw new \Exception(esc_html__("Board doesn't exists", 'fluent-boards'));
812 }
813 $boardUpdatedAt = $this->formatSyncTimestamp($board->updated_at);
814 $boardChanged = !$isCursorRequest || $forceFullSync || $boardUpdatedAt >= $lastUpdated;
815
816 // Reset the local list if a change can remove an item from the user's current view.
817 $stageActivityQuery = Activity::where('object_id', $boardId)
818 ->where('object_type', Constant::ACTIVITY_BOARD)
819 ->where('updated_at', '>=', $lastUpdated)
820 ->where('column', 'stage');
821
822 $stageResetRequired = $forceFullSync || (clone $stageActivityQuery)
823 ->whereIn('action', ['deleted', 'archived', 'restored'])
824 ->exists();
825
826 if ($stageResetRequired) {
827 $stagesQuery = Stage::where('board_id', $boardId)->orderBy('position', 'asc');
828 if (!$includeArchived) {
829 $stagesQuery->whereNull('archived_at');
830 }
831 $stages = $stagesQuery->get();
832 } else {
833 $stages = (new StageService())->getLastOneMinuteUpdatedStages($boardId, $lastUpdated, $includeArchived);
834 }
835
836 $labelResetRequired = $forceFullSync || Activity::where('object_id', $boardId)
837 ->where('object_type', Constant::ACTIVITY_BOARD)
838 ->where('updated_at', '>=', $lastUpdated)
839 ->where('action', 'deleted')
840 ->where('column', 'label')
841 ->exists();
842 if ($labelResetRequired) {
843 $labelsQuery = Label::where('board_id', $boardId)->orderBy('position', 'asc');
844 if (!$includeArchived) {
845 $labelsQuery->whereNull('archived_at');
846 }
847 $labels = $labelsQuery->get();
848 } else {
849 $labels = (new LabelService())->getLastOneMinuteUpdatedLabels($boardId, $lastUpdated, $includeArchived);
850 }
851
852 $stageArchiveRestored = !$forceFullSync && (clone $stageActivityQuery)
853 ->whereIn('action', ['archived', 'restored'])
854 ->exists();
855
856 $taskResetRequired = $forceFullSync || $stageArchiveRestored || Activity::where('object_id', $boardId)
857 ->where('object_type', Constant::ACTIVITY_BOARD)
858 ->where('updated_at', '>=', $lastUpdated)
859 ->where(function($query) {
860 $query->where('action', 'deleted')
861 ->orWhere('action', 'moved')
862 ->orWhere('action', 'archived')
863 ->orWhere('action', 'restored');
864 })
865 ->where('column', 'task')
866 ->exists();
867 if ($taskResetRequired) {
868 $tasksQuery = Task::query()
869 ->where([
870 'board_id' => $boardId,
871 'parent_id' => null,
872 ])
873 ->with(['assignees', 'labels', 'watchers']);
874
875 if (!$includeArchived) {
876 $tasksQuery->whereNull('archived_at');
877 }
878
879 if (!!defined('FLUENT_BOARDS_PRO_VERSION')) {
880 $tasksQuery->with('customFields');
881 }
882
883 $tasks = $tasksQuery->orderBy('due_at', 'ASC')->get();
884 } else {
885 $tasks = (new TaskService())->getLastOneMinuteUpdatedTasks($boardId, $lastUpdated, $includeArchived);
886 }
887
888 foreach ($tasks as $task) {
889 $task->isOverdue = $task->isOverdue();
890 $task->isUpcoming = $task->upcoming();
891 $task->is_watching = $task->isWatching();
892 $task->contact = Helper::crm_contact($task->crm_contact_id);
893 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
894 $task->watchers = Helper::sanitizeUserCollections($task->watchers);
895 }
896
897 $board->background = \maybe_unserialize($board->background);
898 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
899 $board->custom_fields = $board->customFields;
900 }
901
902 $boardPayload = $boardChanged ? $board : (object) [];
903 $hasChanges = $boardChanged
904 || $stageResetRequired
905 || $labelResetRequired
906 || $taskResetRequired
907 || count($stages)
908 || count($labels)
909 || count($tasks);
910
911 return [
912 'board' => $boardPayload,
913 'stages' => $stages,
914 'labels' => $labels,
915 'tasks' => $tasks,
916 'taskDeleted' => $taskResetRequired,
917 'stageDeleted' => $stageResetRequired,
918 'labelDeleted' => $labelResetRequired,
919 'taskResetRequired' => $taskResetRequired,
920 'stageResetRequired' => $stageResetRequired,
921 'labelResetRequired' => $labelResetRequired,
922 'has_changes' => (bool) $hasChanges,
923 'synced_at' => $syncStartedAt,
924 'sync_reset' => $forceFullSync,
925 ];
926 }
927
928 private function normalizeSyncCursor($since, $syncStartedAt)
929 {
930 if (!is_string($since)) {
931 return null;
932 }
933
934 $since = trim($since);
935 if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $since)) {
936 return null;
937 }
938
939 if ($since > $syncStartedAt) {
940 return null;
941 }
942
943 if (strtotime($since) < strtotime('-24 hours', strtotime($syncStartedAt))) {
944 return null;
945 }
946
947 return $since;
948 }
949
950 private function formatSyncTimestamp($timestamp)
951 {
952 if ($timestamp instanceof \DateTimeInterface) {
953 return $timestamp->format('Y-m-d H:i:s');
954 }
955
956 return (string) $timestamp;
957 }
958
959 public function getAssociatedBoards($associatedId, $userId = null)
960 {
961 $associatedId = absint($associatedId);
962 $userId = $userId ?: get_current_user_id();
963
964 if (!$associatedId || !$userId) {
965 return [];
966 }
967
968 $boardIds = Meta::query()->where('value', $associatedId)
969 ->where('object_type', Constant::OBJECT_TYPE_BOARD)
970 ->where('key', Constant::BOARD_ASSOCIATED_CRM_CONTACT)
971 ->pluck('object_id');
972
973 return Board::query()
974 ->whereIn('id', $boardIds)
975 ->byAccessUser($userId)
976 ->with('stages', 'users')
977 ->get();
978 }
979
980 private function deleteBoardMeta($boardId)
981 {
982 Meta::where('object_id', $boardId)
983 ->where('object_type', Constant::OBJECT_TYPE_BOARD)
984 ->delete();
985 }
986
987 public function copyBoard($boardData)
988 {
989 $sourceBoard = Board::findOrFail($boardData['source_board_id']);
990 $boardData['background'] = $sourceBoard->background;
991 $boardData = apply_filters('fluent_boards/before_create_board', $boardData);
992
993 $board = Board::create($boardData);
994
995 $this->setCurrentUserPreferencesOnBoardCreate($board);
996
997 return $board;
998 }
999
1000 public function getBoardReports($board_id)
1001 {
1002 $board = Board::findOrFail($board_id);
1003 $taskQuery = Task::where('board_id', $board_id)
1004 ->whereNull('parent_id')
1005 ->whereNull('archived_at');
1006
1007 if($board->type == 'roadmap') {
1008 $pendingStage = $this->getNewIdeaStage($board->id);
1009 return $this->getIdeaReports($taskQuery, $pendingStage);
1010 } else {
1011 return $this->getTaskReports($taskQuery);
1012 }
1013 }
1014
1015 private function getNewIdeaStage($boardId)
1016 {
1017 return Stage::where('board_id', $boardId)
1018 ->where('type', 'stage')
1019 ->where('archived_at', null)
1020 ->orderBy('position', 'ASC')
1021 ->first();
1022 }
1023
1024 public function getAllBoardReports(){
1025 $userId = get_current_user_id();
1026
1027 $taskQuery = Task::whereNull('parent_id')
1028 ->whereNull('archived_at')
1029 ->whereHas('board', function ($query) {
1030 $query->where('type', 'to-do');
1031 });
1032
1033 if (!PermissionManager::isAdmin($userId))
1034 {
1035 $currentUser = User::find($userId);
1036 $relatedBoardIds = $currentUser->whichBoards->where('type', 'to-do')->pluck('id');
1037 $taskQuery->whereIn('board_id', $relatedBoardIds);
1038 }
1039
1040 return $this->getTaskReports($taskQuery);
1041 }
1042
1043 private function getTaskReports($taskQuery)
1044 {
1045 $totalTasksQuery = clone $taskQuery;
1046 $completedTaskQuery = clone $taskQuery;
1047 $openTaskQuery = clone $taskQuery;
1048 $overDueTaskQuery = clone $taskQuery;
1049
1050 $completedTaskCount = $completedTaskQuery->where('status', 'closed')->count();
1051 $openTaskCount = $openTaskQuery->where('status', 'open')->count();
1052 $overDueTasks = $overDueTaskQuery->overdue(true)->count();
1053 $totalTasks = $totalTasksQuery->count();
1054
1055 $taskQuery->where('status', 'open');
1056
1057 $highQuery = clone $taskQuery;
1058 $mediumQuery = clone $taskQuery;
1059 $lowQuery = clone $taskQuery;
1060
1061 $high = $highQuery->where('priority', 'high')->count();
1062 $low = $mediumQuery->where('priority', 'low')->count();
1063 $medium = $lowQuery->where('priority', 'medium')->count();
1064
1065 $reportData = [
1066 'completion' => [
1067 'completed' => $completedTaskCount,
1068 'incomplete' => $openTaskCount,
1069 'overdue' => $overDueTasks,
1070 'total' => $totalTasks
1071 ],
1072 'priority' => [
1073 'high' => $high,
1074 'medium' => $medium,
1075 'low' => $low
1076 ]
1077
1078 ];
1079 return $reportData;
1080 }
1081
1082 private function getIdeaReports($taskQuery, $pendingStage)
1083 {
1084 $pendingIdeaQuery = clone $taskQuery;
1085 $completedIdeaQuery = clone $taskQuery;
1086 $openIdeaQueryPage = clone $taskQuery;
1087 $openIdeaQueryWeb = clone $taskQuery;
1088
1089 $pendingIdeaCount = $pendingIdeaQuery->where('status', 'open')->where('stage_id', $pendingStage->id)->count();
1090 $completedIdeaCount = $completedIdeaQuery->where('status', 'closed')->count();
1091 $openIdeaCountPage = $openIdeaQueryPage->where('status', 'open')->where('source', 'page')->count();
1092 $openIdeaCountWeb = $openIdeaQueryWeb->where('status', 'open')->where('source', 'web')->count();
1093 $totalIdeas = $openIdeaCountPage + $openIdeaCountWeb;
1094
1095 $taskQuery->where('status', 'open');
1096
1097 $highQuery = clone $taskQuery;
1098 $mediumQuery = clone $taskQuery;
1099 $lowQuery = clone $taskQuery;
1100
1101 $high = $highQuery->where('priority', 'high')->count();
1102 $low = $mediumQuery->where('priority', 'low')->count();
1103 $medium = $lowQuery->where('priority', 'medium')->count();
1104
1105 $reportData = [
1106 'completion' => [
1107 'pending' => $pendingIdeaCount,
1108 'completed' => $completedIdeaCount,
1109 'ideaFromPage' => $openIdeaCountPage,
1110 'total' => $totalIdeas
1111 ],
1112 'priority' => [
1113 'high' => $high,
1114 'medium' => $medium,
1115 'low' => $low
1116 ]
1117 ];
1118 return $reportData;
1119 }
1120
1121 public function getStageWiseBoardReports($board_id)
1122 {
1123 $stages = Stage::where('board_id', $board_id)
1124 ->where('type', 'stage')
1125 ->whereNull('archived_at')
1126 ->get();
1127
1128 foreach ($stages as $stage) {
1129 $completedTaskCount = Task::where('stage_id', $stage->id)
1130 ->where('status', 'closed')
1131 ->count();
1132
1133 $openTaskCount = Task::where('stage_id', $stage->id)
1134 ->whereNull('due_at')
1135 ->where('status', 'open')
1136 ->count();
1137
1138 $overDue = Task::where('stage_id', $stage->id)
1139 ->whereNotNull('due_at')
1140 ->where('status', 'open')
1141 ->overdue(true)
1142 ->count();
1143
1144 $stage->report = [
1145 'completed' => $completedTaskCount,
1146 'incomplete' => $openTaskCount,
1147 'overdue' => $overDue
1148 ];
1149 }
1150
1151 return $stages;
1152 }
1153
1154 public function archiveBoard($boardId)
1155 {
1156 $board = Board::findOrFail($boardId);
1157 $board->archived_at = current_time('mysql');
1158 $board->save();
1159
1160 do_action('fluent_boards/board_archived', $board);
1161 return $board;
1162 }
1163
1164 public function restoreBoard($boardId)
1165 {
1166 $board = Board::findOrFail($boardId);
1167 $board->archived_at = null;
1168 $board->save();
1169
1170 do_action('fluent_boards/board_restored', $board);
1171 return $board;
1172 }
1173
1174 public function makeMember($boardId, $userId)
1175 {
1176 $boardUser = Relation::where('object_id', $boardId)
1177 ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
1178 ->where('foreign_id', $userId)->first();
1179
1180 $boardUser->settings = [
1181 'is_admin' => false,
1182 'is_viewer_only' => false
1183 ];
1184
1185 $boardUser->save();
1186 $user = User::findOrFail($userId);
1187 do_action('fluent_boards/board_member_added', $boardId, $boardUser);
1188 $user['is_admin'] = false;
1189 $user['is_board_admin'] = false;
1190 return $user;
1191 }
1192
1193 public function makeViewer($boardId, $userId)
1194 {
1195 $boardUser = Relation::where('object_id', $boardId)
1196 ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
1197 ->where('foreign_id', $userId)->first();
1198
1199 $boardUser->settings = [
1200 'is_admin' => false,
1201 'is_viewer_only' => true
1202 ];
1203
1204 $boardUser->save();
1205 $user = User::findOrFail($userId);
1206 do_action('fluent_boards/board_viewer_added', $boardId, $boardUser);
1207 $user['is_admin'] = false;
1208 $user['is_board_admin'] = false;
1209 return $user;
1210 }
1211
1212 private function getUserWisePinnedBoards()
1213 {
1214 $userId = get_current_user_id();
1215
1216 $pinnedBoardMeta = Meta::query()->where('object_id', $userId)
1217 ->where('object_type', Constant::OBJECT_TYPE_USER)
1218 ->where('key', Constant::USER_PINNED_BOARDS)
1219 ->first();
1220
1221 return $pinnedBoardMeta;
1222 }
1223
1224 public function getPinnedBoards()
1225 {
1226 $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1227
1228 if (!$pinnedBoardMeta) {
1229 return [];
1230 } else {
1231 $ids = $pinnedBoardMeta->value;
1232
1233 // Convert to array of integers
1234 $intIds = array_map('intval', $ids);
1235
1236 return Board::whereIn('id', $intIds)
1237 ->whereNull('archived_at')
1238 ->byAccessUser(get_current_user_id())
1239 ->get();
1240 }
1241 }
1242
1243 public function pinBoard($boardId)
1244 {
1245 $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1246
1247 if ($pinnedBoardMeta) {
1248 $currentPinnedBoards = $pinnedBoardMeta->value;
1249 if (!in_array($boardId, $currentPinnedBoards)) {
1250 $currentPinnedBoards[] = $boardId;
1251 $pinnedBoardMeta->value = $currentPinnedBoards;
1252 $pinnedBoardMeta->save();
1253 }
1254 } else {
1255 // Create an empty array
1256 $boardIds = [];
1257 $boardIds[] = $boardId;
1258
1259 $meta = new Meta();
1260 $meta->object_id = get_current_user_id();
1261 $meta->object_type = Constant::OBJECT_TYPE_USER;
1262 $meta->key = Constant::USER_PINNED_BOARDS;
1263 $meta->value = $boardIds;
1264 $meta->save();
1265 }
1266 }
1267
1268 /**
1269 * @param $boardId
1270 * @return bool
1271 */
1272 public function unpinBoard($boardId)
1273 {
1274 $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1275
1276 if (!$pinnedBoardMeta) {
1277 return false;
1278 }
1279
1280 $currentPinnedBoards = $pinnedBoardMeta->value;
1281 if (in_array($boardId, $currentPinnedBoards)) {
1282 $index = array_search($boardId, $currentPinnedBoards);
1283 array_splice($currentPinnedBoards, $index, 1);
1284 $pinnedBoardMeta->value = $currentPinnedBoards;
1285 $pinnedBoardMeta->save();
1286 return true;
1287 }
1288
1289 return false;
1290 }
1291
1292 /**
1293 * @param $boardId
1294 * @return bool
1295 * If board id is in user's current pinned boards list
1296 */
1297 public function isPinned($boardId)
1298 {
1299 $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1300
1301 if (!$pinnedBoardMeta) {
1302 return false;
1303 }
1304
1305 $currentPinnedBoards = $pinnedBoardMeta->value;
1306 if (in_array($boardId, $currentPinnedBoards)) {
1307 return true;
1308 }
1309
1310 return false;
1311 }
1312
1313 public function getBoardFolder($boardId)
1314 {
1315 $relation = Relation::where('object_type', Constant::OBJECT_TYPE_FOLDER_BOARD)
1316 ->where('foreign_id', $boardId)
1317 ->first();
1318
1319 if (!$relation) {
1320 return null;
1321 }
1322
1323 return Folder::findOrFail($relation->object_id);
1324 }
1325
1326 public function deleteWebhookData($boardId)
1327 {
1328 $outgoingRelations = Relation::where('object_type', 'outgoing_webhook_board')
1329 ->where('foreign_id', $boardId)
1330 ->get();
1331
1332 foreach ($outgoingRelations as $relation) {
1333 $webhookMetaId = (int) $relation->object_id;
1334
1335 $linkedCount = Relation::where('object_type', 'outgoing_webhook_board')
1336 ->where('object_id', $webhookMetaId)
1337 ->count();
1338
1339 if ($linkedCount === 1) {
1340 Meta::where('id', $webhookMetaId)
1341 ->where('object_type', 'outgoing_webhook')
1342 ->delete();
1343 } else if ($linkedCount > 1) {
1344 $meta = Meta::find($webhookMetaId);
1345 if ($meta && $meta->object_type === 'outgoing_webhook') {
1346 $value = $meta->value;
1347
1348 if (isset($value['board_id'])) {
1349 $boards = $value['board_id'];
1350
1351 if (is_array($boards)) {
1352 $boards = array_values(array_filter($boards, function ($id) use ($boardId) {
1353 return intval($id) !== intval($boardId);
1354 }));
1355 $value['board_id'] = $boards;
1356 } else {
1357 if ($boards !== null && intval($boards) === intval($boardId)) {
1358 $value['board_id'] = [];
1359 }
1360 }
1361
1362 $meta->value = $value;
1363 $meta->save();
1364 }
1365 }
1366 }
1367 $relation->delete();
1368 }
1369 }
1370 }
1371