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/Http/Controllers/BoardController.php +289 -111 1.95.32.1.0 View file →
@@ -9,8 +9,9 @@
9 9 use FluentBoards\App\Models\User;
10 10 use FluentBoards\App\Models\Board;
11 11 use FluentBoards\App\Services\CommentService;
12 12 use FluentBoards\App\Services\Constant;
13 +use FluentBoards\App\Services\DescriptionMarkdownConverter;
13 14 use FluentBoards\App\Services\Helper;
14 15 use FluentBoards\App\Models\Stage;
15 16 use FluentBoards\App\Services\InstallService;
16 17 use FluentBoards\App\Services\StageService;
@@ -15,8 +16,9 @@
15 16 use FluentBoards\App\Services\InstallService;
16 17 use FluentBoards\App\Services\StageService;
17 18 use FluentBoards\App\Services\TaskService;
18 19 use FluentBoards\App\Services\BoardService;
20 +use FluentBoards\App\Services\FolderService;
19 21 use FluentBoards\App\Services\UploadService;
20 22 use FluentBoards\Framework\Http\Request\Request;
21 23 use FluentBoards\App\Services\PermissionManager;
22 24 use FluentBoards\App\Services\PublicAccessService;
@@ -26,14 +28,14 @@
26 28 use FluentBoards\Framework\Support\Arr;
27 29 use FluentBoards\Framework\Support\Collection;
28 30 use FluentBoardsPro\App\Services\AttachmentService;
29 31 use FluentBoardsPro\App\Services\CustomFieldService;
30 -use FluentBoardsPro\App\Services\ProHelper;
31 32 use FluentBoardsPro\App\Services\RemoteUrlParser;
32 -use FluentCrm\App\Models\Subscriber;
33 33
34 34 class BoardController extends Controller
35 35 {
36 + private const LEGACY_BOARD_ASSOCIATED_CRM_CONTACT = 'crm_contact';
37 +
36 38 private $boardService;
37 39 private $taskService;
38 40 private $stageService;
39 41 private $labelService;
@@ -54,8 +56,9 @@
54 56
55 57 public function getBoards(Request $request)
56 58 {
57 59 $per_page = $request->getSafe('per_page', 'intval', 100);
60 + $per_page = max(1, min(100, $per_page));
58 61 $userId = get_current_user_id();
59 62 $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
60 63
61 64 $order = $request->getSafe('order', 'sanitize_text_field', 'created_at');
@@ -79,22 +82,28 @@
79 82 $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
80 83 }
81 84 }
82 85
83 - // If folder ID is provided, filter boards by folder
84 - if ($folderId && defined('FLUENT_BOARDS_PRO')) {
85 - $boardIds = ProHelper::getBoardIdsByFolder($folderId);
86 - $relatedBoardsQuery = $relatedBoardsQuery->whereIn('id', $boardIds);
86 + // Scope pinned before pagination, from the same id source as getBoardCounts(),
87 + // so the pinned page and board_counts.pinned always agree. Filtering pinned
88 + // after pagination would drop pinned boards that fall on a later active page.
89 + if ($option == 'pinned') {
90 + $pinnedIds = $this->boardService->getPinnedBoardIds();
91 +
92 + $relatedBoardsQuery = $pinnedIds
93 + ? $relatedBoardsQuery->whereIn('id', $pinnedIds)
94 + : $relatedBoardsQuery->where('id', 0);
87 95 }
88 96
97 + if ($folderId) {
98 + $boardIds = (new FolderService())->getBoardIdsByFolder($folderId);
99 + $relatedBoardsQuery = $boardIds
100 + ? $relatedBoardsQuery->whereIn('id', $boardIds)
101 + : $relatedBoardsQuery->where('id', 0);
102 + }
103 +
89 104 // Filter out boards that are templates (exclude boards where settings->is_template is true)
90 - $relatedBoardsQuery = $relatedBoardsQuery->where(function ($query) {
91 - $query->whereNull('settings')
92 - ->orWhere(function ($subQuery) {
93 - $subQuery->where('settings', 'NOT LIKE', '%"is_template";b:1%')
94 - ->where('settings', 'NOT LIKE', '%"is_template":true%');
95 - });
96 - });
105 + $relatedBoardsQuery = $relatedBoardsQuery->excludeTemplates();
97 106
98 107 // Add search functionality
99 108 if (!empty($searchInput)) {
100 109 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
@@ -105,25 +114,24 @@
105 114 ->with('stages', 'users')
106 115 ->paginate($per_page);
107 116
108 117 foreach ($relatedBoards as $relatedBoard) {
118 + $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description);
109 119 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
110 120 $relatedBoard->is_pinned = $this->boardService->isPinned($relatedBoard->id);
111 121 }
112 122
113 123 $response = [
114 - 'boards' => $relatedBoards
124 + 'boards' => $relatedBoards,
125 + 'board_counts' => $this->boardService->getBoardCounts($userId)
115 126 ];
116 127
117 - // Include folder mapping if pro version is available - ALWAYS include for consistency
118 - if (defined('FLUENT_BOARDS_PRO')) {
119 - $response['folder_mapping'] = $this->getBoardFolderMapping($userId);
120 - if ($folderId) {
121 - $response['current_folder'] = $this->getCurrentFolderInfo($folderId);
122 - }
123 - } else {
124 - // Include empty folder mapping for consistency
125 - $response['folder_mapping'] = [];
128 + $folderMapping = $this->getBoardFolderMapping($userId);
129 + $response['folder_mapping'] = $folderMapping;
130 + if ($folderId) {
131 + $response['current_folder'] = isset($folderMapping[$folderId])
132 + ? $this->getCurrentFolderInfoFromMapping($folderMapping[$folderId])
133 + : null;
126 134 }
127 135
128 136 return $this->sendSuccess($response);
129 137 }
@@ -132,13 +140,9 @@
132 140 * Get folder mapping for boards
133 141 */
134 142 private function getBoardFolderMapping($userId)
135 143 {
136 - if (!defined('FLUENT_BOARDS_PRO')) {
137 - return [];
138 - }
139 -
140 - $folderService = new \FluentBoardsPro\App\Services\FolderService();
144 + $folderService = new FolderService();
141 145 $folders = $folderService->getFolders($userId);
142 146
143 147 $mapping = [];
144 148 foreach ($folders as $folder) {
@@ -151,28 +155,14 @@
151 155
152 156 return $mapping;
153 157 }
154 158
155 - /**
156 - * Get current folder information
157 - */
158 - private function getCurrentFolderInfo($folderId)
159 + private function getCurrentFolderInfoFromMapping(array $folder)
159 160 {
160 - if (!defined('FLUENT_BOARDS_PRO')) {
161 - return null;
162 - }
163 -
164 - $folderService = new \FluentBoardsPro\App\Services\FolderService();
165 - $folder = $folderService->getFolderById($folderId);
166 -
167 - if (!$folder) {
168 - return null;
169 - }
170 -
171 161 return [
172 - 'id' => $folder->id,
173 - 'title' => $folder->title,
174 - 'board_count' => $folder->boards ? $folder->boards->count() : 0
162 + 'id' => $folder['id'],
163 + 'title' => $folder['title'],
164 + 'board_count' => count($folder['board_ids'])
175 165 ];
176 166 }
177 167
178 168 /**
@@ -187,11 +177,11 @@
187 177
188 178 // Query to fetch boards that are not archived and accessible by the user
189 179 // Check if the FLUENT_ROADMAP constant is defined
190 180 if (!defined('FLUENT_ROADMAP')) {
191 - $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
181 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
192 182 } else {
193 - $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
183 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
194 184 }
195 185
196 186 $relatedBoards = $relatedBoardsQuery->with('stages')->get();
197 187
@@ -212,11 +202,11 @@
212 202
213 203
214 204 if(!defined('FLUENT_ROADMAP'))
215 205 {
216 - $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
206 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
217 207 } else {
218 - $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
208 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
219 209 }
220 210
221 211 if (!empty($searchInput)) {
222 212 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
@@ -238,9 +228,12 @@
238 228 {
239 229 $boards = $this->boardService->getRecentBoards();
240 230
241 231 if (!$boards || $boards->isEmpty()) {
242 - $boards = Board::where('type', 'to-do')->byAccessUser(get_current_user_id())
232 + $boards = Board::whereNull('archived_at')
233 + ->excludeTemplates()
234 + ->availableInCurrentInstall()
235 + ->byAccessUser(get_current_user_id())
243 236 ->limit(4)
244 237 ->withCount('completedTasks')
245 238 ->with(['stages', 'users'])
246 239 ->get();
@@ -346,25 +339,42 @@
346 339 'folder_id' => 'nullable',
347 340 ]);
348 341
349 342 try {
343 + $folderId = $request->getSafe('folder_id', 'intval');
344 + $folderService = new FolderService();
345 + if ($folderId) {
346 + $folderService->assertCanModifyFolder($folderId);
347 + }
348 +
349 + $backgroundData = $this->sanitizeCreateBoardBackground($request->get('background'));
350 + if (!empty($backgroundData)) {
351 + $boardData['background'] = $backgroundData;
352 + }
353 +
354 + $this->validateRequestedLabelPresets($request->get('labels'));
355 +
350 356 $board = $this->boardService->createBoard($boardData);
351 - $this->labelService->createDefaultLabel($board->id);
357 + $this->createBoardLabelsFromRequest($request, $board->id);
358 + $this->addBoardMembersFromRequest($request, $board->id);
352 359 $type = ucfirst($boardData['type']);
360 + $stages = $request->get('stages');
361 + $sanitizedStages = [];
353 362
363 + if (is_array($stages) && !empty($stages)) {
364 + foreach ($stages as $stage) {
365 + $sanitizedStages[] = $this->stageSanitizeAndValidate($stage, [
366 + 'title' => 'required|string',
367 + 'slug' => 'nullable|string',
368 + 'position' => 'nullable|numeric'
369 + ]);
370 + }
371 + }
372 +
354 373 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
355 - $stages = $request->get('stages');
356 - $sanitizedStages = [];
357 - if (is_array($stages) && !empty($stages)) {
358 - foreach ($stages as $stage) {
359 - $sanitizedStages[] = $this->stageSanitizeAndValidate($stage, [
360 - 'title' => 'required|string',
361 - 'slug' => 'nullable|string',
362 - 'position' => 'nullable|numeric'
363 - ]);
364 - }
365 - }
366 374 $this->stageService->createRoadmapStages($board, $sanitizedStages);
375 + } elseif (!empty($sanitizedStages)) {
376 + $this->stageService->createStages($board, $sanitizedStages);
367 377 } else {
368 378 $this->stageService->createDefaultStages($board);
369 379 }
370 380
@@ -375,13 +385,10 @@
375 385
376 386 do_action('fluent_boards/board_created', $board);
377 387
378 388
379 - if(defined('FLUENT_BOARDS_PRO')) {
380 - $folderId = $request->getSafe('folder_id', 'intval');
381 - if ($folderId) {
382 - (new \FluentBoardsPro\App\Services\FolderService())->addBoardToFolder($folderId, [$board->id]);
383 - }
389 + if ($folderId) {
390 + $folderService->addBoardToFolder($folderId, [$board->id]);
384 391 }
385 392
386 393 $message = __('Board has been created successfully', 'fluent-boards');
387 394
@@ -395,26 +402,132 @@
395 402 ]);
396 403 }
397 404 }
398 405
406 + private function sanitizeCreateBoardBackground($background)
407 + {
408 + if (!is_array($background) || empty($background['id'])) {
409 + return '';
410 + }
411 +
412 + $backgroundId = sanitize_text_field($background['id']);
413 +
414 + // Only accept ids from the curated solid/gradient palettes and always
415 + // persist the canonical value from the constant (never the client-supplied
416 + // color) so arbitrary CSS can't be stored and later rendered into a style.
417 + $allowedBackgrounds = [];
418 + foreach (array_merge(
419 + Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
420 + Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
421 + ) as $option) {
422 + if (isset($option['id'], $option['value'])) {
423 + $allowedBackgrounds[$option['id']] = $option['value'];
424 + }
425 + }
426 +
427 + if (!isset($allowedBackgrounds[$backgroundId])) {
428 + return '';
429 + }
430 +
431 + return [
432 + 'id' => $backgroundId,
433 + 'color' => $allowedBackgrounds[$backgroundId],
434 + 'is_image' => false,
435 + 'image_url' => null,
436 + ];
437 + }
438 +
439 + private function createBoardLabelsFromRequest(Request $request, $boardId)
440 + {
441 + $labels = $request->get('labels');
442 +
443 + if (!is_array($labels)) {
444 + $this->labelService->createDefaultLabel($boardId);
445 + return;
446 + }
447 +
448 + foreach ($labels as $label) {
449 + $labelData = Helper::sanitizeLabel((array) $label);
450 +
451 + if (empty($labelData['label']) && empty($labelData['bg_color']) && empty($labelData['color_preset'])) {
452 + continue;
453 + }
454 +
455 + $labelPayload = [
456 + 'label' => $labelData['label'] ?? '',
457 + 'bg_color' => $labelData['bg_color'] ?? '#f3f4f6',
458 + 'color' => $labelData['color'] ?? '#1B2533',
459 + ];
460 +
461 + if (array_key_exists('color_preset', $labelData)) {
462 + $labelPayload['color_preset'] = $labelData['color_preset'];
463 + }
464 +
465 + $this->labelService->createLabel($labelPayload, $boardId);
466 + }
467 + }
468 +
469 + /**
470 + * Reject unsupported label preset ids before creating any board records.
471 + *
472 + * @param mixed $labels
473 + * @return void
474 + * @throws \Exception
475 + */
476 + private function validateRequestedLabelPresets($labels)
477 + {
478 + if (!is_array($labels)) {
479 + return;
480 + }
481 +
482 + foreach ($labels as $label) {
483 + $labelData = Helper::sanitizeLabel((array) $label);
484 + $presetId = $labelData[Constant::LABEL_COLOR_PRESET_SETTING] ?? null;
485 +
486 + if ($presetId === null || $presetId === '') {
487 + continue;
488 + }
489 +
490 + if (!is_string($presetId) || !Constant::getLabelColorPreset($presetId)) {
491 + throw new \Exception(esc_html__('Invalid label color preset', 'fluent-boards'));
492 + }
493 + }
494 + }
495 +
496 + private function addBoardMembersFromRequest(Request $request, $boardId)
497 + {
498 + $memberIds = $request->get('member_ids');
499 +
500 + if (!is_array($memberIds)) {
501 + return;
502 + }
503 +
504 + $memberIds = array_filter(array_unique(array_map('intval', $memberIds)));
505 + $currentUserId = get_current_user_id();
506 +
507 + foreach ($memberIds as $memberId) {
508 + if ($memberId === $currentUserId) {
509 + continue;
510 + }
511 +
512 + $this->boardService->addMembersInBoard($boardId, $memberId);
513 + }
514 + }
515 +
516 + /**
517 + * Get archived stages for a board with optional pagination and archive actor metadata.
518 + */
399 519 public function getArchivedStage(Request $request, $board_id)
400 520 {
401 521 try {
402 - $pagination = $request->getSafe('noPagination', 'boolval', false);
403 - $per_page = $request->getSafe('per_page', 'intval', 30);
404 - $page = $request->getSafe('page', 'intval', 1);
522 + $board_id = absint($board_id);
523 + $sanitizedParams = [
524 + 'noPagination' => $request->getSafe('noPagination', 'boolval', false),
525 + 'per_page' => $request->getSafe('per_page', 'intval', 30),
526 + 'page' => $request->getSafe('page', 'intval', 1),
527 + ];
405 528
406 - if ($pagination) {
407 - $stages = Stage::where('board_id', $board_id)
408 - ->whereNotNull('archived_at')
409 - ->orderBy('created_at', 'DESC')
410 - ->get();
411 - } else {
412 - $stages = Stage::where('board_id', $board_id)
413 - ->whereNotNull('archived_at')
414 - ->orderBy('created_at', 'DESC')
415 - ->paginate($per_page, ['*'], 'page', $page);
416 - }
529 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
417 530
418 531 return $this->sendSuccess([
419 532 'stages' => $stages,
420 533 ], 200);
@@ -425,8 +538,9 @@
425 538
426 539 public function find(Request $request, $board_id)
427 540 {
428 541 $board = Board::findOrFail($board_id);
542 + $board->description = DescriptionMarkdownConverter::normalize($board->description);
429 543 $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
430 544 $board->background = maybe_unserialize($board->background);
431 545 $board->createdOn = $board->created_at->format('Y-m-d');
432 546
@@ -453,8 +567,9 @@
453 567 $this->boardService->updateRecentBoards($board_id);
454 568
455 569 $board->labelColor = Constant::TRELLO_COLOR_MAP;
456 570 $board->labelColorText = Constant::TEXT_COLOR_MAP;
571 + $board->labelColorPresets = Constant::LABEL_COLOR_PRESETS;
457 572
458 573 $board->users = Helper::sanitizeUserCollections($board->users);
459 574 $board->owner = Helper::sanitizeUserCollections($board->owner);
460 575
@@ -469,8 +584,18 @@
469 584 }
470 585
471 586 public function update(Request $request, $board_id)
472 587 {
588 + // Board identity (title/description) is manager-only. This action shares the
589 + // `update` name with CommentController@update under the same policy group, so the
590 + // guard lives here rather than in a SingleBoardPolicy::update() method that would
591 + // also block ordinary members from editing their own comments.
592 + if (!PermissionManager::isBoardManager(absint($board_id))) {
593 + return $this->sendError([
594 + 'message' => __('You do not have permission to edit this board.', 'fluent-boards'),
595 + ], 403);
596 + }
597 +
473 598 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
474 599 'title' => 'required|string',
475 600 'description' => 'nullable|string',
476 601 ]);
@@ -475,8 +600,9 @@
475 600 'description' => 'nullable|string',
476 601 ]);
477 602
478 603 $board = Board::findOrFail($board_id);
604 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
479 605
480 606 $oldBoard = clone $board;
481 607 $board->fill($boardData);
482 608 $board->save();
@@ -632,9 +758,8 @@
632 758
633 759 $formattedUsers[] = [
634 760 'ID' => $user->ID,
635 761 'display_name' => $name,
636 - 'user_login' => $user->user_login,
637 762 'email' => $user->user_email,
638 763 'photo' => fluent_boards_user_avatar($user->user_email, $name),
639 764 'role' => $this->boardUserRole($boardRelation),
640 765 'is_super' => in_array($user->ID, $superAdminIds),
@@ -656,16 +781,24 @@
656 781 return $returnData;
657 782 }
658 783
659 784 /*
660 - * These are the rest of the admin users who are not in the board
785 + * These are the rest of the Fluent Boards and WordPress admins who are not in the board.
661 786 */
662 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
787 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
663 788 ->whereNotIn('object_id', $userIds)
664 789 ->get()
665 790 ->pluck('object_id')
666 791 ->toArray();
667 792
793 + $wordPressAdminIds = get_users([
794 + 'capability' => 'manage_options',
795 + 'exclude' => $userIds,
796 + 'fields' => 'ID',
797 + ]);
798 +
799 + $adminUserIds = array_values(array_unique(array_map('intval', array_merge($fluentBoardAdminIds, $wordPressAdminIds))));
800 +
668 801 if ($adminUserIds) {
669 802 $adminUsers = get_users([
670 803 'include' => $adminUserIds,
671 804 ]);
@@ -715,15 +848,22 @@
715 848 }
716 849
717 850 public function addMembersInBoard(Request $request, $board_id)
718 851 {
719 - $memberId = $request->getSafe('memberId');
720 - $isViewerOnly = $request->getSafe('isViewerOnly');
852 + $memberId = $request->getSafe('memberId', 'intval');
853 + $isViewerOnly = $request->getSafe('isViewerOnly', 'sanitize_text_field');
721 854 $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
855 +
856 + if ($member === null) {
857 + return $this->sendError([
858 + 'message' => __('User not found.', 'fluent-boards'),
859 + ], 404);
860 + }
861 +
722 862 if (!$member) {
723 863 return $this->sendError([
724 864 'message' => __('User already a member', 'fluent-boards'),
725 - ], 304);
865 + ], 409);
726 866 }
727 867
728 868
729 869 return [
@@ -811,9 +951,9 @@
811 951
812 952 if (isset($settings['is_public'])) {
813 953 if ($settings['is_public']) {
814 954 $settings['is_public'] = false;
815 - $message = __('The stage is made admin only!', 'fluent-boards');
955 + $message = __('The stage is made private!', 'fluent-boards');
816 956 } else {
817 957 $settings['is_public'] = true;
818 958 }
819 959 } else {
@@ -832,24 +972,27 @@
832 972 }
833 973
834 974
835 975 /**
836 - * Set board background image or color
976 + * Set or reset board background image/color.
837 977 * @param \FluentBoards\Framework\Http\Request\Request $request
838 978 * @return
839 979 */
840 980 public function setBoardBackground(Request $request, $board_id)
841 981 {
842 - // sanitize and validate image_url
843 - if ($request->image_url) {
982 + $backgroundData = [];
983 + $isResetRequest = $request->getSafe('reset', 'rest_sanitize_boolean');
984 +
985 + if ($isResetRequest) {
986 + $backgroundData = [
987 + 'reset' => true,
988 + ];
989 + } elseif ($request->image_url) {
844 990 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
845 - "id" => 'required',
991 + 'id' => 'required|integer',
846 992 'image_url' => 'required|string|url',
847 993 ]);
848 - }
849 -
850 - // sanitize and validate color
851 - if ($request->color) {
994 + } elseif ($request->color) {
852 995 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
853 996 "id" => 'required',
854 997 'color' => 'required',
855 998 ]);
@@ -860,8 +1003,13 @@
860 1003 $errorMessage = __('Board id is required', 'fluent-boards');
861 1004 throw new \Exception(esc_html($errorMessage), 400);
862 1005 }
863 1006
1007 + if (empty($backgroundData)) {
1008 + $errorMessage = __('Background data is required', 'fluent-boards');
1009 + throw new \Exception(esc_html($errorMessage), 400);
1010 + }
1011 +
864 1012 return $this->sendSuccess([
865 1013 'message' => __('Background updated successfully', 'fluent-boards'),
866 1014 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
867 1015 ]);
@@ -911,24 +1059,47 @@
911 1059 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
912 1060 ->whereNotNull('crm_contact_id')
913 1061 ->get();
914 1062
915 - $formattedContacts = Collection::make($contactAssociatedTasks)
916 - ->groupBy('crm_contact_id')
917 - ->map(function ($tasks, $contactId) {
918 - $subscriber = Subscriber::find($contactId);
919 - if (!$subscriber) {
1063 + $tasksByContact = [];
1064 + foreach ($contactAssociatedTasks as $task) {
1065 + $tasksByContact[absint($task->crm_contact_id)][] = $task;
1066 + }
1067 +
1068 + $boardContactIds = Meta::query()
1069 + ->where('object_id', absint($board_id))
1070 + ->where('object_type', Constant::OBJECT_TYPE_BOARD)
1071 + ->whereIn('key', [
1072 + Constant::BOARD_ASSOCIATED_CRM_CONTACT,
1073 + self::LEGACY_BOARD_ASSOCIATED_CRM_CONTACT,
1074 + ])
1075 + ->pluck('value')
1076 + ->toArray();
1077 + $boardContactIds = array_values(array_unique(array_filter(array_map('absint', $boardContactIds))));
1078 +
1079 + $contactIds = array_values(array_unique(array_filter(array_map('absint', array_merge(
1080 + array_keys($tasksByContact),
1081 + $boardContactIds
1082 + )))));
1083 +
1084 + usort($contactIds, function ($firstContactId, $secondContactId) use ($boardContactIds) {
1085 + return (int) in_array($secondContactId, $boardContactIds, true) - (int) in_array($firstContactId, $boardContactIds, true);
1086 + });
1087 +
1088 + $formattedContacts = Collection::make($contactIds)
1089 + ->map(function ($contactId) use ($tasksByContact, $boardContactIds) {
1090 + $contact = Helper::crm_contact($contactId);
1091 + if (!$contact) {
920 1092 return null; // Skip if subscriber not found
921 1093 }
922 1094
923 - return [
924 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
925 - 'photo' => $subscriber->photo,
926 - 'email' => $subscriber->email,
927 - 'crm_contact_id' => $contactId,
928 - 'id' => $contactId,
929 - 'tasks' => $tasks,
930 - ];
1095 + $tasks = $tasksByContact[$contactId] ?? [];
1096 + $contact['name'] = trim(($contact['first_name'] ?? '') . ' ' . ($contact['last_name'] ?? '')) ?: ($contact['full_name'] ?? $contact['email'] ?? '');
1097 + $contact['crm_contact_id'] = $contactId;
1098 + $contact['is_board_contact'] = in_array($contactId, $boardContactIds, true);
1099 + $contact['tasks'] = $tasks;
1100 +
1101 + return $contact;
931 1102 })
932 1103 ->filter()->toArray();
933 1104
934 1105
@@ -1026,9 +1197,16 @@
1026 1197 if (!$this->currentUserCanReadCrmContacts()) {
1027 1198 return $this->sendError(esc_html__('You do not have permission to view CRM contact boards', 'fluent-boards'), 403);
1028 1199 }
1029 1200
1030 - $associatedBoards = $this->boardService->getAssociatedBoards($associated_id, get_current_user_id());
1201 + $associatedId = absint($associated_id);
1202 +
1203 + if (!$associatedId) {
1204 + return $this->sendError(__('Invalid CRM contact', 'fluent-boards'), 400);
1205 + }
1206 +
1207 + $associatedBoards = $this->boardService->getAssociatedBoards($associatedId, get_current_user_id());
1208 +
1031 1209 return [
1032 1210 'boards' => $associatedBoards,
1033 1211 ];
1034 1212 }