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
fluent-boards / app / Http / Controllers / BoardController.php

BoardController.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.1.0, at app/Http/Controllers/BoardController.php

1,535 lines 52.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Http\Controllers;
4
5 use FluentBoards\App\Models\Attachment;
6 use FluentBoards\App\Models\Meta;
7 use FluentBoards\App\Models\Relation;
8 use FluentBoards\App\Models\Task;
9 use FluentBoards\App\Models\User;
10 use FluentBoards\App\Models\Board;
11 use FluentBoards\App\Services\CommentService;
12 use FluentBoards\App\Services\Constant;
13 use FluentBoards\App\Services\DescriptionMarkdownConverter;
14 use FluentBoards\App\Services\Helper;
15 use FluentBoards\App\Models\Stage;
16 use FluentBoards\App\Services\InstallService;
17 use FluentBoards\App\Services\StageService;
18 use FluentBoards\App\Services\TaskService;
19 use FluentBoards\App\Services\BoardService;
20 use FluentBoards\App\Services\FolderService;
21 use FluentBoards\App\Services\UploadService;
22 use FluentBoards\Framework\Http\Request\Request;
23 use FluentBoards\App\Services\PermissionManager;
24 use FluentBoards\App\Services\PublicAccessService;
25 use FluentBoards\App\Hooks\Handlers\BoardHandler;
26 use FluentBoards\App\Hooks\Handlers\BoardMenuHandler;
27 use FluentBoards\App\Services\LabelService;
28 use FluentBoards\Framework\Support\Arr;
29 use FluentBoards\Framework\Support\Collection;
30 use FluentBoardsPro\App\Services\AttachmentService;
31 use FluentBoardsPro\App\Services\CustomFieldService;
32 use FluentBoardsPro\App\Services\RemoteUrlParser;
33
34 class BoardController extends Controller
35 {
36 private const LEGACY_BOARD_ASSOCIATED_CRM_CONTACT = 'crm_contact';
37
38 private $boardService;
39 private $taskService;
40 private $stageService;
41 private $labelService;
42
43 public function __construct(
44 BoardService $boardService,
45 TaskService $taskService,
46 StageService $stageService,
47 LabelService $labelService
48 )
49 {
50 parent::__construct();
51 $this->boardService = $boardService;
52 $this->taskService = $taskService;
53 $this->stageService = $stageService;
54 $this->labelService = $labelService;
55 }
56
57 public function getBoards(Request $request)
58 {
59 $per_page = $request->getSafe('per_page', 'intval', 100);
60 $per_page = max(1, min(100, $per_page));
61 $userId = get_current_user_id();
62 $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
63
64 $order = $request->getSafe('order', 'sanitize_text_field', 'created_at');
65 $orderBy = $request->getSafe('orderBy', 'sanitize_text_field', 'DESC');
66 $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
67
68 $option = $request->getSafe('option', 'sanitize_text_field');
69 $folderId = $request->getSafe('fid', 'intval'); // Get folder ID from request
70
71 // Initialize the query based on archive status
72 if (!defined('FLUENT_ROADMAP')) {
73 if ($option == 'archived') {
74 $relatedBoardsQuery = Board::whereNotNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
75 } else {
76 $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
77 }
78 } else {
79 if ($option == 'archived') {
80 $relatedBoardsQuery = Board::whereNotNull('archived_at')->byAccessUser($userId);
81 } else {
82 $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
83 }
84 }
85
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);
95 }
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
104 // Filter out boards that are templates (exclude boards where settings->is_template is true)
105 $relatedBoardsQuery = $relatedBoardsQuery->excludeTemplates();
106
107 // Add search functionality
108 if (!empty($searchInput)) {
109 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
110 }
111
112 $relatedBoards = $relatedBoardsQuery->orderBy($order, $orderBy)
113 ->withCount('completedTasks')
114 ->with('stages', 'users')
115 ->paginate($per_page);
116
117 foreach ($relatedBoards as $relatedBoard) {
118 $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description);
119 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
120 $relatedBoard->is_pinned = $this->boardService->isPinned($relatedBoard->id);
121 }
122
123 $response = [
124 'boards' => $relatedBoards,
125 'board_counts' => $this->boardService->getBoardCounts($userId)
126 ];
127
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;
134 }
135
136 return $this->sendSuccess($response);
137 }
138
139 /**
140 * Get folder mapping for boards
141 */
142 private function getBoardFolderMapping($userId)
143 {
144 $folderService = new FolderService();
145 $folders = $folderService->getFolders($userId);
146
147 $mapping = [];
148 foreach ($folders as $folder) {
149 $mapping[$folder->id] = [
150 'id' => $folder->id,
151 'title' => $folder->title,
152 'board_ids' => $folder->boards ? $folder->boards->pluck('id')->toArray() : []
153 ];
154 }
155
156 return $mapping;
157 }
158
159 private function getCurrentFolderInfoFromMapping(array $folder)
160 {
161 return [
162 'id' => $folder['id'],
163 'title' => $folder['title'],
164 'board_count' => count($folder['board_ids'])
165 ];
166 }
167
168 /**
169 * Get the list of boards and their associated stages for the current user.
170 *
171 * @param \FluentBoards\Framework\Http\Request\Request $request
172 * @return \WP_REST_Response
173 */
174 public function getBoardsList(Request $request)
175 {
176 $userId = get_current_user_id();
177
178 // Query to fetch boards that are not archived and accessible by the user
179 // Check if the FLUENT_ROADMAP constant is defined
180 if (!defined('FLUENT_ROADMAP')) {
181 $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
182 } else {
183 $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
184 }
185
186 $relatedBoards = $relatedBoardsQuery->with('stages')->get();
187
188 // Fetch the stages associated with the boards
189 $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();
190
191 return $this->sendSuccess([
192 'boards' => $relatedBoards,
193 'all_stages' => $stages,
194 ], 200);
195 }
196 public function getOnlyBoardsByUser(Request $request)
197 {
198 try {
199 $userId = get_current_user_id();
200
201 $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
202
203
204 if(!defined('FLUENT_ROADMAP'))
205 {
206 $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
207 } else {
208 $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
209 }
210
211 if (!empty($searchInput)) {
212 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
213 }
214
215 $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();
216
217 return $this->sendSuccess([
218 'boards' => $relatedBoards
219 ]);
220 } catch (\Exception $e) {
221 return $this->sendError([
222 'message' => $e->getMessage()
223 ]);
224 }
225 }
226
227 public function getRecentBoards()
228 {
229 $boards = $this->boardService->getRecentBoards();
230
231 if (!$boards || $boards->isEmpty()) {
232 $boards = Board::whereNull('archived_at')
233 ->excludeTemplates()
234 ->availableInCurrentInstall()
235 ->byAccessUser(get_current_user_id())
236 ->limit(4)
237 ->withCount('completedTasks')
238 ->with(['stages', 'users'])
239 ->get();
240 }
241
242 foreach ($boards as $board) {
243 $board->users = Helper::sanitizeUserCollections($board->users);
244 $board->is_pinned = $this->boardService->isPinned($board->id);
245 }
246
247 return [
248 'boards' => $boards,
249 ];
250 }
251
252 /*
253 * TODO: Refactor this method , remove this
254 */
255 public function getBoardsByType($type)
256 {
257 $boards = $this->boardService->getBoardsByType($type);
258
259 return $this->sendSuccess([
260 'boards' => $boards,
261 ]);
262 }
263
264 public function createFirstBoard(Request $request)
265 {
266 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
267 'title' => 'required|string',
268 'description' => 'nullable',
269 'type' => 'required|string',
270 'currency' => 'nullable|string',
271 'crm_contact_id' => 'nullable|numeric',
272 ]);
273
274 $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
275
276 $postStages = $request->get('stages');
277 if (!is_array($postStages)) {
278 $postStages = [];
279 }
280 $stageData = array();
281 foreach ($postStages as $stage) {
282 $temp = $this->stageSanitizeAndValidate($stage, [
283 'title' => 'required|string',
284 ]);
285 $stageData[] = $temp;
286 }
287
288 $taskData = null;
289 if ($request->get('task')) {
290 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
291 'title' => 'required|string',
292 ]);
293 }
294
295 $board = $this->boardService->createBoard($boardData);
296 $this->labelService->createDefaultLabel($board->id);
297 $type = ucfirst($boardData['type']);
298 $stage = $this->stageService->createStages($board, $stageData);
299
300 if ($taskData) {
301 $taskData['board_id'] = $board->id;
302 $taskData['stage_id'] = $stage->id;
303 $this->taskService->createTask($taskData, $board->id);
304 }
305
306 do_action('fluent_boards/board_created', $board);
307
308 if ($installFluentCRM && !defined('FLUENTCRM')) {
309 InstallService::install('fluent-crm');
310 }
311
312 return [
313 'message' => __('Board has been created', 'fluent-boards'),
314 'board' => $board,
315 ];
316 }
317
318 public function skipOnboarding(Request $request)
319 {
320 $onboarding = Meta::where('key', Constant::FBS_ONBOARDING)->first();
321 if($onboarding && $onboarding->value == 'no'){
322 $onboarding->value = 'yes' ;
323 $onboarding->save();
324 }
325
326 return [
327 'message' => __('Onboarding skipped successfully', 'fluent-boards'),
328 ];
329 }
330
331 public function create(Request $request)
332 {
333 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
334 'title' => 'required|string',
335 'description' => 'nullable',
336 'type' => 'required|string',
337 'currency' => 'nullable|string',
338 'crm_contact_id' => 'nullable|numeric',
339 'folder_id' => 'nullable',
340 ]);
341
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
356 $board = $this->boardService->createBoard($boardData);
357 $this->createBoardLabelsFromRequest($request, $board->id);
358 $this->addBoardMembersFromRequest($request, $board->id);
359 $type = ucfirst($boardData['type']);
360 $stages = $request->get('stages');
361 $sanitizedStages = [];
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
373 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
374 $this->stageService->createRoadmapStages($board, $sanitizedStages);
375 } elseif (!empty($sanitizedStages)) {
376 $this->stageService->createStages($board, $sanitizedStages);
377 } else {
378 $this->stageService->createDefaultStages($board);
379 }
380
381 // if board is created from crm contact
382 if (isset($boardData['crm_contact_id'])) {
383 $this->boardService->updateAssociateMember($boardData['crm_contact_id'], $board->id);
384 }
385
386 do_action('fluent_boards/board_created', $board);
387
388
389 if ($folderId) {
390 $folderService->addBoardToFolder($folderId, [$board->id]);
391 }
392
393 $message = __('Board has been created successfully', 'fluent-boards');
394
395 return $this->send([
396 'message' => $message,
397 'board' => $board,
398 ], 201);
399 } catch (\Exception $e) {
400 return $this->sendError([
401 'message' => $e->getMessage()
402 ]);
403 }
404 }
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 */
519 public function getArchivedStage(Request $request, $board_id)
520 {
521 try {
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 ];
528
529 $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
530
531 return $this->sendSuccess([
532 'stages' => $stages,
533 ], 200);
534 } catch (\Exception $e) {
535 return $this->sendError($e->getMessage(), 404);
536 }
537 }
538
539 public function find(Request $request, $board_id)
540 {
541 $board = Board::findOrFail($board_id);
542 $board->description = DescriptionMarkdownConverter::normalize($board->description);
543 $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
544 $board->background = maybe_unserialize($board->background);
545 $board->createdOn = $board->created_at->format('Y-m-d');
546
547 $board->load(['users', 'labels', 'owner']);
548
549 if ($includeArchived) {
550 $board->stages = Stage::where('board_id', $board_id)
551 ->orderBy('position', 'asc')
552 ->get();
553 } else {
554 $board->load('stages');
555 }
556
557 if (defined('FLUENT_BOARDS_PRO')){
558 $customFiledPositionMeta = $board->getMetaByKey('custom_field_positions');
559 if(!$customFiledPositionMeta) {
560 (new CustomFieldService())->reIndexCustomFieldPositions($board_id);
561 $board->updateMeta('custom_field_positions', 'yes');
562 }
563
564 $board->load(['customFields']);
565 }
566
567 $this->boardService->updateRecentBoards($board_id);
568
569 $board->labelColor = Constant::TRELLO_COLOR_MAP;
570 $board->labelColorText = Constant::TEXT_COLOR_MAP;
571 $board->labelColorPresets = Constant::LABEL_COLOR_PRESETS;
572
573 $board->users = Helper::sanitizeUserCollections($board->users);
574 $board->owner = Helper::sanitizeUserCollections($board->owner);
575
576 $board->is_pinned = $this->boardService->isPinned($board->id);
577
578 $board = apply_filters('fluent_boards/board_find', $board);
579
580 return [
581 'board' => $board,
582 'synced_at' => current_time('mysql')
583 ];
584 }
585
586 public function update(Request $request, $board_id)
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
598 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
599 'title' => 'required|string',
600 'description' => 'nullable|string',
601 ]);
602
603 $board = Board::findOrFail($board_id);
604 $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
605
606 $oldBoard = clone $board;
607 $board->fill($boardData);
608 $board->save();
609
610 do_action('fluent_boards/board_updated', $board, $oldBoard);
611
612 return [
613 'message' => __('Board has been updated', 'fluent-boards'),
614 'board' => $board,
615 'stages' => $board->stages()->get(),
616 ];
617 }
618
619 public function archiveStage($board_id, $stage_id)
620 {
621 $board_id = absint($board_id);
622 $stage_id = absint($stage_id);
623
624 try {
625 $stage = $this->findStageOnBoard($stage_id, $board_id);
626
627 $updatedStage = $this->boardService->archiveStage($board_id, $stage);
628
629 return $this->sendSuccess([
630 'updatedStage' => $updatedStage,
631 'message' => __('Stage has been archived', 'fluent-boards'),
632 ], 200);
633 } catch (\Exception $e) {
634 return $this->sendError($e->getMessage(), 400);
635 }
636 }
637
638 public function restoreStage($board_id, $stage_id)
639 {
640 $board_id = absint($board_id);
641 $stage_id = absint($stage_id);
642
643 try {
644 $stage = $this->findStageOnBoard($stage_id, $board_id);
645
646 $updatedStage = $this->boardService->restoreStage($board_id, $stage);
647
648 return $this->sendSuccess([
649 'success' => true,
650 'updatedStage' => $updatedStage,
651 'message' => __('Stage has been restored', 'fluent-boards')
652 ], 200);
653 } catch (\Exception $e) {
654 return $this->sendError($e->getMessage(), 400);
655 }
656 }
657
658
659 public function repositionStages(Request $request, $board_id)
660 {
661 $incomingList = $request->get('list');
662 if (!is_array($incomingList)) {
663 $incomingList = [];
664 }
665 $incomingList = array_map('intval', $incomingList);
666 try {
667 foreach ($incomingList as $stageId) {
668 $this->findStageOnBoard($stageId, $board_id);
669 }
670
671 $this->boardService->repositionStages($board_id, $incomingList);
672 return $this->sendSuccess([
673 'message' => __('Stages Reordered', 'fluent-boards'),
674 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
675 ], 200);
676 } catch (\Exception $e) {
677 return $this->sendError($e->getMessage(), 400);
678 }
679 }
680
681 public function getAssigneesByBoard($board_id)
682 {
683 return $this->sendSuccess([
684 'data' => $this->boardService->getAssigneesByBoard($board_id),
685 ], 200);
686 }
687
688 public function delete($board_id)
689 {
690 try {
691 if (!PermissionManager::isAdmin()) {
692 throw new \Exception(esc_html__('You do not have permission to delete this board', 'fluent-boards'), 400);
693 }
694 $this->boardService->deleteBoard($board_id);
695
696 return $this->sendSuccess([
697 'message' => __('Board has been deleted', 'fluent-boards'),
698 ], 200);
699 } catch (\Exception $e) {
700 return $this->sendError($e->getMessage(), 400);
701 }
702 }
703
704 public function getCurrencies()
705 {
706 return BoardHandler::getCurrencies();
707 }
708
709 public function getActivities(Request $request, $board_id)
710 {
711 try {
712 $activities = $this->boardService->getActivities($board_id, [
713 'per_page' => $request->getSafe('per_page', 'intval', 40),
714 'page' => $request->getSafe('page', 'intval', 1),
715 ]);
716 return $this->sendSuccess([
717 'activities' => $activities,
718 ], 200);
719 } catch (\Exception $e) {
720 return $this->sendError($e->getMessage(), 404);
721 }
722 }
723
724 /*
725 * TODO: Refactor this method - for Masiur
726 */
727 public function getBoardUsers($board_id)
728 {
729 $board = Board::findOrFail($board_id);
730
731 $boardObjects = Relation::where('object_type', 'board_user')
732 ->where('object_id', $board_id)
733 ->get()->keyBy('foreign_id');
734
735 $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
736 ->get()->pluck('object_id')->toArray();
737
738 $userIds = $boardObjects->pluck('foreign_id')->toArray();
739
740 $coreUsers = [];
741 if ($userIds) {
742 // Get the users who are in the board (members and managers
743 $coreUsers = get_users([
744 'include' => $userIds
745 ]);
746 }
747
748 $formattedUsers = [];
749
750 foreach ($coreUsers as $user) {
751 $name = trim($user->first_name . ' ' . $user->last_name);
752 if (!$name) {
753 $name = $user->display_name;
754 }
755
756 $boardRelation = $boardObjects[$user->ID] ?? null;
757
758
759 $formattedUsers[] = [
760 'ID' => $user->ID,
761 'display_name' => $name,
762 'email' => $user->user_email,
763 'photo' => fluent_boards_user_avatar($user->user_email, $name),
764 'role' => $this->boardUserRole($boardRelation),
765 'is_super' => in_array($user->ID, $superAdminIds),
766 'is_wpadmin' => $user->has_cap('manage_options')
767 ];
768 }
769
770 // order formatted users by display_name
771 usort($formattedUsers, function ($a, $b) {
772 return strcmp($a['display_name'], $b['display_name']);
773 });
774
775 $returnData = [
776 'users' => Helper::sanitizeUsersArray($formattedUsers, $board_id),
777 'global_admins' => []
778 ];
779
780 if (!PermissionManager::isAdmin(get_current_user_id())) {
781 return $returnData;
782 }
783
784 /*
785 * These are the rest of the Fluent Boards and WordPress admins who are not in the board.
786 */
787 $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
788 ->whereNotIn('object_id', $userIds)
789 ->get()
790 ->pluck('object_id')
791 ->toArray();
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
801 if ($adminUserIds) {
802 $adminUsers = get_users([
803 'include' => $adminUserIds,
804 ]);
805
806 $formattedAdminUsers = [];
807
808 foreach ($adminUsers as $user) {
809 $name = trim($user->first_name . ' ' . $user->last_name);
810 if (!$name) {
811 $name = $user->display_name;
812 }
813
814 $formattedAdminUsers[] = [
815 'ID' => $user->ID,
816 'display_name' => $name,
817 'email' => $user->user_email,
818 'photo' => fluent_boards_user_avatar($user->user_email, $name),
819 'role' => 'admin',
820 'is_super' => in_array($user->ID, $superAdminIds),
821 'is_wpadmin' => $user->has_cap('manage_options')
822 ];
823 }
824
825 // order formatted users by display_name
826 usort($formattedAdminUsers, function ($a, $b) {
827 return strcmp($a['display_name'], $b['display_name']);
828 });
829
830 $returnData['global_admins'] = Helper::sanitizeUsersArray($formattedAdminUsers, $board_id);
831 }
832
833 return $this->sendSuccess($returnData, 200);
834 }
835
836
837 public function removeUserFromBoard($board_id, $userId)
838 {
839 $this->boardService->removeUserFromBoard($board_id, $userId);
840
841 if (!PermissionManager::isAdmin($userId)) {
842 $this->boardService->removeFromRecentlyOpened($board_id, $userId);
843 }
844
845 return [
846 'message' => __('Member removed successfully', 'fluent-boards'),
847 ];
848 }
849
850 public function addMembersInBoard(Request $request, $board_id)
851 {
852 $memberId = $request->getSafe('memberId', 'intval');
853 $isViewerOnly = $request->getSafe('isViewerOnly', 'sanitize_text_field');
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
862 if (!$member) {
863 return $this->sendError([
864 'message' => __('User already a member', 'fluent-boards'),
865 ], 409);
866 }
867
868
869 return [
870 'message' => __('Member added successfully', 'fluent-boards'),
871 'member' => Helper::sanitizeUserCollections($member)
872 ];
873 }
874
875 private function boardSanitizeAndValidate($data, array $rules = [])
876 {
877 $data = Helper::sanitizeBoard($data);
878
879 return $this->validate($data, $rules);
880 }
881
882 private function stageSanitizeAndValidate($data, array $rules = [])
883 {
884 $data = Helper::sanitizeStage($data);
885
886 return $this->validate($data, $rules);
887 }
888
889 private function taskSanitizeAndValidate($data, array $rules = [])
890 {
891 $data = Helper::sanitizeTask($data);
892
893 return $this->validate($data, $rules);
894 }
895
896 public function searchBoards(Request $request)
897 {
898 $per_page = $request->getSafe('per_page', 'intval', 10);
899 $search_input = $request->getSafe('searchInput', 'sanitize_text_field', '');
900 $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
901
902 $currentUserId = get_current_user_id();
903
904 if (PermissionManager::isAdmin($currentUserId)) {
905 $boards = Board::query()->where('type', $type)
906 ->where('title', 'like', '%' . $search_input . '%')
907 ->with('stages', 'tasks', 'users')
908 ->paginate($per_page);
909
910 foreach ($boards as $board) {
911 $board->users = Helper::sanitizeUserCollections($board->users);
912 }
913
914 } else {
915 $currentUser = User::find($currentUserId);
916 $boards = $currentUser->boards()->where('type', $type)->where('title', 'like', '%' . $search_input . '%')->paginate($per_page);
917 }
918
919 return [
920 'boards' => $boards,
921 ];
922 }
923
924 public function getUsersOfBoards()
925 {
926 $userBoards = $this->boardService->getUsersOfBoards();
927
928 return $this->sendSuccess([
929 'userBoards' => $userBoards,
930 ], 200);
931 }
932
933
934
935 /**
936 * Refactor this code form me - Masiur
937 * change stage settings is_public for roadmap user and admin view
938 * @param $board_id
939 * @param $stage_id
940 * @return
941 */
942 public function changeStageView($board_id, $stage_id)
943 {
944 $board_id = absint($board_id);
945 $stage_id = absint($stage_id);
946
947 try {
948 $stage = $this->findStageOnBoard($stage_id, $board_id);
949 $message = __('The stage is made public!', 'fluent-boards');
950 $settings = $stage->settings;
951
952 if (isset($settings['is_public'])) {
953 if ($settings['is_public']) {
954 $settings['is_public'] = false;
955 $message = __('The stage is made private!', 'fluent-boards');
956 } else {
957 $settings['is_public'] = true;
958 }
959 } else {
960 $settings['is_public'] = true;
961 }
962
963 $stage->settings = $settings;
964 $stage->save();
965 return $this->sendSuccess([
966 'message' => $message,
967 'stage' => $stage
968 ]);
969 } catch (\Exception $e) {
970 return $this->sendError($e->getMessage(), 400);
971 }
972 }
973
974
975 /**
976 * Set or reset board background image/color.
977 * @param \FluentBoards\Framework\Http\Request\Request $request
978 * @return
979 */
980 public function setBoardBackground(Request $request, $board_id)
981 {
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) {
990 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
991 'id' => 'required|integer',
992 'image_url' => 'required|string|url',
993 ]);
994 } elseif ($request->color) {
995 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
996 "id" => 'required',
997 'color' => 'required',
998 ]);
999 }
1000
1001 try {
1002 if (!$board_id) {
1003 $errorMessage = __('Board id is required', 'fluent-boards');
1004 throw new \Exception(esc_html($errorMessage), 400);
1005 }
1006
1007 if (empty($backgroundData)) {
1008 $errorMessage = __('Background data is required', 'fluent-boards');
1009 throw new \Exception(esc_html($errorMessage), 400);
1010 }
1011
1012 return $this->sendSuccess([
1013 'message' => __('Background updated successfully', 'fluent-boards'),
1014 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
1015 ]);
1016 } catch (\Exception $e) {
1017 return $this->sendError($e->getMessage(), 400);
1018 }
1019 }
1020
1021
1022 /**
1023 * Summary of getStageTaskAvailablePositions
1024 * @param mixed $board_id
1025 * @param mixed $stage_slug
1026 * @return $availablePositions as an array
1027 * @throws \Exception
1028 */
1029 public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
1030 {
1031 try {
1032 if ($board_id && $stage_id) {
1033 $taskId = $request->getSafe('task_id', 'intval');
1034 $availablePositions = $this->boardService->getStageTaskAvailablePositions($board_id, $stage_id, $taskId);
1035 return $this->sendSuccess([
1036 'availablePositions' => $availablePositions['availablePositions'],
1037 'moveTargets' => $availablePositions['moveTargets'],
1038 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1039 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
1040 ], 200);
1041 } else {
1042 $message = '';
1043 if (!$board_id) {
1044 $message = 'Board id ';
1045 }
1046 if (!$stage_id) {
1047 $message = 'Stage ';
1048 }
1049 throw new \Exception(esc_html($message . 'is required'), 400);
1050 }
1051 } catch (\Exception $e) {
1052 return $this->sendError($e->getMessage(), 400);
1053 }
1054 }
1055
1056 public function getAssociateCrmContacts($board_id)
1057 {
1058 try {
1059 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
1060 ->whereNotNull('crm_contact_id')
1061 ->get();
1062
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) {
1092 return null; // Skip if subscriber not found
1093 }
1094
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;
1102 })
1103 ->filter()->toArray();
1104
1105
1106 return $this->sendSuccess([
1107 'associatedContacts' => $formattedContacts
1108 ], 200);
1109 } catch (\Exception $e) {
1110 return $this->sendError($e->getMessage(), 404);
1111 }
1112 }
1113
1114 public function updateAssociateCrmContact(Request $request, $board_id)
1115 {
1116 $value = $request->getSafe('value');
1117 $this->boardService->updateAssociateMember($value, $board_id);
1118
1119 return $this->sendSuccess([
1120 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
1121 ], 200);
1122 }
1123
1124 public function hasDataChanged(Request $request, $board_id)
1125 {
1126 $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
1127 $since = $request->getSafe('since', 'sanitize_text_field');
1128 return $this->boardService->hasDataChanged($board_id, $includeArchived, $since);
1129 }
1130
1131 public function createStage(Request $request, $board_id)
1132 {
1133 $stageData = $this->stageSanitizeAndValidate($request->all(), [
1134 'title' => 'required|string',
1135 'position' => 'nullable|numeric'
1136 ]);
1137
1138 $board = Board::find($board_id);
1139 $stage = $this->stageService->createStage($stageData, $board_id);
1140
1141 do_action('fluent_boards/board_stage_added', $board, $stage);
1142
1143 $updatedStates = (new StageService())->getLastOneMinuteUpdatedStages($board_id);
1144
1145 return [
1146 'updatedStages' => $updatedStates,
1147 'message' => __('stage has been created', 'fluent-boards'),
1148 ];
1149 }
1150
1151 public function moveAllTasks(Request $request, $board_id)
1152 {
1153 $oldStageId = $request->getSafe('oldStageId', 'intval');
1154 $newStageId = $request->getSafe('newStageId', 'intval');
1155
1156 if (!$oldStageId || !$newStageId) {
1157 return $this->sendError(__('Invalid stage IDs provided', 'fluent-boards'), 400);
1158 }
1159
1160 // Verify stages exist and belong to the board
1161 $oldStage = Stage::where('id', $oldStageId)->where('board_id', $board_id)->first();
1162 $newStage = Stage::where('id', $newStageId)->where('board_id', $board_id)->first();
1163
1164 if (!$oldStage || !$newStage) {
1165 return $this->sendError(__('One or both stages do not exist or do not belong to this board', 'fluent-boards'), 400);
1166 }
1167
1168 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
1169
1170 return [
1171 'message' => __('Tasks have been moved', 'fluent-boards'),
1172 'updatedTasks' => $updates,
1173 ];
1174
1175 }
1176
1177 public function archiveAllTasksInStage($board_id, $stage_id)
1178 {
1179 $board_id = absint($board_id);
1180 $stage_id = absint($stage_id);
1181
1182 try {
1183 $this->findStageOnBoard($stage_id, $board_id);
1184 $updates = $this->stageService->archiveAllTasksInStage($stage_id, $board_id);
1185
1186 return [
1187 'message' => __('Tasks have been archived', 'fluent-boards'),
1188 'updatedTasks' => $updates,
1189 ];
1190 } catch (\Exception $e) {
1191 return $this->sendError($e->getMessage(), 400);
1192 }
1193 }
1194
1195 public function getAssociatedBoards(Request $request, $associated_id)
1196 {
1197 if (!$this->currentUserCanReadCrmContacts()) {
1198 return $this->sendError(esc_html__('You do not have permission to view CRM contact boards', 'fluent-boards'), 403);
1199 }
1200
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
1209 return [
1210 'boards' => $associatedBoards,
1211 ];
1212 }
1213
1214 private function currentUserCanReadCrmContacts()
1215 {
1216 $permissionManager = 'FluentCrm\\App\\Services\\PermissionManager';
1217
1218 if (!class_exists($permissionManager)) {
1219 return false;
1220 }
1221
1222 return (bool) $permissionManager::currentUserCan('fcrm_read_contacts');
1223 }
1224
1225 public function duplicateBoard(Request $request, $board_id)
1226 {
1227 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
1228 'title' => 'required|string'
1229 ]);
1230
1231 $boardData['source_board_id'] = $board_id;
1232
1233 $isWithLabels = $request->getSafe('isWithLabels');
1234 $isWithTasks = $request->getSafe('isWithTasks');
1235 $isWithTemplates = $request->getSafe('isWithTemplates');
1236
1237 try {
1238 if(!PermissionManager::isAdmin()) {
1239 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
1240 throw new \Exception(esc_html($errorMessage), 400);
1241 }
1242 //create board
1243 $newBoard = $this->boardService->copyBoard($boardData);
1244
1245 //label copy
1246 $labelMap = [];
1247
1248 if ($isWithLabels == 'yes') {
1249 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
1250 }
1251
1252 //stage copy
1253 $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
1254
1255 //copy tasks of selected stages
1256 if ($isWithTasks == 'yes') {
1257 $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
1258 }
1259
1260 return $this->sendSuccess([
1261 'board' => $newBoard,
1262 ], 200);
1263 } catch (\Exception $e) {
1264 return $this->sendError($e->getMessage(), 400);
1265 }
1266 }
1267
1268 public function importFromBoard(Request $request, $board_id)
1269 {
1270 $selectedStages = $request->getSafe('selectedStages');
1271 $position = $request->getSafe('position', 'intval');
1272
1273 // Validate and sanitize selectedStages array
1274 if (!is_array($selectedStages)) {
1275 $selectedStages = [$selectedStages];
1276 }
1277 $selectedStages = array_filter(array_map('intval', $selectedStages));
1278
1279 try {
1280 $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
1281
1282 return $this->sendSuccess([
1283 'message' => __('Import successfully', 'fluent-boards'),
1284 ], 200);
1285
1286 } catch (\Exception $e) {
1287 return $this->sendError($e->getMessage(), 400);
1288 }
1289 }
1290
1291 public function getBoardDefaultBackgroundColors()
1292 {
1293 return [
1294 'solidColors' => Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
1295 'gradients' => Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
1296 ];
1297 }
1298
1299 /*
1300 * TODO: For Masiur - I will update this later
1301 */
1302 public function updateBoardProperties(Request $request, $board_id)
1303 {
1304 $pageId = $request->getSafe('page_id');
1305 $enable_stage_change_email = $request->getSafe('enable_stage_change_email');
1306
1307 $board = Board::findOrFail($board_id);
1308
1309 $board->updateMeta('roadmap_page_id', $pageId);
1310 $board->updateMeta('enable_stage_change_email', $enable_stage_change_email);
1311
1312 $board = $board->fresh();
1313
1314 return [
1315 'message' => __('Board has been updated', 'fluent-boards'),
1316 'board' => apply_filters('fluent_boards/board_find', $board)
1317 ];
1318 }
1319
1320 public function archiveBoard($board_id)
1321 {
1322 try {
1323 $board = $this->boardService->archiveBoard($board_id);
1324
1325 return [
1326 'board' => $board,
1327 'message' => __('Board has been archived successfully!', 'fluent-boards')
1328 ];
1329 } catch (\Exception $e) {
1330 return $this->sendError($e->getMessage(), 400);
1331 }
1332 }
1333
1334 public function restoreBoard($board_id)
1335 {
1336 try {
1337 $board = $this->boardService->restoreBoard($board_id);
1338
1339 return [
1340 'board' => $board,
1341 'message' => __('Board has been restored successfully!', 'fluent-boards')
1342 ];
1343 } catch (\Exception $e) {
1344 return $this->sendError($e->getMessage(), 400);
1345 }
1346 }
1347
1348 private function boardUserRole($boardRelation)
1349 {
1350 return $boardRelation && Arr::get($boardRelation->settings, 'is_admin')
1351 ? 'manager'
1352 : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
1353 ? 'viewer'
1354 : 'member');
1355 }
1356 public function uploadBoardBackground(Request $request,$board_id)
1357 {
1358 $file = Arr::get($request->files(), 'file')->toArray();
1359 (new \FluentBoards\App\Services\UploadService)->validateFile($file);
1360
1361 $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
1362
1363 $fileData = $uploadInfo[0];
1364 $initialDataData = [
1365 'type' => 'url',
1366 'url' => '',
1367 'name' => '',
1368 'size' => 0,
1369 ];
1370
1371 $attachData = array_merge($initialDataData, $fileData);
1372 $UrlMeta = [];
1373 if($attachData['type'] == 'url') {
1374 $UrlMeta = RemoteUrlParser::parse($attachData['url']);
1375 }
1376 $uid = wp_generate_uuid4();
1377 $fileUploadedData = new Attachment();
1378 $fileUploadedData->object_id = $board_id;
1379 $fileUploadedData->object_type = Constant::BOARD_BACKGROUND_IMAGE;
1380 $fileUploadedData->attachment_type = $attachData['type'];
1381 $fileUploadedData->title = (new TaskService())->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
1382 $fileUploadedData->file_path = $attachData['type'] != 'url' ? $attachData['file'] : null;
1383 $fileUploadedData->full_url = esc_url($attachData['url']);
1384 $fileUploadedData->file_size = $attachData['size'];
1385 $fileUploadedData->settings = $attachData['type'] == 'url' ? [
1386 'meta' => $UrlMeta
1387 ] : '';
1388 $fileUploadedData->driver = 'local';
1389 $fileUploadedData->file_hash = md5($uid . wp_rand(0, 1000));
1390 $fileUploadedData->save();
1391 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
1392 $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
1393 $fileUploadedData['driver'] = $mediaData['driver'];
1394 $fileUploadedData['file_path'] = $mediaData['file_path'];
1395 $fileUploadedData['full_url'] = $mediaData['full_url'];
1396 $fileUploadedData->save();
1397 }
1398
1399 $board = Board::find($board_id);
1400 $oldBackground = $board->background;
1401 $publicUrl = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
1402 $background = [
1403 'color' => null,
1404 'id' => $fileUploadedData->id,
1405 'image_url' => $publicUrl,
1406 'is_image' => true,
1407 ];
1408 $board->background = $background;
1409 $board->save();
1410 do_action('fluent_boards/board_background_updated', $board_id, $oldBackground);
1411
1412 return $this->sendSuccess([
1413 'message' => __('Background updated successfully', 'fluent-boards'),
1414 'background' => $board->background,
1415 ]);
1416 }
1417
1418 public function getPinnedBoards()
1419 {
1420 $pinnedBoards = $this->boardService->getPinnedBoards();
1421
1422 return $this->sendSuccess([
1423 'pinnedBoards' => $pinnedBoards,
1424 ], 200);
1425 }
1426
1427 public function pinBoard($boardId)
1428 {
1429 $this->boardService->pinBoard($boardId);
1430
1431 return $this->sendSuccess([
1432 'message' => __('The Board has been pinned', 'fluent-boards'),
1433 ], 200);
1434 }
1435
1436 public function unpinBoard($boardId)
1437 {
1438 $remove = $this->boardService->unpinBoard($boardId);
1439
1440 if (!$remove) {
1441 return $this->sendError([
1442 'message' => __('Board is not pinned', 'fluent-boards'),
1443 ], 400);
1444 }
1445
1446 return $this->sendSuccess([
1447 'message' => __('Board is removed from pinned boards', 'fluent-boards'),
1448 ], 200);
1449 }
1450
1451 public function getBoardFolder($board_id)
1452 {
1453 try {
1454 $folder = $this->boardService->getBoardFolder($board_id);
1455 return $this->sendSuccess([
1456 'folder' => $folder,
1457 ], 200);
1458 } catch (\Exception $e) {
1459 return $this->sendError($e->getMessage(), 400);
1460 }
1461 }
1462
1463 public function getBoardMenuItems($board_id)
1464 {
1465 try {
1466 $menuItems = (new BoardMenuHandler())->getMenuItems($board_id);
1467
1468 return $this->sendSuccess([
1469 'menu_items' => $menuItems
1470 ], 200);
1471 } catch (\Exception $e) {
1472 return $this->sendError($e->getMessage(), 500);
1473 }
1474 }
1475
1476 public function getPublicAccessSettings($board_id)
1477 {
1478 $board_id = absint($board_id);
1479 $board = Board::findOrFail($board_id);
1480
1481 $enabled = (bool) $board->getMetaByKey('public_access_enabled');
1482 $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1483
1484 return $this->sendSuccess([
1485 'enabled' => $enabled,
1486 'shortcode' => $shortcode,
1487 ], 200);
1488 }
1489
1490 public function togglePublicAccess(Request $request, $board_id)
1491 {
1492 $board_id = absint($board_id);
1493 $board = Board::findOrFail($board_id);
1494
1495 $enabled = filter_var(
1496 $request->getSafe('enabled', 'sanitize_text_field', false),
1497 FILTER_VALIDATE_BOOLEAN
1498 );
1499
1500 $board->updateMeta('public_access_enabled', $enabled ? '1' : '');
1501
1502 $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1503
1504 return $this->sendSuccess([
1505 'message' => $enabled
1506 ? __('Public access has been enabled', 'fluent-boards')
1507 : __('Public access has been disabled', 'fluent-boards'),
1508 'enabled' => $enabled,
1509 'shortcode' => $shortcode,
1510 ], 200);
1511 }
1512
1513 /**
1514 * Resolve a stage only when it belongs to the requested board.
1515 *
1516 * @param int $stageId
1517 * @param int $boardId
1518 * @return Stage
1519 * @throws \Exception
1520 */
1521 private function findStageOnBoard($stageId, $boardId)
1522 {
1523 $stage = Stage::where('id', absint($stageId))
1524 ->where('board_id', absint($boardId))
1525 ->first();
1526
1527 if (!$stage) {
1528 throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
1529 }
1530
1531 return $stage;
1532 }
1533
1534 }
1535