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

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

1,333 lines 44.9 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\Helper;
14 use FluentBoards\App\Models\Stage;
15 use FluentBoards\App\Services\InstallService;
16 use FluentBoards\App\Services\StageService;
17 use FluentBoards\App\Services\TaskService;
18 use FluentBoards\App\Services\BoardService;
19 use FluentBoards\App\Services\UploadService;
20 use FluentBoards\Framework\Http\Request\Request;
21 use FluentBoards\App\Services\PermissionManager;
22 use FluentBoards\App\Services\PublicAccessService;
23 use FluentBoards\App\Hooks\Handlers\BoardHandler;
24 use FluentBoards\App\Hooks\Handlers\BoardMenuHandler;
25 use FluentBoards\App\Services\LabelService;
26 use FluentBoards\Framework\Support\Arr;
27 use FluentBoards\Framework\Support\Collection;
28 use FluentBoardsPro\App\Services\AttachmentService;
29 use FluentBoardsPro\App\Services\CustomFieldService;
30 use FluentBoardsPro\App\Services\ProHelper;
31 use FluentBoardsPro\App\Services\RemoteUrlParser;
32 use FluentCrm\App\Models\Subscriber;
33
34 class BoardController extends Controller
35 {
36 private $boardService;
37 private $taskService;
38 private $stageService;
39 private $labelService;
40
41 public function __construct(
42 BoardService $boardService,
43 TaskService $taskService,
44 StageService $stageService,
45 LabelService $labelService
46 )
47 {
48 parent::__construct();
49 $this->boardService = $boardService;
50 $this->taskService = $taskService;
51 $this->stageService = $stageService;
52 $this->labelService = $labelService;
53 }
54
55 public function getBoards(Request $request)
56 {
57 $per_page = $request->getSafe('per_page', 'intval', 100);
58 $userId = get_current_user_id();
59 $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
60
61 $order = $request->getSafe('order', 'sanitize_text_field', 'created_at');
62 $orderBy = $request->getSafe('orderBy', 'sanitize_text_field', 'DESC');
63 $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
64
65 $option = $request->getSafe('option', 'sanitize_text_field');
66 $folderId = $request->getSafe('fid', 'intval'); // Get folder ID from request
67
68 // Initialize the query based on archive status
69 if (!defined('FLUENT_ROADMAP')) {
70 if ($option == 'archived') {
71 $relatedBoardsQuery = Board::whereNotNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
72 } else {
73 $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
74 }
75 } else {
76 if ($option == 'archived') {
77 $relatedBoardsQuery = Board::whereNotNull('archived_at')->byAccessUser($userId);
78 } else {
79 $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
80 }
81 }
82
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);
87 }
88
89 // 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 });
97
98 // Add search functionality
99 if (!empty($searchInput)) {
100 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
101 }
102
103 $relatedBoards = $relatedBoardsQuery->orderBy($order, $orderBy)
104 ->withCount('completedTasks')
105 ->with('stages', 'users')
106 ->paginate($per_page);
107
108 foreach ($relatedBoards as $relatedBoard) {
109 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
110 $relatedBoard->is_pinned = $this->boardService->isPinned($relatedBoard->id);
111 }
112
113 $response = [
114 'boards' => $relatedBoards
115 ];
116
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'] = [];
126 }
127
128 return $this->sendSuccess($response);
129 }
130
131 /**
132 * Get folder mapping for boards
133 */
134 private function getBoardFolderMapping($userId)
135 {
136 if (!defined('FLUENT_BOARDS_PRO')) {
137 return [];
138 }
139
140 $folderService = new \FluentBoardsPro\App\Services\FolderService();
141 $folders = $folderService->getFolders($userId);
142
143 $mapping = [];
144 foreach ($folders as $folder) {
145 $mapping[$folder->id] = [
146 'id' => $folder->id,
147 'title' => $folder->title,
148 'board_ids' => $folder->boards ? $folder->boards->pluck('id')->toArray() : []
149 ];
150 }
151
152 return $mapping;
153 }
154
155 /**
156 * Get current folder information
157 */
158 private function getCurrentFolderInfo($folderId)
159 {
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 return [
172 'id' => $folder->id,
173 'title' => $folder->title,
174 'board_count' => $folder->boards ? $folder->boards->count() : 0
175 ];
176 }
177
178 /**
179 * Get the list of boards and their associated stages for the current user.
180 *
181 * @param \FluentBoards\Framework\Http\Request\Request $request
182 * @return \WP_REST_Response
183 */
184 public function getBoardsList(Request $request)
185 {
186 $userId = get_current_user_id();
187
188 // Query to fetch boards that are not archived and accessible by the user
189 // Check if the FLUENT_ROADMAP constant is defined
190 if (!defined('FLUENT_ROADMAP')) {
191 $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
192 } else {
193 $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
194 }
195
196 $relatedBoards = $relatedBoardsQuery->with('stages')->get();
197
198 // Fetch the stages associated with the boards
199 $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();
200
201 return $this->sendSuccess([
202 'boards' => $relatedBoards,
203 'all_stages' => $stages,
204 ], 200);
205 }
206 public function getOnlyBoardsByUser(Request $request)
207 {
208 try {
209 $userId = get_current_user_id();
210
211 $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
212
213
214 if(!defined('FLUENT_ROADMAP'))
215 {
216 $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
217 } else {
218 $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
219 }
220
221 if (!empty($searchInput)) {
222 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
223 }
224
225 $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();
226
227 return $this->sendSuccess([
228 'boards' => $relatedBoards
229 ]);
230 } catch (\Exception $e) {
231 return $this->sendError([
232 'message' => $e->getMessage()
233 ]);
234 }
235 }
236
237 public function getRecentBoards()
238 {
239 $boards = $this->boardService->getRecentBoards();
240
241 if (!$boards || $boards->isEmpty()) {
242 $boards = Board::where('type', 'to-do')->byAccessUser(get_current_user_id())
243 ->limit(4)
244 ->withCount('completedTasks')
245 ->with(['stages', 'users'])
246 ->get();
247 }
248
249 foreach ($boards as $board) {
250 $board->users = Helper::sanitizeUserCollections($board->users);
251 $board->is_pinned = $this->boardService->isPinned($board->id);
252 }
253
254 return [
255 'boards' => $boards,
256 ];
257 }
258
259 /*
260 * TODO: Refactor this method , remove this
261 */
262 public function getBoardsByType($type)
263 {
264 $boards = $this->boardService->getBoardsByType($type);
265
266 return $this->sendSuccess([
267 'boards' => $boards,
268 ]);
269 }
270
271 public function createFirstBoard(Request $request)
272 {
273 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
274 'title' => 'required|string',
275 'description' => 'nullable',
276 'type' => 'required|string',
277 'currency' => 'nullable|string',
278 'crm_contact_id' => 'nullable|numeric',
279 ]);
280
281 $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
282
283 $postStages = $request->get('stages');
284 if (!is_array($postStages)) {
285 $postStages = [];
286 }
287 $stageData = array();
288 foreach ($postStages as $stage) {
289 $temp = $this->stageSanitizeAndValidate($stage, [
290 'title' => 'required|string',
291 ]);
292 $stageData[] = $temp;
293 }
294
295 $taskData = null;
296 if ($request->get('task')) {
297 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
298 'title' => 'required|string',
299 ]);
300 }
301
302 $board = $this->boardService->createBoard($boardData);
303 $this->labelService->createDefaultLabel($board->id);
304 $type = ucfirst($boardData['type']);
305 $stage = $this->stageService->createStages($board, $stageData);
306
307 if ($taskData) {
308 $taskData['board_id'] = $board->id;
309 $taskData['stage_id'] = $stage->id;
310 $this->taskService->createTask($taskData, $board->id);
311 }
312
313 do_action('fluent_boards/board_created', $board);
314
315 if ($installFluentCRM && !defined('FLUENTCRM')) {
316 InstallService::install('fluent-crm');
317 }
318
319 return [
320 'message' => __('Board has been created', 'fluent-boards'),
321 'board' => $board,
322 ];
323 }
324
325 public function skipOnboarding(Request $request)
326 {
327 $onboarding = Meta::where('key', Constant::FBS_ONBOARDING)->first();
328 if($onboarding && $onboarding->value == 'no'){
329 $onboarding->value = 'yes' ;
330 $onboarding->save();
331 }
332
333 return [
334 'message' => __('Onboarding skipped successfully', 'fluent-boards'),
335 ];
336 }
337
338 public function create(Request $request)
339 {
340 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
341 'title' => 'required|string',
342 'description' => 'nullable',
343 'type' => 'required|string',
344 'currency' => 'nullable|string',
345 'crm_contact_id' => 'nullable|numeric',
346 'folder_id' => 'nullable',
347 ]);
348
349 try {
350 $board = $this->boardService->createBoard($boardData);
351 $this->labelService->createDefaultLabel($board->id);
352 $type = ucfirst($boardData['type']);
353
354 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 $this->stageService->createRoadmapStages($board, $sanitizedStages);
367 } else {
368 $this->stageService->createDefaultStages($board);
369 }
370
371 // if board is created from crm contact
372 if (isset($boardData['crm_contact_id'])) {
373 $this->boardService->updateAssociateMember($boardData['crm_contact_id'], $board->id);
374 }
375
376 do_action('fluent_boards/board_created', $board);
377
378
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 }
384 }
385
386 $message = __('Board has been created successfully', 'fluent-boards');
387
388 return $this->send([
389 'message' => $message,
390 'board' => $board,
391 ], 201);
392 } catch (\Exception $e) {
393 return $this->sendError([
394 'message' => $e->getMessage()
395 ]);
396 }
397 }
398
399 public function getArchivedStage(Request $request, $board_id)
400 {
401 try {
402 $pagination = $request->getSafe('noPagination', 'boolval', false);
403 $per_page = $request->getSafe('per_page', 'intval', 30);
404 $page = $request->getSafe('page', 'intval', 1);
405
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 }
417
418 return $this->sendSuccess([
419 'stages' => $stages,
420 ], 200);
421 } catch (\Exception $e) {
422 return $this->sendError($e->getMessage(), 404);
423 }
424 }
425
426 public function find(Request $request, $board_id)
427 {
428 $board = Board::findOrFail($board_id);
429 $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
430 $board->background = maybe_unserialize($board->background);
431 $board->createdOn = $board->created_at->format('Y-m-d');
432
433 $board->load(['users', 'labels', 'owner']);
434
435 if ($includeArchived) {
436 $board->stages = Stage::where('board_id', $board_id)
437 ->orderBy('position', 'asc')
438 ->get();
439 } else {
440 $board->load('stages');
441 }
442
443 if (defined('FLUENT_BOARDS_PRO')){
444 $customFiledPositionMeta = $board->getMetaByKey('custom_field_positions');
445 if(!$customFiledPositionMeta) {
446 (new CustomFieldService())->reIndexCustomFieldPositions($board_id);
447 $board->updateMeta('custom_field_positions', 'yes');
448 }
449
450 $board->load(['customFields']);
451 }
452
453 $this->boardService->updateRecentBoards($board_id);
454
455 $board->labelColor = Constant::TRELLO_COLOR_MAP;
456 $board->labelColorText = Constant::TEXT_COLOR_MAP;
457
458 $board->users = Helper::sanitizeUserCollections($board->users);
459 $board->owner = Helper::sanitizeUserCollections($board->owner);
460
461 $board->is_pinned = $this->boardService->isPinned($board->id);
462
463 $board = apply_filters('fluent_boards/board_find', $board);
464
465 return [
466 'board' => $board,
467 'synced_at' => current_time('mysql')
468 ];
469 }
470
471 public function update(Request $request, $board_id)
472 {
473 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
474 'title' => 'required|string',
475 'description' => 'nullable|string',
476 ]);
477
478 $board = Board::findOrFail($board_id);
479
480 $oldBoard = clone $board;
481 $board->fill($boardData);
482 $board->save();
483
484 do_action('fluent_boards/board_updated', $board, $oldBoard);
485
486 return [
487 'message' => __('Board has been updated', 'fluent-boards'),
488 'board' => $board,
489 'stages' => $board->stages()->get(),
490 ];
491 }
492
493 public function archiveStage($board_id, $stage_id)
494 {
495 $board_id = absint($board_id);
496 $stage_id = absint($stage_id);
497
498 try {
499 $stage = $this->findStageOnBoard($stage_id, $board_id);
500
501 $updatedStage = $this->boardService->archiveStage($board_id, $stage);
502
503 return $this->sendSuccess([
504 'updatedStage' => $updatedStage,
505 'message' => __('Stage has been archived', 'fluent-boards'),
506 ], 200);
507 } catch (\Exception $e) {
508 return $this->sendError($e->getMessage(), 400);
509 }
510 }
511
512 public function restoreStage($board_id, $stage_id)
513 {
514 $board_id = absint($board_id);
515 $stage_id = absint($stage_id);
516
517 try {
518 $stage = $this->findStageOnBoard($stage_id, $board_id);
519
520 $updatedStage = $this->boardService->restoreStage($board_id, $stage);
521
522 return $this->sendSuccess([
523 'success' => true,
524 'updatedStage' => $updatedStage,
525 'message' => __('Stage has been restored', 'fluent-boards')
526 ], 200);
527 } catch (\Exception $e) {
528 return $this->sendError($e->getMessage(), 400);
529 }
530 }
531
532
533 public function repositionStages(Request $request, $board_id)
534 {
535 $incomingList = $request->get('list');
536 if (!is_array($incomingList)) {
537 $incomingList = [];
538 }
539 $incomingList = array_map('intval', $incomingList);
540 try {
541 foreach ($incomingList as $stageId) {
542 $this->findStageOnBoard($stageId, $board_id);
543 }
544
545 $this->boardService->repositionStages($board_id, $incomingList);
546 return $this->sendSuccess([
547 'message' => __('Stages Reordered', 'fluent-boards'),
548 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
549 ], 200);
550 } catch (\Exception $e) {
551 return $this->sendError($e->getMessage(), 400);
552 }
553 }
554
555 public function getAssigneesByBoard($board_id)
556 {
557 return $this->sendSuccess([
558 'data' => $this->boardService->getAssigneesByBoard($board_id),
559 ], 200);
560 }
561
562 public function delete($board_id)
563 {
564 try {
565 if (!PermissionManager::isAdmin()) {
566 throw new \Exception(esc_html__('You do not have permission to delete this board', 'fluent-boards'), 400);
567 }
568 $this->boardService->deleteBoard($board_id);
569
570 return $this->sendSuccess([
571 'message' => __('Board has been deleted', 'fluent-boards'),
572 ], 200);
573 } catch (\Exception $e) {
574 return $this->sendError($e->getMessage(), 400);
575 }
576 }
577
578 public function getCurrencies()
579 {
580 return BoardHandler::getCurrencies();
581 }
582
583 public function getActivities(Request $request, $board_id)
584 {
585 try {
586 $activities = $this->boardService->getActivities($board_id, [
587 'per_page' => $request->getSafe('per_page', 'intval', 40),
588 'page' => $request->getSafe('page', 'intval', 1),
589 ]);
590 return $this->sendSuccess([
591 'activities' => $activities,
592 ], 200);
593 } catch (\Exception $e) {
594 return $this->sendError($e->getMessage(), 404);
595 }
596 }
597
598 /*
599 * TODO: Refactor this method - for Masiur
600 */
601 public function getBoardUsers($board_id)
602 {
603 $board = Board::findOrFail($board_id);
604
605 $boardObjects = Relation::where('object_type', 'board_user')
606 ->where('object_id', $board_id)
607 ->get()->keyBy('foreign_id');
608
609 $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
610 ->get()->pluck('object_id')->toArray();
611
612 $userIds = $boardObjects->pluck('foreign_id')->toArray();
613
614 $coreUsers = [];
615 if ($userIds) {
616 // Get the users who are in the board (members and managers
617 $coreUsers = get_users([
618 'include' => $userIds
619 ]);
620 }
621
622 $formattedUsers = [];
623
624 foreach ($coreUsers as $user) {
625 $name = trim($user->first_name . ' ' . $user->last_name);
626 if (!$name) {
627 $name = $user->display_name;
628 }
629
630 $boardRelation = $boardObjects[$user->ID] ?? null;
631
632
633 $formattedUsers[] = [
634 'ID' => $user->ID,
635 'display_name' => $name,
636 'user_login' => $user->user_login,
637 'email' => $user->user_email,
638 'photo' => fluent_boards_user_avatar($user->user_email, $name),
639 'role' => $this->boardUserRole($boardRelation),
640 'is_super' => in_array($user->ID, $superAdminIds),
641 'is_wpadmin' => $user->has_cap('manage_options')
642 ];
643 }
644
645 // order formatted users by display_name
646 usort($formattedUsers, function ($a, $b) {
647 return strcmp($a['display_name'], $b['display_name']);
648 });
649
650 $returnData = [
651 'users' => Helper::sanitizeUsersArray($formattedUsers, $board_id),
652 'global_admins' => []
653 ];
654
655 if (!PermissionManager::isAdmin(get_current_user_id())) {
656 return $returnData;
657 }
658
659 /*
660 * These are the rest of the admin users who are not in the board
661 */
662 $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
663 ->whereNotIn('object_id', $userIds)
664 ->get()
665 ->pluck('object_id')
666 ->toArray();
667
668 if ($adminUserIds) {
669 $adminUsers = get_users([
670 'include' => $adminUserIds,
671 ]);
672
673 $formattedAdminUsers = [];
674
675 foreach ($adminUsers as $user) {
676 $name = trim($user->first_name . ' ' . $user->last_name);
677 if (!$name) {
678 $name = $user->display_name;
679 }
680
681 $formattedAdminUsers[] = [
682 'ID' => $user->ID,
683 'display_name' => $name,
684 'email' => $user->user_email,
685 'photo' => fluent_boards_user_avatar($user->user_email, $name),
686 'role' => 'admin',
687 'is_super' => in_array($user->ID, $superAdminIds),
688 'is_wpadmin' => $user->has_cap('manage_options')
689 ];
690 }
691
692 // order formatted users by display_name
693 usort($formattedAdminUsers, function ($a, $b) {
694 return strcmp($a['display_name'], $b['display_name']);
695 });
696
697 $returnData['global_admins'] = Helper::sanitizeUsersArray($formattedAdminUsers, $board_id);
698 }
699
700 return $this->sendSuccess($returnData, 200);
701 }
702
703
704 public function removeUserFromBoard($board_id, $userId)
705 {
706 $this->boardService->removeUserFromBoard($board_id, $userId);
707
708 if (!PermissionManager::isAdmin($userId)) {
709 $this->boardService->removeFromRecentlyOpened($board_id, $userId);
710 }
711
712 return [
713 'message' => __('Member removed successfully', 'fluent-boards'),
714 ];
715 }
716
717 public function addMembersInBoard(Request $request, $board_id)
718 {
719 $memberId = $request->getSafe('memberId');
720 $isViewerOnly = $request->getSafe('isViewerOnly');
721 $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
722 if (!$member) {
723 return $this->sendError([
724 'message' => __('User already a member', 'fluent-boards'),
725 ], 304);
726 }
727
728
729 return [
730 'message' => __('Member added successfully', 'fluent-boards'),
731 'member' => Helper::sanitizeUserCollections($member)
732 ];
733 }
734
735 private function boardSanitizeAndValidate($data, array $rules = [])
736 {
737 $data = Helper::sanitizeBoard($data);
738
739 return $this->validate($data, $rules);
740 }
741
742 private function stageSanitizeAndValidate($data, array $rules = [])
743 {
744 $data = Helper::sanitizeStage($data);
745
746 return $this->validate($data, $rules);
747 }
748
749 private function taskSanitizeAndValidate($data, array $rules = [])
750 {
751 $data = Helper::sanitizeTask($data);
752
753 return $this->validate($data, $rules);
754 }
755
756 public function searchBoards(Request $request)
757 {
758 $per_page = $request->getSafe('per_page', 'intval', 10);
759 $search_input = $request->getSafe('searchInput', 'sanitize_text_field', '');
760 $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
761
762 $currentUserId = get_current_user_id();
763
764 if (PermissionManager::isAdmin($currentUserId)) {
765 $boards = Board::query()->where('type', $type)
766 ->where('title', 'like', '%' . $search_input . '%')
767 ->with('stages', 'tasks', 'users')
768 ->paginate($per_page);
769
770 foreach ($boards as $board) {
771 $board->users = Helper::sanitizeUserCollections($board->users);
772 }
773
774 } else {
775 $currentUser = User::find($currentUserId);
776 $boards = $currentUser->boards()->where('type', $type)->where('title', 'like', '%' . $search_input . '%')->paginate($per_page);
777 }
778
779 return [
780 'boards' => $boards,
781 ];
782 }
783
784 public function getUsersOfBoards()
785 {
786 $userBoards = $this->boardService->getUsersOfBoards();
787
788 return $this->sendSuccess([
789 'userBoards' => $userBoards,
790 ], 200);
791 }
792
793
794
795 /**
796 * Refactor this code form me - Masiur
797 * change stage settings is_public for roadmap user and admin view
798 * @param $board_id
799 * @param $stage_id
800 * @return
801 */
802 public function changeStageView($board_id, $stage_id)
803 {
804 $board_id = absint($board_id);
805 $stage_id = absint($stage_id);
806
807 try {
808 $stage = $this->findStageOnBoard($stage_id, $board_id);
809 $message = __('The stage is made public!', 'fluent-boards');
810 $settings = $stage->settings;
811
812 if (isset($settings['is_public'])) {
813 if ($settings['is_public']) {
814 $settings['is_public'] = false;
815 $message = __('The stage is made admin only!', 'fluent-boards');
816 } else {
817 $settings['is_public'] = true;
818 }
819 } else {
820 $settings['is_public'] = true;
821 }
822
823 $stage->settings = $settings;
824 $stage->save();
825 return $this->sendSuccess([
826 'message' => $message,
827 'stage' => $stage
828 ]);
829 } catch (\Exception $e) {
830 return $this->sendError($e->getMessage(), 400);
831 }
832 }
833
834
835 /**
836 * Set board background image or color
837 * @param \FluentBoards\Framework\Http\Request\Request $request
838 * @return
839 */
840 public function setBoardBackground(Request $request, $board_id)
841 {
842 // sanitize and validate image_url
843 if ($request->image_url) {
844 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
845 "id" => 'required',
846 'image_url' => 'required|string|url',
847 ]);
848 }
849
850 // sanitize and validate color
851 if ($request->color) {
852 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
853 "id" => 'required',
854 'color' => 'required',
855 ]);
856 }
857
858 try {
859 if (!$board_id) {
860 $errorMessage = __('Board id is required', 'fluent-boards');
861 throw new \Exception(esc_html($errorMessage), 400);
862 }
863
864 return $this->sendSuccess([
865 'message' => __('Background updated successfully', 'fluent-boards'),
866 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
867 ]);
868 } catch (\Exception $e) {
869 return $this->sendError($e->getMessage(), 400);
870 }
871 }
872
873
874 /**
875 * Summary of getStageTaskAvailablePositions
876 * @param mixed $board_id
877 * @param mixed $stage_slug
878 * @return $availablePositions as an array
879 * @throws \Exception
880 */
881 public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
882 {
883 try {
884 if ($board_id && $stage_id) {
885 $taskId = $request->getSafe('task_id', 'intval');
886 $availablePositions = $this->boardService->getStageTaskAvailablePositions($board_id, $stage_id, $taskId);
887 return $this->sendSuccess([
888 'availablePositions' => $availablePositions['availablePositions'],
889 'moveTargets' => $availablePositions['moveTargets'],
890 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
891 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
892 ], 200);
893 } else {
894 $message = '';
895 if (!$board_id) {
896 $message = 'Board id ';
897 }
898 if (!$stage_id) {
899 $message = 'Stage ';
900 }
901 throw new \Exception(esc_html($message . 'is required'), 400);
902 }
903 } catch (\Exception $e) {
904 return $this->sendError($e->getMessage(), 400);
905 }
906 }
907
908 public function getAssociateCrmContacts($board_id)
909 {
910 try {
911 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
912 ->whereNotNull('crm_contact_id')
913 ->get();
914
915 $formattedContacts = Collection::make($contactAssociatedTasks)
916 ->groupBy('crm_contact_id')
917 ->map(function ($tasks, $contactId) {
918 $subscriber = Subscriber::find($contactId);
919 if (!$subscriber) {
920 return null; // Skip if subscriber not found
921 }
922
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 ];
931 })
932 ->filter()->toArray();
933
934
935 return $this->sendSuccess([
936 'associatedContacts' => $formattedContacts
937 ], 200);
938 } catch (\Exception $e) {
939 return $this->sendError($e->getMessage(), 404);
940 }
941 }
942
943 public function updateAssociateCrmContact(Request $request, $board_id)
944 {
945 $value = $request->getSafe('value');
946 $this->boardService->updateAssociateMember($value, $board_id);
947
948 return $this->sendSuccess([
949 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
950 ], 200);
951 }
952
953 public function hasDataChanged(Request $request, $board_id)
954 {
955 $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
956 $since = $request->getSafe('since', 'sanitize_text_field');
957 return $this->boardService->hasDataChanged($board_id, $includeArchived, $since);
958 }
959
960 public function createStage(Request $request, $board_id)
961 {
962 $stageData = $this->stageSanitizeAndValidate($request->all(), [
963 'title' => 'required|string',
964 'position' => 'nullable|numeric'
965 ]);
966
967 $board = Board::find($board_id);
968 $stage = $this->stageService->createStage($stageData, $board_id);
969
970 do_action('fluent_boards/board_stage_added', $board, $stage);
971
972 $updatedStates = (new StageService())->getLastOneMinuteUpdatedStages($board_id);
973
974 return [
975 'updatedStages' => $updatedStates,
976 'message' => __('stage has been created', 'fluent-boards'),
977 ];
978 }
979
980 public function moveAllTasks(Request $request, $board_id)
981 {
982 $oldStageId = $request->getSafe('oldStageId', 'intval');
983 $newStageId = $request->getSafe('newStageId', 'intval');
984
985 if (!$oldStageId || !$newStageId) {
986 return $this->sendError(__('Invalid stage IDs provided', 'fluent-boards'), 400);
987 }
988
989 // Verify stages exist and belong to the board
990 $oldStage = Stage::where('id', $oldStageId)->where('board_id', $board_id)->first();
991 $newStage = Stage::where('id', $newStageId)->where('board_id', $board_id)->first();
992
993 if (!$oldStage || !$newStage) {
994 return $this->sendError(__('One or both stages do not exist or do not belong to this board', 'fluent-boards'), 400);
995 }
996
997 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
998
999 return [
1000 'message' => __('Tasks have been moved', 'fluent-boards'),
1001 'updatedTasks' => $updates,
1002 ];
1003
1004 }
1005
1006 public function archiveAllTasksInStage($board_id, $stage_id)
1007 {
1008 $updates = $this->stageService->archiveAllTasksInStage($stage_id);
1009 return [
1010 'message' => __('Tasks have been archived', 'fluent-boards'),
1011 'updatedTasks' => $updates,
1012 ];
1013 }
1014
1015 public function getAssociatedBoards(Request $request, $associated_id)
1016 {
1017 $associatedBoards = $this->boardService->getAssociatedBoards($associated_id);
1018 return [
1019 'boards' => $associatedBoards,
1020 ];
1021 }
1022
1023 public function duplicateBoard(Request $request, $board_id)
1024 {
1025 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
1026 'title' => 'required|string'
1027 ]);
1028
1029 $boardData['source_board_id'] = $board_id;
1030
1031 $isWithLabels = $request->getSafe('isWithLabels');
1032 $isWithTasks = $request->getSafe('isWithTasks');
1033 $isWithTemplates = $request->getSafe('isWithTemplates');
1034
1035 try {
1036 if(!PermissionManager::isAdmin()) {
1037 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
1038 throw new \Exception(esc_html($errorMessage), 400);
1039 }
1040 //create board
1041 $newBoard = $this->boardService->copyBoard($boardData);
1042
1043 //label copy
1044 $labelMap = [];
1045
1046 if ($isWithLabels == 'yes') {
1047 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
1048 }
1049
1050 //stage copy
1051 $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
1052
1053 //copy tasks of selected stages
1054 if ($isWithTasks == 'yes') {
1055 $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
1056 }
1057
1058 return $this->sendSuccess([
1059 'board' => $newBoard,
1060 ], 200);
1061 } catch (\Exception $e) {
1062 return $this->sendError($e->getMessage(), 400);
1063 }
1064 }
1065
1066 public function importFromBoard(Request $request, $board_id)
1067 {
1068 $selectedStages = $request->getSafe('selectedStages');
1069 $position = $request->getSafe('position', 'intval');
1070
1071 // Validate and sanitize selectedStages array
1072 if (!is_array($selectedStages)) {
1073 $selectedStages = [$selectedStages];
1074 }
1075 $selectedStages = array_filter(array_map('intval', $selectedStages));
1076
1077 try {
1078 $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
1079
1080 return $this->sendSuccess([
1081 'message' => __('Import successfully', 'fluent-boards'),
1082 ], 200);
1083
1084 } catch (\Exception $e) {
1085 return $this->sendError($e->getMessage(), 400);
1086 }
1087 }
1088
1089 public function getBoardDefaultBackgroundColors()
1090 {
1091 return [
1092 'solidColors' => Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
1093 'gradients' => Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
1094 ];
1095 }
1096
1097 /*
1098 * TODO: For Masiur - I will update this later
1099 */
1100 public function updateBoardProperties(Request $request, $board_id)
1101 {
1102 $pageId = $request->getSafe('page_id');
1103 $enable_stage_change_email = $request->getSafe('enable_stage_change_email');
1104
1105 $board = Board::findOrFail($board_id);
1106
1107 $board->updateMeta('roadmap_page_id', $pageId);
1108 $board->updateMeta('enable_stage_change_email', $enable_stage_change_email);
1109
1110 $board = $board->fresh();
1111
1112 return [
1113 'message' => __('Board has been updated', 'fluent-boards'),
1114 'board' => apply_filters('fluent_boards/board_find', $board)
1115 ];
1116 }
1117
1118 public function archiveBoard($board_id)
1119 {
1120 try {
1121 $board = $this->boardService->archiveBoard($board_id);
1122
1123 return [
1124 'board' => $board,
1125 'message' => __('Board has been archived successfully!', 'fluent-boards')
1126 ];
1127 } catch (\Exception $e) {
1128 return $this->sendError($e->getMessage(), 400);
1129 }
1130 }
1131
1132 public function restoreBoard($board_id)
1133 {
1134 try {
1135 $board = $this->boardService->restoreBoard($board_id);
1136
1137 return [
1138 'board' => $board,
1139 'message' => __('Board has been restored successfully!', 'fluent-boards')
1140 ];
1141 } catch (\Exception $e) {
1142 return $this->sendError($e->getMessage(), 400);
1143 }
1144 }
1145
1146 private function boardUserRole($boardRelation)
1147 {
1148 return $boardRelation && Arr::get($boardRelation->settings, 'is_admin')
1149 ? 'manager'
1150 : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
1151 ? 'viewer'
1152 : 'member');
1153 }
1154 public function uploadBoardBackground(Request $request,$board_id)
1155 {
1156 $file = Arr::get($request->files(), 'file')->toArray();
1157 (new \FluentBoards\App\Services\UploadService)->validateFile($file);
1158
1159 $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
1160
1161 $fileData = $uploadInfo[0];
1162 $initialDataData = [
1163 'type' => 'url',
1164 'url' => '',
1165 'name' => '',
1166 'size' => 0,
1167 ];
1168
1169 $attachData = array_merge($initialDataData, $fileData);
1170 $UrlMeta = [];
1171 if($attachData['type'] == 'url') {
1172 $UrlMeta = RemoteUrlParser::parse($attachData['url']);
1173 }
1174 $uid = wp_generate_uuid4();
1175 $fileUploadedData = new Attachment();
1176 $fileUploadedData->object_id = $board_id;
1177 $fileUploadedData->object_type = Constant::BOARD_BACKGROUND_IMAGE;
1178 $fileUploadedData->attachment_type = $attachData['type'];
1179 $fileUploadedData->title = (new TaskService())->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
1180 $fileUploadedData->file_path = $attachData['type'] != 'url' ? $attachData['file'] : null;
1181 $fileUploadedData->full_url = esc_url($attachData['url']);
1182 $fileUploadedData->file_size = $attachData['size'];
1183 $fileUploadedData->settings = $attachData['type'] == 'url' ? [
1184 'meta' => $UrlMeta
1185 ] : '';
1186 $fileUploadedData->driver = 'local';
1187 $fileUploadedData->file_hash = md5($uid . wp_rand(0, 1000));
1188 $fileUploadedData->save();
1189 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
1190 $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
1191 $fileUploadedData['driver'] = $mediaData['driver'];
1192 $fileUploadedData['file_path'] = $mediaData['file_path'];
1193 $fileUploadedData['full_url'] = $mediaData['full_url'];
1194 $fileUploadedData->save();
1195 }
1196
1197 $board = Board::find($board_id);
1198 $oldBackground = $board->background;
1199 $publicUrl = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
1200 $background = [
1201 'color' => null,
1202 'id' => $fileUploadedData->id,
1203 'image_url' => $publicUrl,
1204 'is_image' => true,
1205 ];
1206 $board->background = $background;
1207 $board->save();
1208 do_action('fluent_boards/board_background_updated', $board_id, $oldBackground);
1209
1210 return $this->sendSuccess([
1211 'message' => __('Background updated successfully', 'fluent-boards'),
1212 'background' => $board->background,
1213 ]);
1214 }
1215
1216 public function getPinnedBoards()
1217 {
1218 $pinnedBoards = $this->boardService->getPinnedBoards();
1219
1220 return $this->sendSuccess([
1221 'pinnedBoards' => $pinnedBoards,
1222 ], 200);
1223 }
1224
1225 public function pinBoard($boardId)
1226 {
1227 $this->boardService->pinBoard($boardId);
1228
1229 return $this->sendSuccess([
1230 'message' => __('The Board has been pinned', 'fluent-boards'),
1231 ], 200);
1232 }
1233
1234 public function unpinBoard($boardId)
1235 {
1236 $remove = $this->boardService->unpinBoard($boardId);
1237
1238 if (!$remove) {
1239 return $this->sendError([
1240 'message' => __('Board is not pinned', 'fluent-boards'),
1241 ], 400);
1242 }
1243
1244 return $this->sendSuccess([
1245 'message' => __('Board is removed from pinned boards', 'fluent-boards'),
1246 ], 200);
1247 }
1248
1249 public function getBoardFolder($board_id)
1250 {
1251 try {
1252 $folder = $this->boardService->getBoardFolder($board_id);
1253 return $this->sendSuccess([
1254 'folder' => $folder,
1255 ], 200);
1256 } catch (\Exception $e) {
1257 return $this->sendError($e->getMessage(), 400);
1258 }
1259 }
1260
1261 public function getBoardMenuItems($board_id)
1262 {
1263 try {
1264 $menuItems = (new BoardMenuHandler())->getMenuItems($board_id);
1265
1266 return $this->sendSuccess([
1267 'menu_items' => $menuItems
1268 ], 200);
1269 } catch (\Exception $e) {
1270 return $this->sendError($e->getMessage(), 500);
1271 }
1272 }
1273
1274 public function getPublicAccessSettings($board_id)
1275 {
1276 $board_id = absint($board_id);
1277 $board = Board::findOrFail($board_id);
1278
1279 $enabled = (bool) $board->getMetaByKey('public_access_enabled');
1280 $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1281
1282 return $this->sendSuccess([
1283 'enabled' => $enabled,
1284 'shortcode' => $shortcode,
1285 ], 200);
1286 }
1287
1288 public function togglePublicAccess(Request $request, $board_id)
1289 {
1290 $board_id = absint($board_id);
1291 $board = Board::findOrFail($board_id);
1292
1293 $enabled = filter_var(
1294 $request->getSafe('enabled', 'sanitize_text_field', false),
1295 FILTER_VALIDATE_BOOLEAN
1296 );
1297
1298 $board->updateMeta('public_access_enabled', $enabled ? '1' : '');
1299
1300 $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1301
1302 return $this->sendSuccess([
1303 'message' => $enabled
1304 ? __('Public access has been enabled', 'fluent-boards')
1305 : __('Public access has been disabled', 'fluent-boards'),
1306 'enabled' => $enabled,
1307 'shortcode' => $shortcode,
1308 ], 200);
1309 }
1310
1311 /**
1312 * Resolve a stage only when it belongs to the requested board.
1313 *
1314 * @param int $stageId
1315 * @param int $boardId
1316 * @return Stage
1317 * @throws \Exception
1318 */
1319 private function findStageOnBoard($stageId, $boardId)
1320 {
1321 $stage = Stage::where('id', absint($stageId))
1322 ->where('board_id', absint($boardId))
1323 ->first();
1324
1325 if (!$stage) {
1326 throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
1327 }
1328
1329 return $stage;
1330 }
1331
1332 }
1333