PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.95.3
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.95.3
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.3, at app/Http/Controllers/BoardController.php

1,357 lines 45.7 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 $board_id = absint($board_id);
1009 $stage_id = absint($stage_id);
1010
1011 try {
1012 $this->findStageOnBoard($stage_id, $board_id);
1013 $updates = $this->stageService->archiveAllTasksInStage($stage_id, $board_id);
1014
1015 return [
1016 'message' => __('Tasks have been archived', 'fluent-boards'),
1017 'updatedTasks' => $updates,
1018 ];
1019 } catch (\Exception $e) {
1020 return $this->sendError($e->getMessage(), 400);
1021 }
1022 }
1023
1024 public function getAssociatedBoards(Request $request, $associated_id)
1025 {
1026 if (!$this->currentUserCanReadCrmContacts()) {
1027 return $this->sendError(esc_html__('You do not have permission to view CRM contact boards', 'fluent-boards'), 403);
1028 }
1029
1030 $associatedBoards = $this->boardService->getAssociatedBoards($associated_id, get_current_user_id());
1031 return [
1032 'boards' => $associatedBoards,
1033 ];
1034 }
1035
1036 private function currentUserCanReadCrmContacts()
1037 {
1038 $permissionManager = 'FluentCrm\\App\\Services\\PermissionManager';
1039
1040 if (!class_exists($permissionManager)) {
1041 return false;
1042 }
1043
1044 return (bool) $permissionManager::currentUserCan('fcrm_read_contacts');
1045 }
1046
1047 public function duplicateBoard(Request $request, $board_id)
1048 {
1049 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
1050 'title' => 'required|string'
1051 ]);
1052
1053 $boardData['source_board_id'] = $board_id;
1054
1055 $isWithLabels = $request->getSafe('isWithLabels');
1056 $isWithTasks = $request->getSafe('isWithTasks');
1057 $isWithTemplates = $request->getSafe('isWithTemplates');
1058
1059 try {
1060 if(!PermissionManager::isAdmin()) {
1061 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
1062 throw new \Exception(esc_html($errorMessage), 400);
1063 }
1064 //create board
1065 $newBoard = $this->boardService->copyBoard($boardData);
1066
1067 //label copy
1068 $labelMap = [];
1069
1070 if ($isWithLabels == 'yes') {
1071 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
1072 }
1073
1074 //stage copy
1075 $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
1076
1077 //copy tasks of selected stages
1078 if ($isWithTasks == 'yes') {
1079 $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
1080 }
1081
1082 return $this->sendSuccess([
1083 'board' => $newBoard,
1084 ], 200);
1085 } catch (\Exception $e) {
1086 return $this->sendError($e->getMessage(), 400);
1087 }
1088 }
1089
1090 public function importFromBoard(Request $request, $board_id)
1091 {
1092 $selectedStages = $request->getSafe('selectedStages');
1093 $position = $request->getSafe('position', 'intval');
1094
1095 // Validate and sanitize selectedStages array
1096 if (!is_array($selectedStages)) {
1097 $selectedStages = [$selectedStages];
1098 }
1099 $selectedStages = array_filter(array_map('intval', $selectedStages));
1100
1101 try {
1102 $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
1103
1104 return $this->sendSuccess([
1105 'message' => __('Import successfully', 'fluent-boards'),
1106 ], 200);
1107
1108 } catch (\Exception $e) {
1109 return $this->sendError($e->getMessage(), 400);
1110 }
1111 }
1112
1113 public function getBoardDefaultBackgroundColors()
1114 {
1115 return [
1116 'solidColors' => Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
1117 'gradients' => Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
1118 ];
1119 }
1120
1121 /*
1122 * TODO: For Masiur - I will update this later
1123 */
1124 public function updateBoardProperties(Request $request, $board_id)
1125 {
1126 $pageId = $request->getSafe('page_id');
1127 $enable_stage_change_email = $request->getSafe('enable_stage_change_email');
1128
1129 $board = Board::findOrFail($board_id);
1130
1131 $board->updateMeta('roadmap_page_id', $pageId);
1132 $board->updateMeta('enable_stage_change_email', $enable_stage_change_email);
1133
1134 $board = $board->fresh();
1135
1136 return [
1137 'message' => __('Board has been updated', 'fluent-boards'),
1138 'board' => apply_filters('fluent_boards/board_find', $board)
1139 ];
1140 }
1141
1142 public function archiveBoard($board_id)
1143 {
1144 try {
1145 $board = $this->boardService->archiveBoard($board_id);
1146
1147 return [
1148 'board' => $board,
1149 'message' => __('Board has been archived successfully!', 'fluent-boards')
1150 ];
1151 } catch (\Exception $e) {
1152 return $this->sendError($e->getMessage(), 400);
1153 }
1154 }
1155
1156 public function restoreBoard($board_id)
1157 {
1158 try {
1159 $board = $this->boardService->restoreBoard($board_id);
1160
1161 return [
1162 'board' => $board,
1163 'message' => __('Board has been restored successfully!', 'fluent-boards')
1164 ];
1165 } catch (\Exception $e) {
1166 return $this->sendError($e->getMessage(), 400);
1167 }
1168 }
1169
1170 private function boardUserRole($boardRelation)
1171 {
1172 return $boardRelation && Arr::get($boardRelation->settings, 'is_admin')
1173 ? 'manager'
1174 : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
1175 ? 'viewer'
1176 : 'member');
1177 }
1178 public function uploadBoardBackground(Request $request,$board_id)
1179 {
1180 $file = Arr::get($request->files(), 'file')->toArray();
1181 (new \FluentBoards\App\Services\UploadService)->validateFile($file);
1182
1183 $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
1184
1185 $fileData = $uploadInfo[0];
1186 $initialDataData = [
1187 'type' => 'url',
1188 'url' => '',
1189 'name' => '',
1190 'size' => 0,
1191 ];
1192
1193 $attachData = array_merge($initialDataData, $fileData);
1194 $UrlMeta = [];
1195 if($attachData['type'] == 'url') {
1196 $UrlMeta = RemoteUrlParser::parse($attachData['url']);
1197 }
1198 $uid = wp_generate_uuid4();
1199 $fileUploadedData = new Attachment();
1200 $fileUploadedData->object_id = $board_id;
1201 $fileUploadedData->object_type = Constant::BOARD_BACKGROUND_IMAGE;
1202 $fileUploadedData->attachment_type = $attachData['type'];
1203 $fileUploadedData->title = (new TaskService())->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
1204 $fileUploadedData->file_path = $attachData['type'] != 'url' ? $attachData['file'] : null;
1205 $fileUploadedData->full_url = esc_url($attachData['url']);
1206 $fileUploadedData->file_size = $attachData['size'];
1207 $fileUploadedData->settings = $attachData['type'] == 'url' ? [
1208 'meta' => $UrlMeta
1209 ] : '';
1210 $fileUploadedData->driver = 'local';
1211 $fileUploadedData->file_hash = md5($uid . wp_rand(0, 1000));
1212 $fileUploadedData->save();
1213 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
1214 $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
1215 $fileUploadedData['driver'] = $mediaData['driver'];
1216 $fileUploadedData['file_path'] = $mediaData['file_path'];
1217 $fileUploadedData['full_url'] = $mediaData['full_url'];
1218 $fileUploadedData->save();
1219 }
1220
1221 $board = Board::find($board_id);
1222 $oldBackground = $board->background;
1223 $publicUrl = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
1224 $background = [
1225 'color' => null,
1226 'id' => $fileUploadedData->id,
1227 'image_url' => $publicUrl,
1228 'is_image' => true,
1229 ];
1230 $board->background = $background;
1231 $board->save();
1232 do_action('fluent_boards/board_background_updated', $board_id, $oldBackground);
1233
1234 return $this->sendSuccess([
1235 'message' => __('Background updated successfully', 'fluent-boards'),
1236 'background' => $board->background,
1237 ]);
1238 }
1239
1240 public function getPinnedBoards()
1241 {
1242 $pinnedBoards = $this->boardService->getPinnedBoards();
1243
1244 return $this->sendSuccess([
1245 'pinnedBoards' => $pinnedBoards,
1246 ], 200);
1247 }
1248
1249 public function pinBoard($boardId)
1250 {
1251 $this->boardService->pinBoard($boardId);
1252
1253 return $this->sendSuccess([
1254 'message' => __('The Board has been pinned', 'fluent-boards'),
1255 ], 200);
1256 }
1257
1258 public function unpinBoard($boardId)
1259 {
1260 $remove = $this->boardService->unpinBoard($boardId);
1261
1262 if (!$remove) {
1263 return $this->sendError([
1264 'message' => __('Board is not pinned', 'fluent-boards'),
1265 ], 400);
1266 }
1267
1268 return $this->sendSuccess([
1269 'message' => __('Board is removed from pinned boards', 'fluent-boards'),
1270 ], 200);
1271 }
1272
1273 public function getBoardFolder($board_id)
1274 {
1275 try {
1276 $folder = $this->boardService->getBoardFolder($board_id);
1277 return $this->sendSuccess([
1278 'folder' => $folder,
1279 ], 200);
1280 } catch (\Exception $e) {
1281 return $this->sendError($e->getMessage(), 400);
1282 }
1283 }
1284
1285 public function getBoardMenuItems($board_id)
1286 {
1287 try {
1288 $menuItems = (new BoardMenuHandler())->getMenuItems($board_id);
1289
1290 return $this->sendSuccess([
1291 'menu_items' => $menuItems
1292 ], 200);
1293 } catch (\Exception $e) {
1294 return $this->sendError($e->getMessage(), 500);
1295 }
1296 }
1297
1298 public function getPublicAccessSettings($board_id)
1299 {
1300 $board_id = absint($board_id);
1301 $board = Board::findOrFail($board_id);
1302
1303 $enabled = (bool) $board->getMetaByKey('public_access_enabled');
1304 $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1305
1306 return $this->sendSuccess([
1307 'enabled' => $enabled,
1308 'shortcode' => $shortcode,
1309 ], 200);
1310 }
1311
1312 public function togglePublicAccess(Request $request, $board_id)
1313 {
1314 $board_id = absint($board_id);
1315 $board = Board::findOrFail($board_id);
1316
1317 $enabled = filter_var(
1318 $request->getSafe('enabled', 'sanitize_text_field', false),
1319 FILTER_VALIDATE_BOOLEAN
1320 );
1321
1322 $board->updateMeta('public_access_enabled', $enabled ? '1' : '');
1323
1324 $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1325
1326 return $this->sendSuccess([
1327 'message' => $enabled
1328 ? __('Public access has been enabled', 'fluent-boards')
1329 : __('Public access has been disabled', 'fluent-boards'),
1330 'enabled' => $enabled,
1331 'shortcode' => $shortcode,
1332 ], 200);
1333 }
1334
1335 /**
1336 * Resolve a stage only when it belongs to the requested board.
1337 *
1338 * @param int $stageId
1339 * @param int $boardId
1340 * @return Stage
1341 * @throws \Exception
1342 */
1343 private function findStageOnBoard($stageId, $boardId)
1344 {
1345 $stage = Stage::where('id', absint($stageId))
1346 ->where('board_id', absint($boardId))
1347 ->first();
1348
1349 if (!$stage) {
1350 throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
1351 }
1352
1353 return $stage;
1354 }
1355
1356 }
1357