PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / trunk
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration vtrunk
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
← All changes | app/Http/Controllers/BoardController.php +282 -115 1.95trunk 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,40 @@
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 +
350 354 $board = $this->boardService->createBoard($boardData);
351 - $this->labelService->createDefaultLabel($board->id);
355 + $this->createBoardLabelsFromRequest($request, $board->id);
356 + $this->addBoardMembersFromRequest($request, $board->id);
352 357 $type = ucfirst($boardData['type']);
358 + $stages = $request->get('stages');
359 + $sanitizedStages = [];
353 360
361 + if (is_array($stages) && !empty($stages)) {
362 + foreach ($stages as $stage) {
363 + $sanitizedStages[] = $this->stageSanitizeAndValidate($stage, [
364 + 'title' => 'required|string',
365 + 'slug' => 'nullable|string',
366 + 'position' => 'nullable|numeric'
367 + ]);
368 + }
369 + }
370 +
354 371 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 372 $this->stageService->createRoadmapStages($board, $sanitizedStages);
373 + } elseif (!empty($sanitizedStages)) {
374 + $this->stageService->createStages($board, $sanitizedStages);
367 375 } else {
368 376 $this->stageService->createDefaultStages($board);
369 377 }
370 378
@@ -375,13 +383,10 @@
375 383
376 384 do_action('fluent_boards/board_created', $board);
377 385
378 386
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 - }
387 + if ($folderId) {
388 + $folderService->addBoardToFolder($folderId, [$board->id]);
384 389 }
385 390
386 391 $message = __('Board has been created successfully', 'fluent-boards');
387 392
@@ -395,26 +400,99 @@
395 400 ]);
396 401 }
397 402 }
398 403
404 + private function sanitizeCreateBoardBackground($background)
405 + {
406 + if (!is_array($background) || empty($background['id'])) {
407 + return '';
408 + }
409 +
410 + $backgroundId = sanitize_text_field($background['id']);
411 +
412 + // Only accept ids from the curated solid/gradient palettes and always
413 + // persist the canonical value from the constant (never the client-supplied
414 + // color) so arbitrary CSS can't be stored and later rendered into a style.
415 + $allowedBackgrounds = [];
416 + foreach (array_merge(
417 + Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
418 + Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
419 + ) as $option) {
420 + if (isset($option['id'], $option['value'])) {
421 + $allowedBackgrounds[$option['id']] = $option['value'];
422 + }
423 + }
424 +
425 + if (!isset($allowedBackgrounds[$backgroundId])) {
426 + return '';
427 + }
428 +
429 + return [
430 + 'id' => $backgroundId,
431 + 'color' => $allowedBackgrounds[$backgroundId],
432 + 'is_image' => false,
433 + 'image_url' => null,
434 + ];
435 + }
436 +
437 + private function createBoardLabelsFromRequest(Request $request, $boardId)
438 + {
439 + $labels = $request->get('labels');
440 +
441 + if (!is_array($labels)) {
442 + $this->labelService->createDefaultLabel($boardId);
443 + return;
444 + }
445 +
446 + foreach ($labels as $label) {
447 + $labelData = Helper::sanitizeLabel((array) $label);
448 +
449 + if (empty($labelData['label']) && empty($labelData['bg_color'])) {
450 + continue;
451 + }
452 +
453 + $this->labelService->createLabel([
454 + 'label' => $labelData['label'] ?? '',
455 + 'bg_color' => $labelData['bg_color'] ?? '#f3f4f6',
456 + 'color' => $labelData['color'] ?? '#1B2533',
457 + ], $boardId);
458 + }
459 + }
460 +
461 + private function addBoardMembersFromRequest(Request $request, $boardId)
462 + {
463 + $memberIds = $request->get('member_ids');
464 +
465 + if (!is_array($memberIds)) {
466 + return;
467 + }
468 +
469 + $memberIds = array_filter(array_unique(array_map('intval', $memberIds)));
470 + $currentUserId = get_current_user_id();
471 +
472 + foreach ($memberIds as $memberId) {
473 + if ($memberId === $currentUserId) {
474 + continue;
475 + }
476 +
477 + $this->boardService->addMembersInBoard($boardId, $memberId);
478 + }
479 + }
480 +
481 + /**
482 + * Get archived stages for a board with optional pagination and archive actor metadata.
483 + */
399 484 public function getArchivedStage(Request $request, $board_id)
400 485 {
401 486 try {
402 - $pagination = $request->getSafe('noPagination', 'boolval', false);
403 - $per_page = $request->getSafe('per_page', 'intval', 30);
404 - $page = $request->getSafe('page', 'intval', 1);
487 + $board_id = absint($board_id);
488 + $sanitizedParams = [
489 + 'noPagination' => $request->getSafe('noPagination', 'boolval', false),
490 + 'per_page' => $request->getSafe('per_page', 'intval', 30),
491 + 'page' => $request->getSafe('page', 'intval', 1),
492 + ];
405 493
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 - }
494 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
417 495
418 496 return $this->sendSuccess([
419 497 'stages' => $stages,
420 498 ], 200);
@@ -425,8 +503,9 @@
425 503
426 504 public function find(Request $request, $board_id)
427 505 {
428 506 $board = Board::findOrFail($board_id);
507 + $board->description = DescriptionMarkdownConverter::normalize($board->description);
429 508 $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
430 509 $board->background = maybe_unserialize($board->background);
431 510 $board->createdOn = $board->created_at->format('Y-m-d');
432 511
@@ -469,8 +548,18 @@
469 548 }
470 549
471 550 public function update(Request $request, $board_id)
472 551 {
552 + // Board identity (title/description) is manager-only. This action shares the
553 + // `update` name with CommentController@update under the same policy group, so the
554 + // guard lives here rather than in a SingleBoardPolicy::update() method that would
555 + // also block ordinary members from editing their own comments.
556 + if (!PermissionManager::isBoardManager(absint($board_id))) {
557 + return $this->sendError([
558 + 'message' => __('You do not have permission to edit this board.', 'fluent-boards'),
559 + ], 403);
560 + }
561 +
473 562 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
474 563 'title' => 'required|string',
475 564 'description' => 'nullable|string',
476 565 ]);
@@ -475,8 +564,9 @@
475 564 'description' => 'nullable|string',
476 565 ]);
477 566
478 567 $board = Board::findOrFail($board_id);
568 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
479 569
480 570 $oldBoard = clone $board;
481 571 $board->fill($boardData);
482 572 $board->save();
@@ -656,16 +746,24 @@
656 746 return $returnData;
657 747 }
658 748
659 749 /*
660 - * These are the rest of the admin users who are not in the board
750 + * These are the rest of the Fluent Boards and WordPress admins who are not in the board.
661 751 */
662 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
752 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
663 753 ->whereNotIn('object_id', $userIds)
664 754 ->get()
665 755 ->pluck('object_id')
666 756 ->toArray();
667 757
758 + $wordPressAdminIds = get_users([
759 + 'capability' => 'manage_options',
760 + 'exclude' => $userIds,
761 + 'fields' => 'ID',
762 + ]);
763 +
764 + $adminUserIds = array_values(array_unique(array_map('intval', array_merge($fluentBoardAdminIds, $wordPressAdminIds))));
765 +
668 766 if ($adminUserIds) {
669 767 $adminUsers = get_users([
670 768 'include' => $adminUserIds,
671 769 ]);
@@ -715,15 +813,22 @@
715 813 }
716 814
717 815 public function addMembersInBoard(Request $request, $board_id)
718 816 {
719 - $memberId = $request->getSafe('memberId');
720 - $isViewerOnly = $request->getSafe('isViewerOnly');
817 + $memberId = $request->getSafe('memberId', 'intval');
818 + $isViewerOnly = $request->getSafe('isViewerOnly', 'sanitize_text_field');
721 819 $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
820 +
821 + if ($member === null) {
822 + return $this->sendError([
823 + 'message' => __('User not found.', 'fluent-boards'),
824 + ], 404);
825 + }
826 +
722 827 if (!$member) {
723 828 return $this->sendError([
724 829 'message' => __('User already a member', 'fluent-boards'),
725 - ], 304);
830 + ], 409);
726 831 }
727 832
728 833
729 834 return [
@@ -811,9 +916,9 @@
811 916
812 917 if (isset($settings['is_public'])) {
813 918 if ($settings['is_public']) {
814 919 $settings['is_public'] = false;
815 - $message = __('The stage is made admin only!', 'fluent-boards');
920 + $message = __('The stage is made private!', 'fluent-boards');
816 921 } else {
817 922 $settings['is_public'] = true;
818 923 }
819 924 } else {
@@ -832,24 +937,27 @@
832 937 }
833 938
834 939
835 940 /**
836 - * Set board background image or color
941 + * Set or reset board background image/color.
837 942 * @param \FluentBoards\Framework\Http\Request\Request $request
838 943 * @return
839 944 */
840 945 public function setBoardBackground(Request $request, $board_id)
841 946 {
842 - // sanitize and validate image_url
843 - if ($request->image_url) {
947 + $backgroundData = [];
948 + $isResetRequest = $request->getSafe('reset', 'rest_sanitize_boolean');
949 +
950 + if ($isResetRequest) {
951 + $backgroundData = [
952 + 'reset' => true,
953 + ];
954 + } elseif ($request->image_url) {
844 955 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
845 - "id" => 'required',
956 + 'id' => 'required|integer',
846 957 'image_url' => 'required|string|url',
847 958 ]);
848 - }
849 -
850 - // sanitize and validate color
851 - if ($request->color) {
959 + } elseif ($request->color) {
852 960 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
853 961 "id" => 'required',
854 962 'color' => 'required',
855 963 ]);
@@ -860,8 +968,13 @@
860 968 $errorMessage = __('Board id is required', 'fluent-boards');
861 969 throw new \Exception(esc_html($errorMessage), 400);
862 970 }
863 971
972 + if (empty($backgroundData)) {
973 + $errorMessage = __('Background data is required', 'fluent-boards');
974 + throw new \Exception(esc_html($errorMessage), 400);
975 + }
976 +
864 977 return $this->sendSuccess([
865 978 'message' => __('Background updated successfully', 'fluent-boards'),
866 979 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
867 980 ]);
@@ -911,24 +1024,47 @@
911 1024 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
912 1025 ->whereNotNull('crm_contact_id')
913 1026 ->get();
914 1027
915 - $formattedContacts = Collection::make($contactAssociatedTasks)
916 - ->groupBy('crm_contact_id')
917 - ->map(function ($tasks, $contactId) {
918 - $subscriber = Subscriber::find($contactId);
919 - if (!$subscriber) {
1028 + $tasksByContact = [];
1029 + foreach ($contactAssociatedTasks as $task) {
1030 + $tasksByContact[absint($task->crm_contact_id)][] = $task;
1031 + }
1032 +
1033 + $boardContactIds = Meta::query()
1034 + ->where('object_id', absint($board_id))
1035 + ->where('object_type', Constant::OBJECT_TYPE_BOARD)
1036 + ->whereIn('key', [
1037 + Constant::BOARD_ASSOCIATED_CRM_CONTACT,
1038 + self::LEGACY_BOARD_ASSOCIATED_CRM_CONTACT,
1039 + ])
1040 + ->pluck('value')
1041 + ->toArray();
1042 + $boardContactIds = array_values(array_unique(array_filter(array_map('absint', $boardContactIds))));
1043 +
1044 + $contactIds = array_values(array_unique(array_filter(array_map('absint', array_merge(
1045 + array_keys($tasksByContact),
1046 + $boardContactIds
1047 + )))));
1048 +
1049 + usort($contactIds, function ($firstContactId, $secondContactId) use ($boardContactIds) {
1050 + return (int) in_array($secondContactId, $boardContactIds, true) - (int) in_array($firstContactId, $boardContactIds, true);
1051 + });
1052 +
1053 + $formattedContacts = Collection::make($contactIds)
1054 + ->map(function ($contactId) use ($tasksByContact, $boardContactIds) {
1055 + $contact = Helper::crm_contact($contactId);
1056 + if (!$contact) {
920 1057 return null; // Skip if subscriber not found
921 1058 }
922 1059
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 - ];
1060 + $tasks = $tasksByContact[$contactId] ?? [];
1061 + $contact['name'] = trim(($contact['first_name'] ?? '') . ' ' . ($contact['last_name'] ?? '')) ?: ($contact['full_name'] ?? $contact['email'] ?? '');
1062 + $contact['crm_contact_id'] = $contactId;
1063 + $contact['is_board_contact'] = in_array($contactId, $boardContactIds, true);
1064 + $contact['tasks'] = $tasks;
1065 +
1066 + return $contact;
931 1067 })
932 1068 ->filter()->toArray();
933 1069
934 1070
@@ -1004,21 +1140,52 @@
1004 1140 }
1005 1141
1006 1142 public function archiveAllTasksInStage($board_id, $stage_id)
1007 1143 {
1008 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
1009 - return [
1010 - 'message' => __('Tasks have been archived', 'fluent-boards'),
1011 - 'updatedTasks' => $updates,
1012 - ];
1144 + $board_id = absint($board_id);
1145 + $stage_id = absint($stage_id);
1146 +
1147 + try {
1148 + $this->findStageOnBoard($stage_id, $board_id);
1149 + $updates = $this->stageService->archiveAllTasksInStage($stage_id, $board_id);
1150 +
1151 + return [
1152 + 'message' => __('Tasks have been archived', 'fluent-boards'),
1153 + 'updatedTasks' => $updates,
1154 + ];
1155 + } catch (\Exception $e) {
1156 + return $this->sendError($e->getMessage(), 400);
1157 + }
1013 1158 }
1014 1159
1015 1160 public function getAssociatedBoards(Request $request, $associated_id)
1016 1161 {
1017 - $associatedBoards = $this->boardService->getAssociatedBoards($associated_id);
1162 + if (!$this->currentUserCanReadCrmContacts()) {
1163 + return $this->sendError(esc_html__('You do not have permission to view CRM contact boards', 'fluent-boards'), 403);
1164 + }
1165 +
1166 + $associatedId = absint($associated_id);
1167 +
1168 + if (!$associatedId) {
1169 + return $this->sendError(__('Invalid CRM contact', 'fluent-boards'), 400);
1170 + }
1171 +
1172 + $associatedBoards = $this->boardService->getAssociatedBoards($associatedId, get_current_user_id());
1173 +
1018 1174 return [
1019 1175 'boards' => $associatedBoards,
1020 1176 ];
1177 + }
1178 +
1179 + private function currentUserCanReadCrmContacts()
1180 + {
1181 + $permissionManager = 'FluentCrm\\App\\Services\\PermissionManager';
1182 +
1183 + if (!class_exists($permissionManager)) {
1184 + return false;
1185 + }
1186 +
1187 + return (bool) $permissionManager::currentUserCan('fcrm_read_contacts');
1021 1188 }
1022 1189
1023 1190 public function duplicateBoard(Request $request, $board_id)
1024 1191 {