PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.95
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.95
2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 1.45 All 41 releases
fluent-boards / app / Services / BoardService.php

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

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