PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
← All changes | app/Services/BoardService.php +769 -203 1.212.1.0 View file →
@@ -2,23 +2,33 @@
2 2
3 3 namespace FluentBoards\App\Services;
4 4
5 5 use FluentBoards\App\Models\Activity;
6 -use FluentBoards\App\Models\Stage;
7 -use FluentBoards\App\Models\User;
6 +use FluentBoards\App\Models\Attachment;
8 7 use FluentBoards\App\Models\Board;
8 +use FluentBoards\App\Models\Comment;
9 +use FluentBoards\App\Models\Folder;
10 +use FluentBoards\App\Models\Label;
11 +use FluentBoards\App\Models\Meta;
9 12 use FluentBoards\App\Models\Relation;
13 +use FluentBoards\App\Models\Stage;
10 14 use FluentBoards\App\Models\Task;
11 -use FluentBoards\App\Models\Meta;
12 15 use FluentBoards\App\Models\TaskMeta;
16 +use FluentBoards\App\Models\User;
13 17 use FluentBoards\App\Services\Libs\FileSystem;
14 -use FluentBoards\Framework\Support\Arr;
18 +use FluentBoards\App\Services\DescriptionMarkdownConverter;
15 19
16 20 class BoardService
17 21 {
22 + private const LEGACY_BOARD_ASSOCIATED_CRM_CONTACT = 'crm_contact';
23 +
18 24 public function getBoardsByType($type)
19 25 {
20 - return Board::where('type', $type)->whereNull('archived_at')->orderBy('created_at', 'ASC')->get();
26 + return Board::where('type', sanitize_text_field($type))
27 + ->whereNull('archived_at')
28 + ->byAccessUser(get_current_user_id())
29 + ->orderBy('created_at', 'ASC')
30 + ->get();
21 31 }
22 32
23 33 public function deleteBoard($boardId)
24 34 {
@@ -33,9 +43,16 @@
33 43 $taskRelatedRelations = Relation::whereIn('object_id', $allTaskIdsInBoard);
34 44 $taskRelatedRelations->delete();
35 45 TaskMeta::whereIn('task_id', $allTaskIdsInBoard)->delete();
36 46
47 + // Delete time tracking records for all tasks in the board
48 + (new TaskService())->deleteTimeTrackingRecords($allTaskIdsInBoard->toArray());
49 +
37 50 Task::whereIn('id', $allTaskIdsInBoard)->delete();
51 +
52 + // delete all activities
53 + Activity::whereIn('object_id', $allTaskIdsInBoard)->where('object_type', Constant::ACTIVITY_TASK)->delete();
54 + $board->activities()->delete();
38 55
39 56 //removing all Board Settings
40 57 $board->boardUserEmailNotificationSettings()->detach();
41 58 $board->boardUserNotificationSettings()->detach();
@@ -47,15 +64,25 @@
47 64
48 65 //removing add board labels
49 66 $board->labels()->delete();
50 67
51 - //removing add board comment
52 - $board->comments()->delete();
68 + //removing all board comments (delete individually to fire model events and clean up images)
69 + $comments = $board->comments()->get();
70 + foreach ($comments as $comment) {
71 + $comment->delete();
72 + }
73 +
74 + //removing add board custom fields
75 + if (defined('FLUENT_BOARDS_PRO')) {
76 + $board->customFields()->delete();
77 + }
53 78
79 +
54 80 foreach ($board->notifications as $notification) {
55 81 $notification->users()->detach();
56 82 }
57 83 $board->notifications()->delete();
84 + $board->removeBoardFromFolder();
58 85
59 86 //delete board related meta
60 87 $this->deleteBoardMeta($boardId);
61 88
@@ -61,13 +88,13 @@
61 88
62 89 //delete from recently viewed
63 90 $this->deleteFromRecentlyViewed($boardId);
64 91
92 + //delete webhook data
93 + $this->deleteWebhookData($boardId);
94 +
65 95 $board->delete();
66 - if(!defined('FLUENT_BOARDS_PRO')){
67 - FileSystem::deleteDir('board_'.$boardId);
68 - }
69 -// do_action('fluent_boards/board_deleted', $board);
96 + FileSystem::deleteDir('board_'.$boardId);
70 97 }
71 98
72 99 public function fetchBoardMeta($boardId)
73 100 {
@@ -122,12 +149,13 @@
122 149 public function createBoard($boardData)
123 150 {
124 151 $boardData = [
125 152 'title' => $boardData['title'],
126 - 'type' => $boardData['type'] ? $boardData['type'] : 'todo',
127 - 'description' => $boardData['description'],
153 + 'type' => $boardData['type'] ? $boardData['type'] : 'to-do',
154 + 'description' => DescriptionMarkdownConverter::normalize($boardData['description'] ?? ''),
128 155 'currency' => isset($boardData['currency']) ? $boardData['currency'] : 'USD',
129 156 'background' => isset($boardData['background']) ? $boardData['background'] : '',
157 + 'created_by' => isset($boardData['created_by']) ? $boardData['created_by'] : get_current_user_id()
130 158 ];
131 159
132 160 $boardData = apply_filters('fluent_boards/before_create_board', $boardData);
133 161
@@ -137,12 +165,23 @@
137 165
138 166 return $board;
139 167 }
140 168
141 - private function setCurrentUserPreferencesOnBoardCreate($board)
169 + /**
170 + * Attach a user-owned board to its creator with Board Admin preferences.
171 + *
172 + * @param Board $board
173 + * @return void
174 + */
175 + public function setCurrentUserPreferencesOnBoardCreate($board)
142 176 {
177 + $creatorId = absint($board->created_by);
178 + if (!$creatorId) {
179 + return;
180 + }
181 +
143 182 $board->users()->attach(
144 - get_current_user_id(),
183 + $creatorId,
145 184 [
146 185 'object_type' => Constant::OBJECT_TYPE_BOARD_USER,
147 186 'settings' => maybe_serialize([
148 187 Constant::IS_BOARD_ADMIN => true
@@ -167,11 +206,22 @@
167 206
168 207 $user->tasks()->detach($taskIdsToDetach);
169 208 $user->watchingTasks()->detach($taskIdsToDetach);
170 209
171 -// do_action('fluent_boards/board_member_removed', $boardId, $user->display_name);
172 210 }
173 211
212 + private function removeFromDefaultAssignee($boardId, $user)
213 + {
214 + $stages = Stage::where('board_id', $boardId)->get();
215 + foreach ($stages as $stage) {
216 + if (isset($stage->settings['default_task_assignees'])) {
217 + if (($key = array_search($user, $stage->settings['default_task_assignees'])) !== false) {
218 + unset($stage->settings['default_task_assignees'][$key]);
219 + }
220 + }
221 + }
222 + }
223 +
174 224 public function removeFromRecentlyOpened($boardId, $userId)
175 225 {
176 226 $recentlyOpened = Meta::where('object_id', $userId)
177 227 ->where('object_type', Constant::OBJECT_TYPE_USER)
@@ -179,9 +229,18 @@
179 229 ->first();
180 230 if ($recentlyOpened) {
181 231 $recentBoardIds = $recentlyOpened->value;
182 232
233 + // Recently opened meta can be empty or legacy-shaped; only splice a usable board ID list.
234 + if (!is_array($recentBoardIds)) {
235 + return;
236 + }
237 +
183 238 $index = array_search($boardId, $recentBoardIds);
239 + if ($index === false) {
240 + return;
241 + }
242 +
184 243 array_splice($recentBoardIds, $index, 1);
185 244
186 245 $recentlyOpened->value = $recentBoardIds;
187 246 $recentlyOpened->save();
@@ -193,12 +252,12 @@
193 252 {
194 253 if ($data['title']) {
195 254 $data['title'] = $data['title'];
196 255 } else {
197 - throw new \Exception('Title cannot be empty');
256 + throw new \Exception(esc_html__('Title cannot be empty', 'fluent-boards'));
198 257 }
199 258 if (isset($data['description'])) {
200 - $data['description'] = $data['description'];
259 + $data['description'] = DescriptionMarkdownConverter::normalize($data['description']);
201 260 }
202 261 $board->fill($data);
203 262 $board->save();
204 263 // do_action('fluent_boards/board_updated', $board);
@@ -224,50 +283,10 @@
224 283
225 284 return serialize($this->processStages($stages));
226 285 }
227 286
228 - public function changePositionOfStage($boardId, $changeData)
229 - {
230 - $allStages = Stage::where('board_id', $boardId)
231 - ->where('is_archived', 0)
232 - ->where('type', 'stage')
233 - ->orderBy('position', 'asc')
234 - ->get();
235 287
236 - $changedStage = null;
237 -
238 - foreach ($allStages as $stage) {
239 - if ($changeData['fromPosition'] < $changeData['toPosition']) {
240 - if ($stage['position'] == $changeData['fromPosition']) {
241 - $stage['position'] = (int)$changeData['toPosition'];
242 - $stage->save();
243 - $changedStage = $stage;
244 - } elseif (
245 - $stage['position'] > $changeData['fromPosition'] &&
246 - $stage['position'] <= $changeData['toPosition']
247 - ) {
248 - $stage['position'] = $stage->position - 1;
249 - $stage->save();
250 - }
251 - } else {
252 - if (
253 - $stage['position'] >= $changeData['toPosition'] &&
254 - $stage['position'] < $changeData['fromPosition']
255 - ) {
256 - $stage['position'] = $stage->position + 1;
257 - $stage->save();
258 - } elseif ($stage['position'] == $changeData['fromPosition']) {
259 - $stage['position'] = (int)$changeData['toPosition'];
260 - $stage->save();
261 - $changedStage = $stage;
262 - }
263 - }
264 - }
265 -
266 - do_action('fluent_boards/board_stage_dragged', $changedStage);
267 - }
268 -
269 - public function rePositionStages($boardId, $incomingList)
288 + public function repositionStages($boardId, $incomingList)
270 289 {
271 290 $oldList = Stage::where('board_id', $boardId)->where('type', 'stage')->whereNull('archived_at')->orderBy('position')->pluck('id');
272 291
273 292 foreach ($incomingList as $key => $stage_id) {
@@ -295,26 +314,42 @@
295 314 }
296 315 return $processedStages;
297 316 }
298 317
299 - public function archiveStage($id, $stage)
318 + /**
319 + * Archive a stage and persist the user who archived it for future archive-list metadata.
320 + */
321 + public function archiveStage($boardId, $stage)
300 322 {
323 + $settings = $stage->settings ?: [];
324 + $settings['archived_by_id'] = absint(get_current_user_id()) ?: null;
325 +
301 326 $stage->archived_at = current_time('mysql');
302 327 $stage->position = 0;
328 + $stage->settings = $settings;
303 329 $stage->save();
304 330
305 - do_action('fluent_boards/board_stage_archived', $id, $stage->title);
331 + do_action('fluent_boards/stage_archived', $boardId, $stage); // Old hook
332 + do_action('fluent_boards/stage_archived_with_tasks', $boardId, $stage); // New hook
306 333 return $stage;
307 334 }
308 335
309 - public function restoreStage($id, $stage)
336 + /**
337 + * Restore an archived stage and clear stale archived-by metadata.
338 + */
339 + public function restoreStage($boardId, $stage)
310 340 {
311 341 $stageService = new StageService();
312 342 $lastStagePosition = $stageService->getLastPositionOfStagesOfBoard($stage->board_id);
343 + $settings = $stage->settings ?: [];
344 + $settings['archived_by_id'] = null;
345 +
313 346 $stage->archived_at = null;
314 347 $stage->position = $lastStagePosition ? $lastStagePosition->position + 1 : 1;
348 + $stage->settings = $settings;
315 349 $stage->save();
316 - do_action('fluent_boards/board_stage_restored', $id, $stage->title);
350 + do_action('fluent_boards/board_stage_restored', $boardId, $stage->title); // Old hook
351 + do_action('fluent_boards/stage_restored_with_tasks', $boardId, $stage); // New hook
317 352 return $stage;
318 353 }
319 354
320 355 public function getActivities($id, $data)
@@ -320,11 +355,15 @@
320 355 public function getActivities($id, $data)
321 356 {
322 357 $per_page = isset($data['per_page']) ? $data['per_page'] : 40;
323 358 $page = isset($data['page']) ? $data['page'] : 1;
324 - return Activity::where('object_id', $id)->where('object_type', Constant::ACTIVITY_BOARD)->with(['user'])
359 + $activities = Activity::where('object_id', $id)->where('object_type', Constant::ACTIVITY_BOARD)->with(['user'])
325 360 ->orderBy('id', 'DESC')
326 361 ->paginate($per_page, ['*'], 'page', $page);
362 +
363 + Helper::translateActivities($activities);
364 +
365 + return $activities;
327 366 }
328 367
329 368 public function isAlreadyMember($boardId, $memberId)
330 369 {
@@ -334,26 +373,55 @@
334 373
335 374 return $isAlreadyMember ?? false;
336 375 }
337 376
338 - public function addMembersInBoard($boardId, $memberId)
377 + /**
378 + * Add a WordPress user to a board.
379 + *
380 + * @return User|false|null User on success, false for an existing relation,
381 + * or null when the board/user does not exist.
382 + */
383 + public function addMembersInBoard($boardId, $memberId, $isViewerOnly = null)
339 384 {
385 + $boardId = intval($boardId);
386 + $memberId = intval($memberId);
387 + $isViewerOnly = sanitize_text_field((string)$isViewerOnly);
388 +
389 + if ($boardId <= 0 || $memberId <= 0) {
390 + return null;
391 + }
392 +
340 393 $board = Board::find($boardId);
394 + $boardMember = User::find($memberId);
341 395
342 - if (!$board) {
396 + if (!$board || !$boardMember) {
397 + return null;
398 + }
399 + $isAlreadyMember = $this->isAlreadyMember($boardId, $memberId);
400 + if($isAlreadyMember) {
343 401 return false;
344 402 }
403 + $settings = Constant::BOARD_USER_SETTINGS;
345 404
405 + if($isViewerOnly === 'yes') {
406 + $settings = Constant::BOARD_USER_VIEWER_ONLY_SETTINGS;
407 + }
408 +
409 +
410 +
346 411 $board->users()->attach(
347 412 $memberId,
348 413 [
349 414 'object_type' => Constant::OBJECT_TYPE_BOARD_USER,
350 - 'settings' => maybe_serialize(Constant::BOARD_USER_SETTINGS),
415 + 'settings' => maybe_serialize($settings),
351 416 'preferences' => maybe_serialize(Constant::BOARD_NOTIFICATION_TYPES)
352 417 ]
353 418 );
354 - $boardMember = User::find($memberId);
355 - do_action('fluent_boards/board_member_added', $boardId, $boardMember->display_name);
419 + if(!$isViewerOnly) {
420 + do_action('fluent_boards/board_member_added', $boardId, $boardMember);
421 + } else {
422 + do_action('fluent_boards/board_viewer_added', $boardId, $boardMember);
423 + }
356 424 return $boardMember;
357 425 }
358 426
359 427 public function makeAdminOfBoard($boardId, $userId)
@@ -390,8 +458,75 @@
390 458 $user['is_board_admin'] = false;
391 459 return $user;
392 460 }
393 461
462 + /**
463 + * Create or update a board access relation with the selected member role.
464 + */
465 + public function syncBoardUserRole($boardId, $userId, $role)
466 + {
467 + $boardId = absint($boardId);
468 + $userId = absint($userId);
469 + $role = sanitize_text_field($role);
470 +
471 + if (!$boardId || !$userId || !in_array($role, ['admin', 'member', 'viewer'], true)) {
472 + return false;
473 + }
474 +
475 + $board = Board::find($boardId);
476 + $user = User::find($userId);
477 +
478 + if (!$board || !$user) {
479 + return false;
480 + }
481 +
482 + $boardUser = Relation::where('object_id', $boardId)
483 + ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
484 + ->where('foreign_id', $userId)
485 + ->first();
486 +
487 + $previousSettings = $boardUser ? (array)$boardUser->settings : [];
488 +
489 + // Board roles live as flags on the board_user relation; member access means both flags stay false.
490 + $settings = [
491 + 'is_admin' => 'admin' === $role,
492 + 'is_viewer_only' => 'viewer' === $role,
493 + ];
494 +
495 + if ($boardUser) {
496 + $boardUser->settings = $settings;
497 + $boardUser->save();
498 + } else {
499 + // New access should get the same default notification preferences as the normal add-member flow.
500 + $board->users()->attach(
501 + $userId,
502 + [
503 + 'object_type' => Constant::OBJECT_TYPE_BOARD_USER,
504 + 'settings' => maybe_serialize($settings),
505 + 'preferences' => maybe_serialize(Constant::BOARD_NOTIFICATION_TYPES)
506 + ]
507 + );
508 + }
509 +
510 + // Only emit admin transition hooks when the role actually changes.
511 + if ('admin' === $role && empty($previousSettings['is_admin'])) {
512 + do_action('fluent_boards/board_admin_added', $boardId, $userId);
513 + } elseif (!empty($previousSettings['is_admin'])) {
514 + do_action('fluent_boards/board_admin_removed', $boardId, $userId);
515 + }
516 +
517 + if ('viewer' === $role) {
518 + do_action('fluent_boards/board_viewer_added', $boardId, $user);
519 + } elseif ('member' === $role) {
520 + do_action('fluent_boards/board_member_added', $boardId, $user);
521 + }
522 +
523 + $user['is_admin'] = 'admin' === $role;
524 + $user['is_board_admin'] = 'admin' === $role;
525 +
526 + return $user;
527 + }
528 +
394 529 public function getUsersOfBoards()
395 530 {
396 531 $userBoards = Relation::whereNotNull('board_id')
397 532 ->where('user_id', get_current_user_id())
@@ -400,35 +535,68 @@
400 535 return $userBoards;
401 536 }
402 537
403 538 /**
404 - * change board background
405 - * @param mixed $backgroundData
406 - * @return string
539 + * Change or clear the board background.
540 + *
541 + * Image attachments must belong to the target board and use the board
542 + * background attachment type before their identifiers can be persisted.
543 + *
544 + * @param array $backgroundData
545 + * @param int $board_id
546 + * @return array|string
547 + * @throws \Exception
407 548 */
408 -
409 549 public function setBoardBackground($backgroundData, $board_id)
410 550 {
411 - $board = Board::find($board_id);
551 + $boardId = absint($board_id);
552 + $board = Board::find($boardId);
553 +
554 + if (!$board) {
555 + throw new \Exception(esc_html__('Board not found.', 'fluent-boards'));
556 + }
557 +
558 + $oldBackground = $board->background;
559 +
560 + if (!empty($backgroundData['reset'])) {
561 + $board->background = '';
562 + $board->save();
563 + do_action('fluent_boards/board_background_updated', $boardId, $oldBackground);
564 +
565 + return $board->background;
566 + }
567 +
412 568 $background = $board->background;
413 -
414 - // if board background has color
415 - if (isset($backgroundData['color'])) {
416 - $background['color'] = $backgroundData['color'];
417 - $background['image_url'] = null;
418 - $background['is_image'] = false;
569 + if (!is_array($background)) {
570 + $background = [];
419 571 }
420 572
421 - // if board background has image
573 + // Resolve image metadata from the board-owned attachment, never from the client URL.
422 574 if (isset($backgroundData['image_url'])) {
423 - $background['image_url'] = $backgroundData['image_url'];
575 + $attachmentId = absint($backgroundData['id'] ?? 0);
576 + $attachment = Attachment::where('id', $attachmentId)
577 + ->where('object_id', $boardId)
578 + ->where('object_type', Constant::BOARD_BACKGROUND_IMAGE)
579 + ->first();
580 +
581 + if (!$attachment) {
582 + throw new \Exception(esc_html__('Background image not found.', 'fluent-boards'));
583 + }
584 +
585 + $background['id'] = (int) $attachment->id;
586 + $background['image_url'] = (new CommentService())->createPublicUrl($attachment, $boardId);
424 587 $background['is_image'] = true;
425 588 $background['color'] = null;
589 + } elseif (isset($backgroundData['color'])) {
590 + $background['id'] = $backgroundData['id'];
591 + $background['color'] = $backgroundData['color'];
592 + $background['image_url'] = null;
593 + $background['is_image'] = false;
426 594 }
427 - $background['id'] = $backgroundData['id'];
428 595
429 596 $board->background = $background;
430 597 $board->save();
598 + do_action('fluent_boards/board_background_updated', $boardId, $oldBackground);
431 599
432 600 return $board->background;
433 601 }
434 602
@@ -438,25 +606,85 @@
438 606 * @param mixed $board_id
439 607 * @param mixed $stage_slug
440 608 * @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
441 609 */
442 - public function getStageTaskAvailablePositions($board_id, $stage_id)
610 + public function getStageTaskAvailablePositions($board_id, $stage_id, $task_id = null)
443 611 {
444 - $availablePositions = Task::query()
612 + $task_id = absint($task_id);
613 + $task = $task_id ? Task::find($task_id) : null;
614 + $isCurrentStage = $task
615 + && (int) $task->board_id === (int) $board_id
616 + && (int) $task->stage_id === (int) $stage_id;
617 +
618 + $stageTasks = Task::query()
445 619 ->where('board_id', $board_id)
446 620 ->where('parent_id', null)
447 621 ->where('stage_id', $stage_id)
448 622 ->whereNull('archived_at')
449 623 ->orderBy('position', 'asc')
450 - ->get()
451 - ->pluck('position')->toArray();
624 + ->get(['id', 'position']);
452 625
453 - $totalPosition = count($availablePositions);
454 - $availablePositions[$totalPosition] = $totalPosition + 1;
626 + if ($isCurrentStage) {
627 + $stageTasks = $stageTasks->filter(function ($stageTask) use ($task_id) {
628 + return (int) $stageTask->id !== $task_id;
629 + })->values();
630 + }
455 631
456 - return $availablePositions;
632 + $availablePositions = [];
633 + $moveTargets = [];
634 + $currentMoveTargetKey = null;
635 + $totalSlots = $stageTasks->count() + 1;
636 + $currentSlot = $this->getCurrentStageSlotIndex($task, $stageTasks, $isCurrentStage);
637 +
638 + for ($slotIndex = 0; $slotIndex < $totalSlots; $slotIndex++) {
639 + $slotNumber = $slotIndex + 1;
640 + // Each slot represents a drop target between two ordered tasks, so the
641 + // modal can send exact neighbour ids instead of a fragile display index.
642 + $prevTask = $slotIndex > 0 ? $stageTasks->get($slotIndex - 1) : null;
643 + $nextTask = $slotIndex < $stageTasks->count() ? $stageTasks->get($slotIndex) : null;
644 + $slotKey = 'slot_' . $slotNumber;
645 +
646 + $availablePositions[] = $slotNumber;
647 + $moveTargets[] = [
648 + 'key' => $slotKey,
649 + 'label' => $slotNumber,
650 + 'prevTaskId' => $prevTask ? (int) $prevTask->id : null,
651 + 'nextTaskId' => $nextTask ? (int) $nextTask->id : null,
652 + 'isCurrent' => $isCurrentStage && $currentSlot === $slotNumber,
653 + ];
654 +
655 + if ($isCurrentStage && $currentSlot === $slotNumber) {
656 + $currentMoveTargetKey = $slotKey;
657 + }
658 + }
659 +
660 + return [
661 + 'availablePositions' => $availablePositions,
662 + 'moveTargets' => $moveTargets,
663 + 'currentMoveTargetKey' => $currentMoveTargetKey,
664 + 'defaultMoveTargetKey' => 'slot_' . $totalSlots,
665 + ];
457 666 }
458 667
668 + private function getCurrentStageSlotIndex($task, $stageTasks, $isCurrentStage)
669 + {
670 + if (!$isCurrentStage || !$task) {
671 + return null;
672 + }
673 +
674 + $slotNumber = 1;
675 + foreach ($stageTasks as $stageTask) {
676 + if ((float) $task->position > (float) $stageTask->position) {
677 + $slotNumber++;
678 + continue;
679 + }
680 +
681 + break;
682 + }
683 +
684 + return $slotNumber;
685 + }
686 +
459 687 public function getAssigneesByBoard($board_id, $search = '')
460 688 {
461 689 $assignees = [];
462 690 $boardUsers = [];
@@ -516,10 +744,10 @@
516 744 }
517 745
518 746 // Check if the board is already in the list
519 747 if (!in_array($boardId, $recentBoardIds)) {
520 - // If there are already 3 boards, remove the last one
521 - if (count($recentBoardIds) >= 3) {
748 + // Keep the 4 most recently opened boards for the dashboard view.
749 + if (count($recentBoardIds) >= 4) {
522 750 array_pop($recentBoardIds);
523 751 }
524 752 } else {
525 753 // Remove the existing board id to move it to the front
@@ -555,8 +783,12 @@
555 783 if (!$recentBoardIds) {
556 784 return [];
557 785 }
558 786
787 + if (!is_array($recentBoardIds)) {
788 + $recentBoardIds = [];
789 + }
790 +
559 791 $currentUser = User::find($userId);
560 792
561 793 if (!PermissionManager::isAdmin($userId)){
562 794 $recentBoardIds = array_intersect($recentBoardIds, $currentUser->whichBoards->pluck('id')->toArray());
@@ -561,8 +793,10 @@
561 793 if (!PermissionManager::isAdmin($userId)){
562 794 $recentBoardIds = array_intersect($recentBoardIds, $currentUser->whichBoards->pluck('id')->toArray());
563 795 }
564 796
797 + $recentBoardIds = array_values(array_slice($recentBoardIds, 0, 4));
798 +
565 799 // This is for checking if that board is exists
566 800 // TODO: we will remove this code in future version
567 801 if (!$this->recentBoardBackwardCompatibilityCheck()) {
568 802 foreach ($recentBoardIds as $index => $boardId) {
@@ -575,9 +809,15 @@
575 809
576 810 $this->updateRecentBoardCheckMeta();
577 811 }
578 812
579 - return Board::whereIn('id', $recentBoardIds)->withCount('completedTasks')->with(['stages', 'users'])->get();
813 + return Board::whereIn('id', $recentBoardIds)
814 + ->whereNull('archived_at')
815 + ->excludeTemplates()
816 + ->availableInCurrentInstall()
817 + ->withCount('completedTasks')
818 + ->with(['stages', 'users'])
819 + ->get();
580 820 }
581 821
582 822 public function getRecentBoardCheckMeta($userId = null){
583 823 if (!$userId) {
@@ -675,10 +915,15 @@
675 915
676 916 $contactOfBoard->delete();
677 917 }
678 918
679 - public function sendInvitationToBoard($boardId, $email)
919 + public function sendInvitationToBoard($boardId, $email, $role = 'member')
680 920 {
921 + $role = sanitize_text_field($role);
922 + if (!in_array($role, ['manager', 'member', 'viewer'], true)) {
923 + $role = 'member';
924 + }
925 +
681 926 $user = User::query()->where('user_email', $email)->first();
682 927
683 928 if ($user) {
684 929 return $user;
@@ -685,9 +930,9 @@
685 930 }
686 931
687 932 $current_user_id = get_current_user_id();
688 933
689 - do_action('fluent_boards/send_invitation', $boardId, $email, $current_user_id);
934 + do_action('fluent_boards/send_invitation', $boardId, $email, $current_user_id, $role);
690 935
691 936 return;
692 937
693 938 }
@@ -699,92 +944,242 @@
699 944 ->where('key', Constant::BOARD_INVITATION)
700 945 ->get();
701 946 }
702 947
703 - public function deleteInvitation($invitationId)
948 + /**
949 + * Delete an invitation only when it belongs to the supplied board.
950 + *
951 + * The optional second argument lets older Pro releases receive a controlled
952 + * error instead of reporting a successful deletion that never happened.
953 + */
954 + public function deleteInvitation($boardId, $invitationId = null)
704 955 {
705 - Meta::findOrFail($invitationId)->delete();
956 + if ($invitationId === null) {
957 + throw new \Exception(
958 + __('A board ID is required to delete an invitation.', 'fluent-boards')
959 + );
960 + }
961 +
962 + $boardId = intval($boardId);
963 + $invitationId = intval($invitationId);
964 +
965 + if ($boardId <= 0 || $invitationId <= 0) {
966 + return false;
967 + }
968 +
969 + return (bool) Meta::query()
970 + ->where('id', $invitationId)
971 + ->where('object_id', $boardId)
972 + ->where('object_type', Constant::OBJECT_TYPE_BOARD)
973 + ->where('key', Constant::BOARD_INVITATION)
974 + ->delete();
706 975 }
707 976
708 - public function hasDataChanged($boardId)
977 + public function hasDataChanged($boardId, $includeArchived = false, $since = null)
709 978 {
710 979 $stages = [];
711 980 $labels = [];
712 981 $tasks = [];
713 - $labels = [];
714 - $taskDeleted = false;
715 - $stageDeleted = false;
716 - $labelDeleted = false;
717 - $oneMinuteAgoTimestamp = current_time('timestamp') - 60; // Get the current timestamp and subtract 60 seconds
718 - $oneMinuteAgo = date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp); // Format the timestamp into the desired format in GMT
982 + $syncStartedAt = current_time('mysql');
983 + $isCursorRequest = !empty($since);
984 + $forceFullSync = false;
719 985
986 + if ($isCursorRequest) {
987 + $lastUpdated = $this->normalizeSyncCursor($since, $syncStartedAt);
988 + $forceFullSync = !$lastUpdated;
989 + } else {
990 + $oneMinuteAgoTimestamp = current_time('timestamp') - 60;
991 + $lastUpdated = date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp);
992 + }
993 +
720 994 $board = Board::find($boardId);
721 995 if (!$board) {
722 - throw new \Exception("Board doesn't exists");
996 + throw new \Exception(esc_html__("Board doesn't exists", 'fluent-boards'));
723 997 }
724 - // if stage in this board has been deleted
725 - $stageDeleted = Activity::where('object_id', $boardId)->where('updated_at', '>=', $oneMinuteAgo)->where('action', 'deleted')->where('column', 'stage')->exists();
998 + $boardUpdatedAt = $this->formatSyncTimestamp($board->updated_at);
999 + $boardChanged = !$isCursorRequest || $forceFullSync || $boardUpdatedAt >= $lastUpdated;
726 1000
727 - if ($stageDeleted) {
728 - $stages = $board->stages;
1001 + // Reset the local list if a change can remove an item from the user's current view.
1002 + $stageActivityQuery = Activity::where('object_id', $boardId)
1003 + ->where('object_type', Constant::ACTIVITY_BOARD)
1004 + ->where('updated_at', '>=', $lastUpdated)
1005 + ->where('column', 'stage');
1006 +
1007 + $stageResetRequired = $forceFullSync || (clone $stageActivityQuery)
1008 + ->whereIn('action', ['deleted', 'archived', 'restored'])
1009 + ->exists();
1010 +
1011 + if ($stageResetRequired) {
1012 + $stagesQuery = Stage::where('board_id', $boardId)->orderBy('position', 'asc');
1013 + if (!$includeArchived) {
1014 + $stagesQuery->whereNull('archived_at');
1015 + }
1016 + $stages = $stagesQuery->get();
729 1017 } else {
730 - $stages = (new StageService())->getLastOneMinuteUpdatedStages($boardId);
1018 + $stages = (new StageService())->getLastOneMinuteUpdatedStages($boardId, $lastUpdated, $includeArchived);
731 1019 }
732 1020
733 - $labelDeleted = Activity::where('object_id', $boardId)->where('updated_at', '>=', $oneMinuteAgo)->where('action', 'deleted')->where('column', 'label')->exists();
734 - if ($labelDeleted) {
735 - $labels = $board->labels;
1021 + $labelResetRequired = $forceFullSync || Activity::where('object_id', $boardId)
1022 + ->where('object_type', Constant::ACTIVITY_BOARD)
1023 + ->where('updated_at', '>=', $lastUpdated)
1024 + ->where('action', 'deleted')
1025 + ->where('column', 'label')
1026 + ->exists();
1027 + if ($labelResetRequired) {
1028 + $labelsQuery = Label::where('board_id', $boardId)->orderBy('position', 'asc');
1029 + if (!$includeArchived) {
1030 + $labelsQuery->whereNull('archived_at');
1031 + }
1032 + $labels = $labelsQuery->get();
736 1033 } else {
737 - $labels = (new LabelService())->getLastOneMinuteUpdatedLabels($boardId);
1034 + $labels = (new LabelService())->getLastOneMinuteUpdatedLabels($boardId, $lastUpdated, $includeArchived);
738 1035 }
739 1036
740 - // if task in this board has been deleted
741 - $taskDeletedOrMovedFormBoard = Activity::where('object_id', $boardId)->where('updated_at', '>=', $oneMinuteAgo)->where('action', 'deleted')->orWhere('action', 'moved')->where('column', 'task')->exists();
1037 + $stageArchiveRestored = !$forceFullSync && (clone $stageActivityQuery)
1038 + ->whereIn('action', ['archived', 'restored'])
1039 + ->exists();
742 1040
743 - if ($taskDeletedOrMovedFormBoard) {
744 - $tasks = Task::query()
1041 + $taskResetRequired = $forceFullSync || $stageArchiveRestored || Activity::where('object_id', $boardId)
1042 + ->where('object_type', Constant::ACTIVITY_BOARD)
1043 + ->where('updated_at', '>=', $lastUpdated)
1044 + ->where(function($query) {
1045 + $query->where('action', 'deleted')
1046 + ->orWhere('action', 'moved')
1047 + ->orWhere('action', 'archived')
1048 + ->orWhere('action', 'restored');
1049 + })
1050 + ->where('column', 'task')
1051 + ->exists();
1052 + if ($taskResetRequired) {
1053 + $tasksQuery = Task::query()
745 1054 ->where([
746 1055 'board_id' => $boardId,
747 1056 'parent_id' => null,
748 - 'archived_at' => null
749 1057 ])
750 - ->with(['assignees', 'labels'])
751 - ->orderBy('due_at', 'ASC')
752 - ->get();
1058 + ->with(['assignees', 'labels', 'watchers']);
1059 +
1060 + if (!$includeArchived) {
1061 + $tasksQuery->whereNull('archived_at');
1062 + }
1063 +
1064 + if (!!defined('FLUENT_BOARDS_PRO_VERSION')) {
1065 + $tasksQuery->with('customFields');
1066 + }
1067 +
1068 + $tasks = $tasksQuery->orderBy('due_at', 'ASC')->get();
753 1069 } else {
754 - $tasks = (new TaskService())->getLastOneMinuteUpdatedTasks($boardId);
1070 + $tasks = (new TaskService())->getLastOneMinuteUpdatedTasks($boardId, $lastUpdated, $includeArchived);
755 1071 }
756 1072
757 1073 foreach ($tasks as $task) {
758 1074 $task->isOverdue = $task->isOverdue();
759 1075 $task->isUpcoming = $task->upcoming();
1076 + $task->is_watching = $task->isWatching();
1077 + $task->contact = Helper::crm_contact($task->crm_contact_id);
1078 + $task->assignees = Helper::sanitizeUserCollections($task->assignees);
1079 + $task->watchers = Helper::sanitizeUserCollections($task->watchers);
760 1080 }
761 1081
762 - if ($board->updated_at >= $oneMinuteAgo) {
763 - $board->background = \maybe_unserialize($board->background);
764 - } else {
765 - $board = [];
1082 + $board->background = \maybe_unserialize($board->background);
1083 + if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
1084 + $board->custom_fields = $board->customFields;
766 1085 }
767 1086
1087 + $boardPayload = $boardChanged ? $board : (object) [];
1088 + $hasChanges = $boardChanged
1089 + || $stageResetRequired
1090 + || $labelResetRequired
1091 + || $taskResetRequired
1092 + || count($stages)
1093 + || count($labels)
1094 + || count($tasks);
1095 +
768 1096 return [
769 - 'board' => $board,
770 - 'stages' => $stages,
771 - 'labels' => $labels,
772 - 'tasks' => $tasks,
773 - 'taskDeleted' => $taskDeletedOrMovedFormBoard,
774 - 'stageDeleted' => $stageDeleted,
1097 + 'board' => $boardPayload,
1098 + 'stages' => $stages,
1099 + 'labels' => $labels,
1100 + 'tasks' => $tasks,
1101 + 'taskDeleted' => $taskResetRequired,
1102 + 'stageDeleted' => $stageResetRequired,
1103 + 'labelDeleted' => $labelResetRequired,
1104 + 'taskResetRequired' => $taskResetRequired,
1105 + 'stageResetRequired' => $stageResetRequired,
1106 + 'labelResetRequired' => $labelResetRequired,
1107 + 'has_changes' => (bool) $hasChanges,
1108 + 'synced_at' => $syncStartedAt,
1109 + 'sync_reset' => $forceFullSync,
775 1110 ];
776 1111 }
777 1112
778 - public function getAssociatedBoards($associatedId)
1113 + private function normalizeSyncCursor($since, $syncStartedAt)
779 1114 {
1115 + if (!is_string($since)) {
1116 + return null;
1117 + }
780 1118
1119 + $since = trim($since);
1120 + if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $since)) {
1121 + return null;
1122 + }
1123 +
1124 + if ($since > $syncStartedAt) {
1125 + return null;
1126 + }
1127 +
1128 + if (strtotime($since) < strtotime('-24 hours', strtotime($syncStartedAt))) {
1129 + return null;
1130 + }
1131 +
1132 + return $since;
1133 + }
1134 +
1135 + private function formatSyncTimestamp($timestamp)
1136 + {
1137 + if ($timestamp instanceof \DateTimeInterface) {
1138 + return $timestamp->format('Y-m-d H:i:s');
1139 + }
1140 +
1141 + return (string) $timestamp;
1142 + }
1143 +
1144 + /**
1145 + * Get CRM-associated boards that the current user can access.
1146 + *
1147 + * @param int $associatedId CRM contact/subscriber id.
1148 + * @param int|null $userId WordPress user id used for board access checks.
1149 + * @return \FluentBoards\Framework\Database\Orm\Collection|array
1150 + */
1151 + public function getAssociatedBoards($associatedId, $userId = null)
1152 + {
1153 + $associatedId = absint($associatedId);
1154 + $userId = $userId ?: get_current_user_id();
1155 +
1156 + if (!$associatedId || !$userId) {
1157 + return [];
1158 + }
1159 +
781 1160 $boardIds = Meta::query()->where('value', $associatedId)
782 1161 ->where('object_type', Constant::OBJECT_TYPE_BOARD)
783 - ->where('key', Constant::BOARD_ASSOCIATED_CRM_CONTACT)
1162 + ->whereIn('key', [
1163 + Constant::BOARD_ASSOCIATED_CRM_CONTACT,
1164 + self::LEGACY_BOARD_ASSOCIATED_CRM_CONTACT,
1165 + ])
784 1166 ->pluck('object_id');
785 1167
786 - return Board::query()->whereIn('id', $boardIds)->with('stages', 'users')->get();
1168 + $boards = Board::query()
1169 + ->whereIn('id', array_values(array_unique(array_map('intval', $boardIds->toArray()))))
1170 + ->whereNull('archived_at')
1171 + ->byAccessUser($userId)
1172 + ->withCount('completedTasks')
1173 + ->with(['stages', 'users'])
1174 + ->orderBy('created_at', 'DESC')
1175 + ->get();
1176 +
1177 + foreach ($boards as $board) {
1178 + $board->users = Helper::sanitizeUserCollections($board->users);
1179 + }
1180 +
1181 + return $boards;
787 1182 }
788 1183
789 1184 private function deleteBoardMeta($boardId)
790 1185 {
@@ -794,8 +1189,13 @@
794 1189 }
795 1190
796 1191 public function copyBoard($boardData)
797 1192 {
1193 + $sourceBoard = Board::findOrFail($boardData['source_board_id']);
1194 + $boardData['background'] = $sourceBoard->background;
1195 + if (isset($boardData['description'])) {
1196 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
1197 + }
798 1198 $boardData = apply_filters('fluent_boards/before_create_board', $boardData);
799 1199
800 1200 $board = Board::create($boardData);
801 1201
@@ -803,101 +1203,267 @@
803 1203
804 1204 return $board;
805 1205 }
806 1206
807 - public function getBoardReports($board_id)
1207 + public function archiveBoard($boardId)
808 1208 {
809 - $taskQuery = Task::where('board_id', $board_id)
810 - ->whereNull('parent_id')
811 - ->whereNull('archived_at');
1209 + $board = Board::findOrFail($boardId);
1210 + $board->archived_at = current_time('mysql');
1211 + $board->save();
812 1212
813 - return $this->getTaskReports($taskQuery);
1213 + do_action('fluent_boards/board_archived', $board);
1214 + return $board;
814 1215 }
815 1216
816 - public function getAllBoardReports(){
1217 + public function restoreBoard($boardId)
1218 + {
1219 + $board = Board::findOrFail($boardId);
1220 + $board->archived_at = null;
1221 + $board->save();
1222 +
1223 + do_action('fluent_boards/board_restored', $board);
1224 + return $board;
1225 + }
1226 +
1227 + public function makeMember($boardId, $userId)
1228 + {
1229 + $boardUser = Relation::where('object_id', $boardId)
1230 + ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
1231 + ->where('foreign_id', $userId)->first();
1232 +
1233 + $boardUser->settings = [
1234 + 'is_admin' => false,
1235 + 'is_viewer_only' => false
1236 + ];
1237 +
1238 + $boardUser->save();
1239 + $user = User::findOrFail($userId);
1240 + do_action('fluent_boards/board_member_added', $boardId, $boardUser);
1241 + $user['is_admin'] = false;
1242 + $user['is_board_admin'] = false;
1243 + return $user;
1244 + }
1245 +
1246 + public function makeViewer($boardId, $userId)
1247 + {
1248 + $boardUser = Relation::where('object_id', $boardId)
1249 + ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
1250 + ->where('foreign_id', $userId)->first();
1251 +
1252 + $boardUser->settings = [
1253 + 'is_admin' => false,
1254 + 'is_viewer_only' => true
1255 + ];
1256 +
1257 + $boardUser->save();
1258 + $user = User::findOrFail($userId);
1259 + do_action('fluent_boards/board_viewer_added', $boardId, $boardUser);
1260 + $user['is_admin'] = false;
1261 + $user['is_board_admin'] = false;
1262 + return $user;
1263 + }
1264 +
1265 + private function getUserWisePinnedBoards()
1266 + {
817 1267 $userId = get_current_user_id();
818 1268
819 - $taskQuery = Task::whereNull('parent_id')
820 - ->whereNull('archived_at');
1269 + $pinnedBoardMeta = Meta::query()->where('object_id', $userId)
1270 + ->where('object_type', Constant::OBJECT_TYPE_USER)
1271 + ->where('key', Constant::USER_PINNED_BOARDS)
1272 + ->first();
821 1273
822 - if (!PermissionManager::isAdmin($userId))
823 - {
824 - $currentUser = User::find($userId);
825 - $relatedBoardIds = $currentUser->whichBoards->where('type', 'to-do')->pluck('id');
826 - $taskQuery->whereIn('board_id', $relatedBoardIds);
1274 + return $pinnedBoardMeta;
1275 + }
1276 +
1277 + /**
1278 + * Sidebar counts cover every board the user can access, so they are counted
1279 + * with their own queries rather than derived from the filtered/paginated list.
1280 + *
1281 + * byAccessUser() re-reads the user's accessible board ids from the database on
1282 + * every call, so the access scope is resolved once and cloned per count.
1283 + *
1284 + * @return array{all: int, pinned: int, archived: int}
1285 + */
1286 + public function getBoardCounts($userId)
1287 + {
1288 + $baseQuery = Board::byAccessUser($userId)
1289 + ->excludeTemplates()
1290 + ->availableInCurrentInstall();
1291 +
1292 + $counts = [
1293 + 'all' => (clone $baseQuery)->whereNull('archived_at')->count(),
1294 + 'pinned' => 0,
1295 + 'archived' => (clone $baseQuery)->whereNotNull('archived_at')->count()
1296 + ];
1297 +
1298 + $pinnedIds = $this->getPinnedBoardIds();
1299 +
1300 + if ($pinnedIds) {
1301 + $counts['pinned'] = (clone $baseQuery)->whereNull('archived_at')
1302 + ->whereIn('id', $pinnedIds)
1303 + ->count();
827 1304 }
828 1305
829 - return $this->getTaskReports($taskQuery);
1306 + return $counts;
830 1307 }
831 1308
832 - private function getTaskReports($taskQuery)
1309 + /**
1310 + * @return array board ids the current user has pinned
1311 + */
1312 + public function getPinnedBoardIds()
833 1313 {
834 - $totalTasksQuery = clone $taskQuery;
835 - $completedTaskQuery = clone $taskQuery;
836 - $openTaskQuery = clone $taskQuery;
837 - $overDueTaskQuery = clone $taskQuery;
1314 + $pinnedBoardMeta = $this->getUserWisePinnedBoards();
838 1315
839 - $completedTaskCount = $completedTaskQuery->where('status', 'closed')->count();
840 - $openTaskCount = $openTaskQuery->where('status', 'open')->count();
841 - $overDueTasks = $overDueTaskQuery->overdue(true)->count();
842 - $totalTasks = $totalTasksQuery->count();
1316 + if (!$pinnedBoardMeta) {
1317 + return [];
1318 + }
843 1319
844 - $taskQuery->where('status', 'open');
1320 + return array_map('intval', (array) $pinnedBoardMeta->value);
1321 + }
845 1322
846 - $highQuery = clone $taskQuery;
847 - $mediumQuery = clone $taskQuery;
848 - $lowQuery = clone $taskQuery;
1323 + public function getPinnedBoards()
1324 + {
1325 + $pinnedBoardMeta = $this->getUserWisePinnedBoards();
849 1326
850 - $high = $highQuery->where('priority', 'high')->count();
851 - $low = $mediumQuery->where('priority', 'low')->count();
852 - $medium = $lowQuery->where('priority', 'medium')->count();
1327 + if (!$pinnedBoardMeta) {
1328 + return [];
1329 + } else {
1330 + $ids = $pinnedBoardMeta->value;
853 1331
854 - $reportData = [
855 - 'completion' => [
856 - 'completed' => $completedTaskCount,
857 - 'incomplete' => $openTaskCount,
858 - 'overdue' => $overDueTasks,
859 - 'total' => $totalTasks
860 - ],
861 - 'priority' => [
862 - 'high' => $high,
863 - 'medium' => $medium,
864 - 'low' => $low
865 - ]
1332 + // Convert to array of integers
1333 + $intIds = array_map('intval', $ids);
866 1334
867 - ];
868 - return $reportData;
1335 + return Board::whereIn('id', $intIds)
1336 + ->whereNull('archived_at')
1337 + ->byAccessUser(get_current_user_id())
1338 + ->get();
1339 + }
869 1340 }
870 1341
871 - public function getStageWiseBoardReports($board_id)
1342 + public function pinBoard($boardId)
872 1343 {
873 - $stages = Stage::where('board_id', $board_id)
874 - ->where('type', 'stage')
875 - ->whereNull('archived_at')
1344 + $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1345 +
1346 + if ($pinnedBoardMeta) {
1347 + $currentPinnedBoards = $pinnedBoardMeta->value;
1348 + if (!in_array($boardId, $currentPinnedBoards)) {
1349 + $currentPinnedBoards[] = $boardId;
1350 + $pinnedBoardMeta->value = $currentPinnedBoards;
1351 + $pinnedBoardMeta->save();
1352 + }
1353 + } else {
1354 + // Create an empty array
1355 + $boardIds = [];
1356 + $boardIds[] = $boardId;
1357 +
1358 + $meta = new Meta();
1359 + $meta->object_id = get_current_user_id();
1360 + $meta->object_type = Constant::OBJECT_TYPE_USER;
1361 + $meta->key = Constant::USER_PINNED_BOARDS;
1362 + $meta->value = $boardIds;
1363 + $meta->save();
1364 + }
1365 + }
1366 +
1367 + /**
1368 + * @param $boardId
1369 + * @return bool
1370 + */
1371 + public function unpinBoard($boardId)
1372 + {
1373 + $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1374 +
1375 + if (!$pinnedBoardMeta) {
1376 + return false;
1377 + }
1378 +
1379 + $currentPinnedBoards = $pinnedBoardMeta->value;
1380 + if (in_array($boardId, $currentPinnedBoards)) {
1381 + $index = array_search($boardId, $currentPinnedBoards);
1382 + array_splice($currentPinnedBoards, $index, 1);
1383 + $pinnedBoardMeta->value = $currentPinnedBoards;
1384 + $pinnedBoardMeta->save();
1385 + return true;
1386 + }
1387 +
1388 + return false;
1389 + }
1390 +
1391 + /**
1392 + * @param $boardId
1393 + * @return bool
1394 + * If board id is in user's current pinned boards list
1395 + */
1396 + public function isPinned($boardId)
1397 + {
1398 + $pinnedBoardMeta = $this->getUserWisePinnedBoards();
1399 +
1400 + if (!$pinnedBoardMeta) {
1401 + return false;
1402 + }
1403 +
1404 + $currentPinnedBoards = $pinnedBoardMeta->value;
1405 + if (in_array($boardId, $currentPinnedBoards)) {
1406 + return true;
1407 + }
1408 +
1409 + return false;
1410 + }
1411 +
1412 + public function getBoardFolder($boardId)
1413 + {
1414 + $relation = Relation::where('object_type', Constant::OBJECT_TYPE_FOLDER_BOARD)
1415 + ->where('foreign_id', $boardId)
1416 + ->first();
1417 +
1418 + if (!$relation) {
1419 + return null;
1420 + }
1421 +
1422 + return Folder::find($relation->object_id);
1423 + }
1424 +
1425 + public function deleteWebhookData($boardId)
1426 + {
1427 + $outgoingRelations = Relation::where('object_type', 'outgoing_webhook_board')
1428 + ->where('foreign_id', $boardId)
876 1429 ->get();
877 1430
878 - foreach ($stages as $stage) {
879 - $completedTaskCount = Task::where('stage_id', $stage->id)
880 - ->where('status', 'closed')
1431 + foreach ($outgoingRelations as $relation) {
1432 + $webhookMetaId = (int) $relation->object_id;
1433 +
1434 + $linkedCount = Relation::where('object_type', 'outgoing_webhook_board')
1435 + ->where('object_id', $webhookMetaId)
881 1436 ->count();
882 1437
883 - $openTaskCount = Task::where('stage_id', $stage->id)
884 - ->whereNull('due_at')
885 - ->where('status', 'open')
886 - ->count();
1438 + if ($linkedCount === 1) {
1439 + Meta::where('id', $webhookMetaId)
1440 + ->where('object_type', 'outgoing_webhook')
1441 + ->delete();
1442 + } else if ($linkedCount > 1) {
1443 + $meta = Meta::find($webhookMetaId);
1444 + if ($meta && $meta->object_type === 'outgoing_webhook') {
1445 + $value = $meta->value;
887 1446
888 - $overDue = Task::where('stage_id', $stage->id)
889 - ->whereNotNull('due_at')
890 - ->where('status', 'open')
891 - ->overdue(true)
892 - ->count();
1447 + if (isset($value['board_id'])) {
1448 + $boards = $value['board_id'];
893 1449
894 - $stage->report = [
895 - 'completed' => $completedTaskCount,
896 - 'incomplete' => $openTaskCount,
897 - 'overdue' => $overDue
898 - ];
1450 + if (is_array($boards)) {
1451 + $boards = array_values(array_filter($boards, function ($id) use ($boardId) {
1452 + return intval($id) !== intval($boardId);
1453 + }));
1454 + $value['board_id'] = $boards;
1455 + } else {
1456 + if ($boards !== null && intval($boards) === intval($boardId)) {
1457 + $value['board_id'] = [];
1458 + }
1459 + }
1460 +
1461 + $meta->value = $value;
1462 + $meta->save();
1463 + }
1464 + }
1465 + }
1466 + $relation->delete();
899 1467 }
900 -
901 - return $stages;
902 1468 }
903 1469 }