# fluent-boards/1.95.3/app/Http/Controllers/BoardController.php

FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration, version 1.95.3. 1,357 lines.

- Page: https://pluginprobe.com/plugins/fluent-boards/1.95.3/code/app/Http/Controllers/BoardController.php
- Raw: https://pluginprobe.com/plugins/fluent-boards/1.95.3/raw/app/Http/Controllers/BoardController.php
- Modified: 2026-07-14T14:11:44+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/fluent-boards/1.95.3/code/app/Http/Controllers/BoardController.php#L10-L20`.

```php
<?php

namespace FluentBoards\App\Http\Controllers;

use FluentBoards\App\Models\Attachment;
use FluentBoards\App\Models\Meta;
use FluentBoards\App\Models\Relation;
use FluentBoards\App\Models\Task;
use FluentBoards\App\Models\User;
use FluentBoards\App\Models\Board;
use FluentBoards\App\Services\CommentService;
use FluentBoards\App\Services\Constant;
use FluentBoards\App\Services\Helper;
use FluentBoards\App\Models\Stage;
use FluentBoards\App\Services\InstallService;
use FluentBoards\App\Services\StageService;
use FluentBoards\App\Services\TaskService;
use FluentBoards\App\Services\BoardService;
use FluentBoards\App\Services\UploadService;
use FluentBoards\Framework\Http\Request\Request;
use FluentBoards\App\Services\PermissionManager;
use FluentBoards\App\Services\PublicAccessService;
use FluentBoards\App\Hooks\Handlers\BoardHandler;
use FluentBoards\App\Hooks\Handlers\BoardMenuHandler;
use FluentBoards\App\Services\LabelService;
use FluentBoards\Framework\Support\Arr;
use FluentBoards\Framework\Support\Collection;
use FluentBoardsPro\App\Services\AttachmentService;
use FluentBoardsPro\App\Services\CustomFieldService;
use FluentBoardsPro\App\Services\ProHelper;
use FluentBoardsPro\App\Services\RemoteUrlParser;
use FluentCrm\App\Models\Subscriber;

class BoardController extends Controller
{
    private $boardService;
    private $taskService;
    private $stageService;
    private $labelService;

    public function __construct(
        BoardService $boardService,
        TaskService  $taskService,
        StageService $stageService,
        LabelService $labelService
    )
    {
        parent::__construct();
        $this->boardService = $boardService;
        $this->taskService = $taskService;
        $this->stageService = $stageService;
        $this->labelService = $labelService;
    }

    public function getBoards(Request $request)
    {
        $per_page = $request->getSafe('per_page', 'intval', 100);
        $userId = get_current_user_id();
        $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');

        $order   = $request->getSafe('order', 'sanitize_text_field', 'created_at');
        $orderBy = $request->getSafe('orderBy', 'sanitize_text_field', 'DESC');
        $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');

        $option = $request->getSafe('option', 'sanitize_text_field');
        $folderId = $request->getSafe('fid', 'intval'); // Get folder ID from request

        // Initialize the query based on archive status
        if (!defined('FLUENT_ROADMAP')) {
            if ($option == 'archived') {
                $relatedBoardsQuery = Board::whereNotNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
            } else {
                $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
            }
        } else {
            if ($option == 'archived') {
                $relatedBoardsQuery = Board::whereNotNull('archived_at')->byAccessUser($userId);
            } else {
                $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
            }
        }

        // If folder ID is provided, filter boards by folder
        if ($folderId && defined('FLUENT_BOARDS_PRO')) {
            $boardIds = ProHelper::getBoardIdsByFolder($folderId);
            $relatedBoardsQuery = $relatedBoardsQuery->whereIn('id', $boardIds);
        }

        // Filter out boards that are templates (exclude boards where settings->is_template is true)
        $relatedBoardsQuery = $relatedBoardsQuery->where(function ($query) {
            $query->whereNull('settings')
                  ->orWhere(function ($subQuery) {
                      $subQuery->where('settings', 'NOT LIKE', '%"is_template";b:1%')
                               ->where('settings', 'NOT LIKE', '%"is_template":true%');
                  });
        });

        // Add search functionality
        if (!empty($searchInput)) {
            $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
        }

        $relatedBoards = $relatedBoardsQuery->orderBy($order, $orderBy)
                                            ->withCount('completedTasks')
                                            ->with('stages', 'users')
                                            ->paginate($per_page);

        foreach ($relatedBoards as $relatedBoard) {
            $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
            $relatedBoard->is_pinned = $this->boardService->isPinned($relatedBoard->id);
        }

        $response = [
            'boards' => $relatedBoards
        ];

        // Include folder mapping if pro version is available - ALWAYS include for consistency
        if (defined('FLUENT_BOARDS_PRO')) {
            $response['folder_mapping'] = $this->getBoardFolderMapping($userId);
            if ($folderId) {
                $response['current_folder'] = $this->getCurrentFolderInfo($folderId);
            }
        } else {
            // Include empty folder mapping for consistency
            $response['folder_mapping'] = [];
        }

        return $this->sendSuccess($response);
    }

    /**
     * Get folder mapping for boards
     */
    private function getBoardFolderMapping($userId)
    {
        if (!defined('FLUENT_BOARDS_PRO')) {
            return [];
        }

        $folderService = new \FluentBoardsPro\App\Services\FolderService();
        $folders = $folderService->getFolders($userId);

        $mapping = [];
        foreach ($folders as $folder) {
            $mapping[$folder->id] = [
                'id' => $folder->id,
                'title' => $folder->title,
                'board_ids' => $folder->boards ? $folder->boards->pluck('id')->toArray() : []
            ];
        }

        return $mapping;
    }

    /**
     * Get current folder information
     */
    private function getCurrentFolderInfo($folderId)
    {
        if (!defined('FLUENT_BOARDS_PRO')) {
            return null;
        }

        $folderService = new \FluentBoardsPro\App\Services\FolderService();
        $folder = $folderService->getFolderById($folderId);

        if (!$folder) {
            return null;
        }

        return [
            'id' => $folder->id,
            'title' => $folder->title,
            'board_count' => $folder->boards ? $folder->boards->count() : 0
        ];
    }

    /**
     * Get the list of boards and their associated stages for the current user.
     *
     * @param \FluentBoards\Framework\Http\Request\Request $request
     * @return \WP_REST_Response
     */
    public function getBoardsList(Request $request)
    {
        $userId = get_current_user_id();

        // Query to fetch boards that are not archived and accessible by the user
        // Check if the FLUENT_ROADMAP constant is defined
        if (!defined('FLUENT_ROADMAP')) {
            $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
        } else {
            $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
        }

        $relatedBoards = $relatedBoardsQuery->with('stages')->get();

        // Fetch the stages associated with the boards
        $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();

        return $this->sendSuccess([
            'boards' => $relatedBoards,
            'all_stages' => $stages,
        ], 200);
    }
    public function getOnlyBoardsByUser(Request $request)
    {
        try {
            $userId = get_current_user_id();

            $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');


            if(!defined('FLUENT_ROADMAP'))
            {
                $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
            } else {
                $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
            }

            if (!empty($searchInput)) {
                $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
            }

            $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();

            return $this->sendSuccess([
                'boards' => $relatedBoards
            ]);
        } catch (\Exception $e) {
            return $this->sendError([
                'message' => $e->getMessage()
            ]);
        }
    }

    public function getRecentBoards()
    {
        $boards = $this->boardService->getRecentBoards();

        if (!$boards || $boards->isEmpty()) {
            $boards = Board::where('type', 'to-do')->byAccessUser(get_current_user_id())
                ->limit(4)
                ->withCount('completedTasks')
                ->with(['stages', 'users'])
                ->get();
        }

        foreach ($boards as $board) {
            $board->users = Helper::sanitizeUserCollections($board->users);
            $board->is_pinned = $this->boardService->isPinned($board->id);
        }

        return [
            'boards' => $boards,
        ];
    }

    /*
     * TODO: Refactor this method , remove this
     */
    public function getBoardsByType($type)
    {
        $boards = $this->boardService->getBoardsByType($type);

        return $this->sendSuccess([
            'boards' => $boards,
        ]);
    }

    public function createFirstBoard(Request $request)
    {
        $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
            'title'          => 'required|string',
            'description'    => 'nullable',
            'type'           => 'required|string',
            'currency'       => 'nullable|string',
            'crm_contact_id' => 'nullable|numeric',
        ]);

        $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;

        $postStages = $request->get('stages');
        if (!is_array($postStages)) {
            $postStages = [];
        }
        $stageData = array();
        foreach ($postStages as $stage) {
            $temp = $this->stageSanitizeAndValidate($stage, [
                'title' => 'required|string',
            ]);
            $stageData[] = $temp;
        }

        $taskData = null;
        if ($request->get('task')) {
            $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
                'title' => 'required|string',
            ]);
        }

        $board = $this->boardService->createBoard($boardData);
        $this->labelService->createDefaultLabel($board->id);
        $type = ucfirst($boardData['type']);
        $stage = $this->stageService->createStages($board, $stageData);

        if ($taskData) {
            $taskData['board_id'] = $board->id;
            $taskData['stage_id'] = $stage->id;
            $this->taskService->createTask($taskData, $board->id);
        }

        do_action('fluent_boards/board_created', $board);

        if ($installFluentCRM && !defined('FLUENTCRM')) {
            InstallService::install('fluent-crm');
        }

        return [
            'message' => __('Board has been created', 'fluent-boards'),
            'board'   => $board,
        ];
    }

    public function skipOnboarding(Request $request)
    {
        $onboarding = Meta::where('key', Constant::FBS_ONBOARDING)->first();
        if($onboarding && $onboarding->value == 'no'){
            $onboarding->value = 'yes' ;
            $onboarding->save();
        }

        return [
            'message' => __('Onboarding skipped successfully', 'fluent-boards'),
        ];
    }

    public function create(Request $request)
    {
        $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
            'title'          => 'required|string',
            'description'    => 'nullable',
            'type'           => 'required|string',
            'currency'       => 'nullable|string',
            'crm_contact_id' => 'nullable|numeric',
            'folder_id'      => 'nullable',
        ]);

        try {
            $board = $this->boardService->createBoard($boardData);
            $this->labelService->createDefaultLabel($board->id);
            $type = ucfirst($boardData['type']);

            if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
                $stages = $request->get('stages');
                $sanitizedStages = [];
                if (is_array($stages) && !empty($stages)) {
                    foreach ($stages as $stage) {
                        $sanitizedStages[] = $this->stageSanitizeAndValidate($stage, [
                            'title' => 'required|string',
                            'slug' => 'nullable|string',
                            'position' => 'nullable|numeric'
                        ]);
                    }
                }
                $this->stageService->createRoadmapStages($board, $sanitizedStages);
            } else {
                $this->stageService->createDefaultStages($board);
            }

            // if board is created from crm contact
            if (isset($boardData['crm_contact_id'])) {
                $this->boardService->updateAssociateMember($boardData['crm_contact_id'], $board->id);
            }

            do_action('fluent_boards/board_created', $board);


            if(defined('FLUENT_BOARDS_PRO')) {
                $folderId = $request->getSafe('folder_id', 'intval');
                if ($folderId) {
                    (new \FluentBoardsPro\App\Services\FolderService())->addBoardToFolder($folderId, [$board->id]);
                }
            }

            $message = __('Board has been created successfully', 'fluent-boards');

            return $this->send([
                'message' => $message,
                'board'   => $board,
            ], 201);
        } catch (\Exception $e) {
            return $this->sendError([
                'message' => $e->getMessage()
            ]);
        }
    }

    public function getArchivedStage(Request $request, $board_id)
    {
        try {
            $pagination = $request->getSafe('noPagination', 'boolval', false);
            $per_page = $request->getSafe('per_page', 'intval', 30);
            $page = $request->getSafe('page', 'intval', 1);

            if ($pagination) {
                $stages = Stage::where('board_id', $board_id)
                    ->whereNotNull('archived_at')
                    ->orderBy('created_at', 'DESC')
                    ->get();
            } else {
                $stages = Stage::where('board_id', $board_id)
                    ->whereNotNull('archived_at')
                    ->orderBy('created_at', 'DESC')
                    ->paginate($per_page, ['*'], 'page', $page);
            }

            return $this->sendSuccess([
                'stages' => $stages,
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 404);
        }
    }

    public function find(Request $request, $board_id)
    {
        $board = Board::findOrFail($board_id);
        $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
        $board->background = maybe_unserialize($board->background);
        $board->createdOn = $board->created_at->format('Y-m-d');

        $board->load(['users', 'labels', 'owner']);

        if ($includeArchived) {
            $board->stages = Stage::where('board_id', $board_id)
                ->orderBy('position', 'asc')
                ->get();
        } else {
            $board->load('stages');
        }

        if (defined('FLUENT_BOARDS_PRO')){
            $customFiledPositionMeta = $board->getMetaByKey('custom_field_positions');
            if(!$customFiledPositionMeta) {
                (new CustomFieldService())->reIndexCustomFieldPositions($board_id);
                $board->updateMeta('custom_field_positions', 'yes');
            }
            
            $board->load(['customFields']);
        }

        $this->boardService->updateRecentBoards($board_id);

        $board->labelColor = Constant::TRELLO_COLOR_MAP;
        $board->labelColorText = Constant::TEXT_COLOR_MAP;

        $board->users = Helper::sanitizeUserCollections($board->users);
        $board->owner = Helper::sanitizeUserCollections($board->owner);

        $board->is_pinned = $this->boardService->isPinned($board->id);

        $board = apply_filters('fluent_boards/board_find', $board);

        return [
            'board'     => $board,
            'synced_at' => current_time('mysql')
        ];
    }

    public function update(Request $request, $board_id)
    {
        $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
            'title'       => 'required|string',
            'description' => 'nullable|string',
        ]);

        $board = Board::findOrFail($board_id);

        $oldBoard = clone $board;
        $board->fill($boardData);
        $board->save();

        do_action('fluent_boards/board_updated', $board, $oldBoard);

        return [
            'message' => __('Board has been updated', 'fluent-boards'),
            'board'   => $board,
            'stages'  => $board->stages()->get(),  
        ];
    }

    public function archiveStage($board_id, $stage_id)
    {
        $board_id = absint($board_id);
        $stage_id = absint($stage_id);

        try {
            $stage = $this->findStageOnBoard($stage_id, $board_id);

            $updatedStage = $this->boardService->archiveStage($board_id, $stage);

            return $this->sendSuccess([
                'updatedStage' => $updatedStage,
                'message'      => __('Stage has been archived', 'fluent-boards'),
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }

    public function restoreStage($board_id, $stage_id)
    {
        $board_id = absint($board_id);
        $stage_id = absint($stage_id);

        try {
            $stage = $this->findStageOnBoard($stage_id, $board_id);

            $updatedStage = $this->boardService->restoreStage($board_id, $stage);

            return $this->sendSuccess([
                'success' => true,
                'updatedStage' => $updatedStage,
                'message' => __('Stage has been restored', 'fluent-boards')
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }


    public function repositionStages(Request $request, $board_id)
    {
        $incomingList = $request->get('list');
        if (!is_array($incomingList)) {
            $incomingList = [];
        }
        $incomingList = array_map('intval', $incomingList);
        try {
            foreach ($incomingList as $stageId) {
                $this->findStageOnBoard($stageId, $board_id);
            }

            $this->boardService->repositionStages($board_id, $incomingList);
            return $this->sendSuccess([
                'message'       => __('Stages Reordered', 'fluent-boards'),
                'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }

    public function getAssigneesByBoard($board_id)
    {
        return $this->sendSuccess([
            'data' => $this->boardService->getAssigneesByBoard($board_id),
        ], 200);
    }

    public function delete($board_id)
    {
        try {
            if (!PermissionManager::isAdmin()) {
                throw new \Exception(esc_html__('You do not have permission to delete this board', 'fluent-boards'), 400);
            }
            $this->boardService->deleteBoard($board_id);

            return $this->sendSuccess([
                'message' => __('Board has been deleted', 'fluent-boards'),
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }

    public function getCurrencies()
    {
        return BoardHandler::getCurrencies();
    }

    public function getActivities(Request $request, $board_id)
    {
        try {
            $activities = $this->boardService->getActivities($board_id, [
                'per_page' => $request->getSafe('per_page', 'intval', 40),
                'page' => $request->getSafe('page', 'intval', 1),
            ]);
            return $this->sendSuccess([
                'activities' => $activities,
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 404);
        }
    }

    /*
     * TODO: Refactor this method - for Masiur
     */
    public function getBoardUsers($board_id)
    {
        $board = Board::findOrFail($board_id);

        $boardObjects = Relation::where('object_type', 'board_user')
            ->where('object_id', $board_id)
            ->get()->keyBy('foreign_id');

        $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
            ->get()->pluck('object_id')->toArray();

        $userIds = $boardObjects->pluck('foreign_id')->toArray();

        $coreUsers = [];
        if ($userIds) {
            // Get the users who are in the board (members and managers
            $coreUsers = get_users([
                'include' => $userIds
            ]);
        }

        $formattedUsers = [];

        foreach ($coreUsers as $user) {
            $name = trim($user->first_name . ' ' . $user->last_name);
            if (!$name) {
                $name = $user->display_name;
            }

            $boardRelation = $boardObjects[$user->ID] ?? null;


            $formattedUsers[] = [
                'ID'           => $user->ID,
                'display_name' => $name,
                'user_login'   => $user->user_login,
                'email'        => $user->user_email,
                'photo'        => fluent_boards_user_avatar($user->user_email, $name),
                'role'         => $this->boardUserRole($boardRelation),
                'is_super'     => in_array($user->ID, $superAdminIds),
                'is_wpadmin'   => $user->has_cap('manage_options')
            ];
        }

        // order formatted users by display_name
        usort($formattedUsers, function ($a, $b) {
            return strcmp($a['display_name'], $b['display_name']);
        });

        $returnData = [
            'users'         => Helper::sanitizeUsersArray($formattedUsers, $board_id),
            'global_admins' => []
        ];

        if (!PermissionManager::isAdmin(get_current_user_id())) {
            return $returnData;
        }

        /*
         * These are the rest of the admin users who are not in the board
         */
        $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
            ->whereNotIn('object_id', $userIds)
            ->get()
            ->pluck('object_id')
            ->toArray();

        if ($adminUserIds) {
            $adminUsers = get_users([
                'include' => $adminUserIds,
            ]);

            $formattedAdminUsers = [];

            foreach ($adminUsers as $user) {
                $name = trim($user->first_name . ' ' . $user->last_name);
                if (!$name) {
                    $name = $user->display_name;
                }

                $formattedAdminUsers[] = [
                    'ID'           => $user->ID,
                    'display_name' => $name,
                    'email'        => $user->user_email,
                    'photo'        => fluent_boards_user_avatar($user->user_email, $name),
                    'role'         => 'admin',
                    'is_super'     => in_array($user->ID, $superAdminIds),
                    'is_wpadmin'   => $user->has_cap('manage_options')
                ];
            }

            // order formatted users by display_name
            usort($formattedAdminUsers, function ($a, $b) {
                return strcmp($a['display_name'], $b['display_name']);
            });

            $returnData['global_admins'] = Helper::sanitizeUsersArray($formattedAdminUsers, $board_id);
        }

        return $this->sendSuccess($returnData, 200);
    }


    public function removeUserFromBoard($board_id, $userId)
    {
        $this->boardService->removeUserFromBoard($board_id, $userId);

        if (!PermissionManager::isAdmin($userId)) {
            $this->boardService->removeFromRecentlyOpened($board_id, $userId);
        }

        return [
            'message' => __('Member removed successfully', 'fluent-boards'),
        ];
    }

    public function addMembersInBoard(Request $request, $board_id)
    {
        $memberId = $request->getSafe('memberId');
        $isViewerOnly = $request->getSafe('isViewerOnly');
        $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
        if (!$member) {
            return $this->sendError([
                'message' => __('User already a member', 'fluent-boards'),
            ], 304);
        }


        return [
            'message' => __('Member added successfully', 'fluent-boards'),
            'member'  => Helper::sanitizeUserCollections($member)
        ];
    }

    private function boardSanitizeAndValidate($data, array $rules = [])
    {
        $data = Helper::sanitizeBoard($data);

        return $this->validate($data, $rules);
    }

    private function stageSanitizeAndValidate($data, array $rules = [])
    {
        $data = Helper::sanitizeStage($data);

        return $this->validate($data, $rules);
    }

    private function taskSanitizeAndValidate($data, array $rules = [])
    {
        $data = Helper::sanitizeTask($data);

        return $this->validate($data, $rules);
    }

    public function searchBoards(Request $request)
    {
        $per_page = $request->getSafe('per_page', 'intval', 10);
        $search_input = $request->getSafe('searchInput', 'sanitize_text_field', '');
        $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');

        $currentUserId = get_current_user_id();

        if (PermissionManager::isAdmin($currentUserId)) {
            $boards = Board::query()->where('type', $type)
                ->where('title', 'like', '%' . $search_input . '%')
                ->with('stages', 'tasks', 'users')
                ->paginate($per_page);

            foreach ($boards as $board) {
                $board->users = Helper::sanitizeUserCollections($board->users);
            }

        } else {
            $currentUser = User::find($currentUserId);
            $boards = $currentUser->boards()->where('type', $type)->where('title', 'like', '%' . $search_input . '%')->paginate($per_page);
        }

        return [
            'boards' => $boards,
        ];
    }

    public function getUsersOfBoards()
    {
        $userBoards = $this->boardService->getUsersOfBoards();

        return $this->sendSuccess([
            'userBoards' => $userBoards,
        ], 200);
    }



    /**
     * Refactor this code form me - Masiur
     * change stage settings is_public for roadmap user and admin view
     * @param $board_id
     * @param $stage_id
     * @return
     */
    public function changeStageView($board_id, $stage_id)
    {
        $board_id = absint($board_id);
        $stage_id = absint($stage_id);

        try {
            $stage = $this->findStageOnBoard($stage_id, $board_id);
            $message = __('The stage is made public!', 'fluent-boards');
            $settings = $stage->settings;

            if (isset($settings['is_public'])) {
                if ($settings['is_public']) {
                    $settings['is_public'] = false;
                    $message = __('The stage is made admin only!', 'fluent-boards');
                } else {
                    $settings['is_public'] = true;
                }
            } else {
                $settings['is_public'] = true;
            }

            $stage->settings = $settings;
            $stage->save();
            return $this->sendSuccess([
                'message' => $message,
                'stage'   => $stage
            ]);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }


    /**
     * Set board background image or color
     * @param \FluentBoards\Framework\Http\Request\Request $request
     * @return
     */
    public function setBoardBackground(Request $request, $board_id)
    {
        // sanitize and validate image_url
        if ($request->image_url) {
            $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
                "id"        => 'required',
                'image_url' => 'required|string|url',
            ]);
        }

        // sanitize and validate color
        if ($request->color) {
            $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
                "id"    => 'required',
                'color' => 'required',
            ]);
        }

        try {
            if (!$board_id) {
                $errorMessage = __('Board id is required', 'fluent-boards');
                throw new \Exception(esc_html($errorMessage), 400);
            }

            return $this->sendSuccess([
                'message'    => __('Background updated successfully', 'fluent-boards'),
                'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
            ]);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }


    /**
     * Summary of getStageTaskAvailablePositions
     * @param mixed $board_id
     * @param mixed $stage_slug
     * @return $availablePositions as an array
     * @throws \Exception
     */
    public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
    {
        try {
            if ($board_id && $stage_id) {
                $taskId = $request->getSafe('task_id', 'intval');
                $availablePositions = $this->boardService->getStageTaskAvailablePositions($board_id, $stage_id, $taskId);
                return $this->sendSuccess([
                    'availablePositions' => $availablePositions['availablePositions'],
                    'moveTargets' => $availablePositions['moveTargets'],
                    'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
                    'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
                ], 200);
            } else {
                $message = '';
                if (!$board_id) {
                    $message = 'Board id ';
                }
                if (!$stage_id) {
                    $message = 'Stage ';
                }
                throw new \Exception(esc_html($message . 'is required'), 400);
            }
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }

    public function getAssociateCrmContacts($board_id)
    {
        try {
            $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
                ->whereNotNull('crm_contact_id')
                ->get();

            $formattedContacts = Collection::make($contactAssociatedTasks)
                ->groupBy('crm_contact_id')
                ->map(function ($tasks, $contactId) {
                    $subscriber = Subscriber::find($contactId);
                    if (!$subscriber) {
                        return null; // Skip if subscriber not found
                    }

                    return [
                        'name'           => $subscriber->first_name . ' ' . $subscriber->last_name,
                        'photo'          => $subscriber->photo,
                        'email'          => $subscriber->email,
                        'crm_contact_id' => $contactId,
                        'id'             => $contactId,
                        'tasks'          => $tasks,
                    ];
                })
                ->filter()->toArray();


            return $this->sendSuccess([
                'associatedContacts' => $formattedContacts
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 404);
        }
    }

    public function updateAssociateCrmContact(Request $request, $board_id)
    {
        $value = $request->getSafe('value');
        $this->boardService->updateAssociateMember($value, $board_id);

        return $this->sendSuccess([
            'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
        ], 200);
    }

    public function hasDataChanged(Request $request, $board_id)
    {
        $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
        $since = $request->getSafe('since', 'sanitize_text_field');
        return $this->boardService->hasDataChanged($board_id, $includeArchived, $since);
    }

    public function createStage(Request $request, $board_id)
    {
        $stageData = $this->stageSanitizeAndValidate($request->all(), [
            'title' => 'required|string',
            'position' => 'nullable|numeric'
        ]);

        $board = Board::find($board_id);
        $stage = $this->stageService->createStage($stageData, $board_id);

        do_action('fluent_boards/board_stage_added', $board, $stage);

        $updatedStates = (new StageService())->getLastOneMinuteUpdatedStages($board_id);

        return [
            'updatedStages' => $updatedStates,
            'message'       => __('stage has been created', 'fluent-boards'),
        ];
    }

    public function moveAllTasks(Request $request, $board_id)
    {
        $oldStageId = $request->getSafe('oldStageId', 'intval');
        $newStageId = $request->getSafe('newStageId', 'intval');

        if (!$oldStageId || !$newStageId) {
            return $this->sendError(__('Invalid stage IDs provided', 'fluent-boards'), 400);
        }

        // Verify stages exist and belong to the board
        $oldStage = Stage::where('id', $oldStageId)->where('board_id', $board_id)->first();
        $newStage = Stage::where('id', $newStageId)->where('board_id', $board_id)->first();

        if (!$oldStage || !$newStage) {
            return $this->sendError(__('One or both stages do not exist or do not belong to this board', 'fluent-boards'), 400);
        }

        $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);

        return [
            'message'      => __('Tasks have been moved', 'fluent-boards'),
            'updatedTasks' => $updates,
        ];

    }

    public function archiveAllTasksInStage($board_id, $stage_id)
    {
        $board_id = absint($board_id);
        $stage_id = absint($stage_id);

        try {
            $this->findStageOnBoard($stage_id, $board_id);
            $updates = $this->stageService->archiveAllTasksInStage($stage_id, $board_id);

            return [
                'message'      => __('Tasks have been archived', 'fluent-boards'),
                'updatedTasks' => $updates,
            ];
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }

    public function getAssociatedBoards(Request $request, $associated_id)
    {
        if (!$this->currentUserCanReadCrmContacts()) {
            return $this->sendError(esc_html__('You do not have permission to view CRM contact boards', 'fluent-boards'), 403);
        }

        $associatedBoards = $this->boardService->getAssociatedBoards($associated_id, get_current_user_id());
        return [
            'boards' => $associatedBoards,
        ];
    }

    private function currentUserCanReadCrmContacts()
    {
        $permissionManager = 'FluentCrm\\App\\Services\\PermissionManager';

        if (!class_exists($permissionManager)) {
            return false;
        }

        return (bool) $permissionManager::currentUserCan('fcrm_read_contacts');
    }

    public function duplicateBoard(Request $request, $board_id)
    {
        $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
            'title' => 'required|string'
        ]);

        $boardData['source_board_id'] = $board_id;

        $isWithLabels = $request->getSafe('isWithLabels');
        $isWithTasks = $request->getSafe('isWithTasks');
        $isWithTemplates = $request->getSafe('isWithTemplates');

        try {
            if(!PermissionManager::isAdmin()) {
                $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
                throw new \Exception(esc_html($errorMessage), 400);
            }
            //create board
            $newBoard = $this->boardService->copyBoard($boardData);

            //label copy
            $labelMap = [];

            if ($isWithLabels == 'yes') {
                $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
            }

            //stage copy
            $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);

            //copy tasks of selected stages
            if ($isWithTasks == 'yes') {
                $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
            }

            return $this->sendSuccess([
                'board' => $newBoard,
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }

    public function importFromBoard(Request $request, $board_id)
    {
        $selectedStages = $request->getSafe('selectedStages');
        $position = $request->getSafe('position', 'intval');

        // Validate and sanitize selectedStages array
        if (!is_array($selectedStages)) {
            $selectedStages = [$selectedStages];
        }
        $selectedStages = array_filter(array_map('intval', $selectedStages));

        try {
            $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);

            return $this->sendSuccess([
                'message' => __('Import successfully', 'fluent-boards'),
            ], 200);

        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }

    public function getBoardDefaultBackgroundColors()
    {
        return [
            'solidColors' => Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
            'gradients'   => Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
        ];
    }

    /*
     * TODO: For Masiur - I will update this later
     */
    public function updateBoardProperties(Request $request, $board_id)
    {
        $pageId = $request->getSafe('page_id');
        $enable_stage_change_email = $request->getSafe('enable_stage_change_email');

        $board = Board::findOrFail($board_id);

        $board->updateMeta('roadmap_page_id', $pageId);
        $board->updateMeta('enable_stage_change_email', $enable_stage_change_email);

        $board = $board->fresh();

        return [
            'message' => __('Board has been updated', 'fluent-boards'),
            'board'   => apply_filters('fluent_boards/board_find', $board)
        ];
    }

    public function archiveBoard($board_id)
    {
        try {
            $board = $this->boardService->archiveBoard($board_id);

            return [
                'board' => $board,
                'message' => __('Board has been archived successfully!', 'fluent-boards')
            ];
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }

    public function restoreBoard($board_id)
    {
        try {
            $board = $this->boardService->restoreBoard($board_id);

            return [
                'board' => $board,
                'message' => __('Board has been restored successfully!', 'fluent-boards')
            ];
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }

    private function boardUserRole($boardRelation)
    {
        return $boardRelation && Arr::get($boardRelation->settings, 'is_admin')
            ? 'manager'
            : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
            ? 'viewer'
            : 'member');
    }
    public function uploadBoardBackground(Request $request,$board_id)
    {
        $file = Arr::get($request->files(), 'file')->toArray();
        (new \FluentBoards\App\Services\UploadService)->validateFile($file);

        $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);

        $fileData = $uploadInfo[0];
        $initialDataData = [
            'type' => 'url',
            'url' => '',
            'name' => '',
            'size' => 0,
        ];

        $attachData = array_merge($initialDataData, $fileData);
        $UrlMeta = [];
        if($attachData['type'] == 'url') {
            $UrlMeta = RemoteUrlParser::parse($attachData['url']);
        }
        $uid = wp_generate_uuid4();
        $fileUploadedData = new Attachment();
        $fileUploadedData->object_id = $board_id;
        $fileUploadedData->object_type = Constant::BOARD_BACKGROUND_IMAGE;
        $fileUploadedData->attachment_type = $attachData['type'];
        $fileUploadedData->title = (new TaskService())->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
        $fileUploadedData->file_path = $attachData['type'] != 'url' ?  $attachData['file'] : null;
        $fileUploadedData->full_url = esc_url($attachData['url']);
        $fileUploadedData->file_size = $attachData['size'];
        $fileUploadedData->settings = $attachData['type'] == 'url' ? [
            'meta' => $UrlMeta
        ] : '';
        $fileUploadedData->driver = 'local';
        $fileUploadedData->file_hash = md5($uid . wp_rand(0, 1000));
        $fileUploadedData->save();
        if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
            $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
            $fileUploadedData['driver'] = $mediaData['driver'];
            $fileUploadedData['file_path'] = $mediaData['file_path'];
            $fileUploadedData['full_url'] = $mediaData['full_url'];
            $fileUploadedData->save();
        }

        $board = Board::find($board_id);
        $oldBackground = $board->background;
        $publicUrl = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
        $background = [
            'color' => null,
            'id' => $fileUploadedData->id,
            'image_url' => $publicUrl,
            'is_image' => true,
        ];
        $board->background = $background;
        $board->save();
        do_action('fluent_boards/board_background_updated', $board_id, $oldBackground);

        return $this->sendSuccess([
            'message'    => __('Background updated successfully', 'fluent-boards'),
            'background' => $board->background,
        ]);
    }

    public function getPinnedBoards()
    {
        $pinnedBoards = $this->boardService->getPinnedBoards();

        return $this->sendSuccess([
            'pinnedBoards' => $pinnedBoards,
        ], 200);
    }

    public function pinBoard($boardId)
    {
        $this->boardService->pinBoard($boardId);

        return $this->sendSuccess([
            'message' => __('The Board has been pinned', 'fluent-boards'),
        ], 200);
    }

    public function unpinBoard($boardId)
    {
        $remove = $this->boardService->unpinBoard($boardId);

        if (!$remove) {
            return $this->sendError([
                'message' => __('Board is not pinned', 'fluent-boards'),
            ], 400);
        }

        return $this->sendSuccess([
            'message' => __('Board is removed from pinned boards', 'fluent-boards'),
        ], 200);
    }

    public function getBoardFolder($board_id)
    {
        try {
            $folder = $this->boardService->getBoardFolder($board_id);
            return $this->sendSuccess([
                'folder' => $folder,
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 400);
        }
    }

    public function getBoardMenuItems($board_id)
    {
        try {
            $menuItems = (new BoardMenuHandler())->getMenuItems($board_id);
            
            return $this->sendSuccess([
                'menu_items' => $menuItems
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 500);
        }
    }

    public function getPublicAccessSettings($board_id)
    {
        $board_id = absint($board_id);
        $board = Board::findOrFail($board_id);

        $enabled = (bool) $board->getMetaByKey('public_access_enabled');
        $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';

        return $this->sendSuccess([
            'enabled'   => $enabled,
            'shortcode' => $shortcode,
        ], 200);
    }

    public function togglePublicAccess(Request $request, $board_id)
    {
        $board_id = absint($board_id);
        $board = Board::findOrFail($board_id);

        $enabled = filter_var(
            $request->getSafe('enabled', 'sanitize_text_field', false),
            FILTER_VALIDATE_BOOLEAN
        );

        $board->updateMeta('public_access_enabled', $enabled ? '1' : '');

        $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';

        return $this->sendSuccess([
            'message'   => $enabled
                ? __('Public access has been enabled', 'fluent-boards')
                : __('Public access has been disabled', 'fluent-boards'),
            'enabled'   => $enabled,
            'shortcode' => $shortcode,
        ], 200);
    }

    /**
     * Resolve a stage only when it belongs to the requested board.
     *
     * @param int $stageId
     * @param int $boardId
     * @return Stage
     * @throws \Exception
     */
    private function findStageOnBoard($stageId, $boardId)
    {
        $stage = Stage::where('id', absint($stageId))
            ->where('board_id', absint($boardId))
            ->first();

        if (!$stage) {
            throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
        }

        return $stage;
    }

}

```
