defaultStages($board); foreach ($stages as $stage) { Stage::create($stage); } } public function defaultStages($board) { return $this->defaultStagesForTodos($board->id); } public function defaultStagesForTodos($boardId) { return [ [ 'board_id' => $boardId, 'title' => 'Open', 'position' => 1, 'slug' => 'open', 'settings' => [ 'default_task_status' => 'open' ] ], [ 'board_id' => $boardId, 'title' => 'In Progress', 'position' => 2, 'slug' => 'in-progress', 'settings' => [ 'default_task_status' => 'open' ] ], [ 'board_id' => $boardId, 'title' => 'Completed', 'position' => 3, 'slug' => 'completed', 'settings' => [ 'default_task_status' => 'closed' ] ] ]; } /** * Update one stage property and emit side effects for real title transitions. * * @param string $col * @param mixed $value * @param int $stageId * @return Stage */ public function updateStageProperty($col, $value, $stageId) { $stage = Stage::findOrFail($stageId); $stageBeforeUpdate = clone $stage; if ('title' == $col) { $stage = $this->updateTitle($value, $stage); } elseif ('status' == $col) { $stage = $this->updateStatus($value, $stage); } elseif ('color' == $col) { $stage = $this->updateColor($value, $stage); } elseif ('bg_color' == $col) { $stage = $this->updateBackgroundColor($value, $stage); } elseif ('archived_at' == $col) { $stage = $this->updateArchivedAt($value, $stage); } if ('title' == $col && $stageBeforeUpdate->title !== $stage->title) { do_action('fluent_boards/stage_updated', $stage->board_id, [ 'title' => $stage->title, 'cover_bg' => $stage->bg_color ], $stageBeforeUpdate); } return $stage; } public function getLastOneMinuteUpdatedStages($boardId, $lastUpdated = null, $includeArchived = true) { if (!$lastUpdated) { $oneMinuteAgoTimestamp = current_time('timestamp') - 60; $lastUpdated = date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp); } $stagesQuery = Stage::where('board_id', $boardId) ->where('updated_at', '>=', $lastUpdated); if (!$includeArchived) { $stagesQuery->whereNull('archived_at'); } return $stagesQuery->get(); } /** * Get archived stages newest-first with archive actor metadata. * * @param array $data * @param int $boardId * @return mixed * @throws \Exception */ public function getArchivedStages($data, $boardId) { if (!$boardId) { throw new \Exception(esc_html__('Board id is required', 'fluent-boards')); } $noPagination = !empty($data['noPagination']); $perPage = max(1, min(50, absint($data['per_page'] ?? 30))); $page = max(1, absint($data['page'] ?? 1)); $stagesQuery = Stage::where('board_id', $boardId) ->whereNotNull('archived_at') ->orderBy('archived_at', 'DESC') ->orderBy('id', 'DESC'); if ($noPagination) { $stages = $stagesQuery->get(); } else { $stages = $stagesQuery->paginate($perPage, ['*'], 'page', $page); } $this->attachArchivedStageActors($stages); return $stages; } /** * Attach nullable archived-by fields from stage settings without per-stage user queries. * * @param mixed $stages * @return void */ private function attachArchivedStageActors($stages) { $archivedByIds = []; foreach ($stages as $stage) { $stage->archived_by_id = null; $stage->archived_by = null; $settings = is_array($stage->settings) ? $stage->settings : []; $archivedById = !empty($settings['archived_by_id']) ? absint($settings['archived_by_id']) : 0; if ($archivedById) { $stage->archived_by_id = $archivedById; $archivedByIds[] = $archivedById; } } $archivedByIds = array_values(array_unique(array_filter($archivedByIds))); if (empty($archivedByIds)) { return; } $users = User::whereIn('ID', $archivedByIds)->get()->keyBy('ID'); foreach ($stages as $stage) { if (!$stage->archived_by_id) { continue; } $user = $users->get($stage->archived_by_id); $stage->archived_by = $user ? Helper::sanitizeUserCollections($user) : null; } } private function updateTitle($value, $stage) { $stage->title = $value; $stage->save(); return $stage; } private function updateStatus($value, $stage) { $oldSettings = $stage->settings; $oldSettings['default_task_status'] = $value; $stage->settings = $oldSettings; $stage->save(); return $stage; } private function updateColor($value, $stage) { $stage->color = $value; $stage->save(); return $stage; } private function updateBackgroundColor($value, $stage) { $stage->bg_color = $value; $stage->save(); return $stage; } private function updateArchivedAt($value, $stage) { $stage->archived_at = $value; $stage->save(); return $stage; } public function createStage($stageData, $boardId) { $stage = new Stage(); $stage->board_id = $boardId; $stage->title = $stageData['title']; $stage->settings = [ 'default_task_status' => Arr::get($stageData, 'status') ?? 'open', 'default_task_assignees' => [] ]; $providerPosition = Arr::get($stageData, 'position'); $lastStagePosition = $this->getLastPositionOfStagesOfBoard($boardId); $stage->position = $lastStagePosition ? $lastStagePosition->position + 1 : 1; $stage->save(); if($providerPosition){ $stage->moveToNewPosition($providerPosition); } return $stage; } public function getLastPositionOfStagesOfBoard($boardId) { // return last position of stages of board return Stage::where('board_id', $boardId) ->whereNull('archived_at') ->orderBy('position', 'desc') ->first(); } protected function moveOtherStages($stage) { $stages = Stage::where('board_id', $stage->board_id) ->where('position', '>=', $stage->position) ->whereNotIn('id', [$stage->id]) ->whereNull('archived_at')->get(); foreach ($stages as $stage) { $stage->position = $stage->position + 1; $stage->save(); } } /** * Copy all active stages and their settings to another board in display order. * * @param Board $board * @param int $fromBoardId * @param string $isWithTemplates * @param array|null $stageIds * @return array */ public function copyStagesOfBoard($board, $fromBoardId, $isWithTemplates = 'no', $stageIds = null) { $stageQuery = Stage::where('board_id', $fromBoardId) ->where('type', 'stage') ->whereNull('archived_at'); if (is_array($stageIds)) { $stageIds = array_values(array_filter(array_map('absint', $stageIds))); if (!$stageIds) { return []; } $stageQuery->whereIn('id', $stageIds); } $stages = $stageQuery->orderBy('position', 'asc')->get(); $stageMap = []; foreach ($stages as $key => $stage) { $settings = $stage->settings ?: []; // Board copies must not turn their stages into reusable stage templates. if ($isWithTemplates !== 'yes') { unset($settings['is_template']); } $stageData = [ 'title' => $stage->title, 'board_id' => $board->id, 'slug' => str_replace(' ', '-', strtolower($stage->title)), 'type' => 'stage', 'position' => $key + 1, 'bg_color' => $stage->bg_color, 'settings' => $settings, ]; $newStage = Stage::create($stageData); $stageMap[$stage->id] = $newStage->id; } return $stageMap; } public function importStagesFromBoard($board_id, $selectedStages, $position = null) { $board_id = absint($board_id); $selectedStages = array_values(array_unique(array_filter(array_map('absint', (array) $selectedStages)))); if (!$selectedStages) { return; } $targetStages = Stage::whereIn('id', $selectedStages) ->whereNull('archived_at') ->get(); if (count($targetStages) !== count($selectedStages)) { throw new \Exception(esc_html__('Stage not found', 'fluent-boards')); } $sourceBoardIds = array_values(array_unique(array_map('absint', $targetStages->pluck('board_id')->toArray()))); if (in_array(0, $sourceBoardIds, true)) { throw new \Exception(esc_html__('Stage not found', 'fluent-boards')); } $userId = get_current_user_id(); if (!$userId) { throw new \Exception(esc_html__('Stage not found', 'fluent-boards')); } if (!PermissionManager::isAdmin($userId)) { $allowedBoardIds = Relation::where('foreign_id', $userId) ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER) ->whereIn('object_id', $sourceBoardIds) ->pluck('object_id') ->toArray(); $allowedBoardIds = array_map('intval', $allowedBoardIds); $unauthorizedBoardIds = array_filter($sourceBoardIds, function ($sourceBoardId) use ($allowedBoardIds) { return !in_array($sourceBoardId, $allowedBoardIds, true); }); if ($unauthorizedBoardIds) { throw new \Exception(esc_html__('Stage not found', 'fluent-boards')); } } $stageMapForCopyingTask = array(); $stageIdsToCopy = array(); $numberOfStages = Stage::where('board_id', $board_id)->whereNull('archived_at')->count(); foreach($targetStages as $key => $stage) { $stageToSave = array(); $stageToSave['title'] = $stage['title'] . ' - imported'; $stageToSave['board_id'] = $board_id; $stageToSave['slug'] = str_replace(' ', '-', strtolower($stage['title'])); $stageToSave['type'] = 'stage'; $stageToSave['position'] = $numberOfStages + $key + 1; $stageToSave['settings'] = [ 'default_task_status' => $stage->settings['default_task_status'] ]; $newStage = Stage::create($stageToSave); if($position) { $newStage->moveToNewPosition( (int) $position + $key ); } $stageMapForCopyingTask[$stage['id']] = $newStage->id; $stageIdsToCopy[] = $stage['id']; } $this->importTasks($board_id, $stageMapForCopyingTask, $stageIdsToCopy); } public function importTasks($boardId, $stageMapper, $stageIds) { $tasksToImport = $this->getAllParentAndSubTasksOfStages($stageIds); $subtaskGroupChildRelations = []; $taskIdsToImport = $tasksToImport->pluck('id')->toArray(); if ($taskIdsToImport) { foreach (TaskMeta::where('key', Constant::SUBTASK_GROUP_CHILD) ->whereIn('task_id', $taskIdsToImport) ->get() as $relation) { $subtaskGroupChildRelations[$relation->task_id] = $relation; } } $taskMap = []; $subtaskGroupMap = []; $attachmentFileService = new AttachmentFileService(); $dbInstance = App::getInstance('db'); $dbInstance->beginTransaction(); try { foreach($tasksToImport as $task) { $newTask = array(); $newTask['title'] = $task->title; $newTask['parent_id'] = $task->parent_id ? $taskMap[$task->parent_id] : null; $newTask['description'] = $task->description; $newTask['board_id'] = $boardId; $newTask['stage_id'] = $stageMapper[$task->stage_id]; $newTask['status'] = $task->status; $newTask['priority'] = $task->priority; $newTask['position'] = $task->position; $newTask['due_at'] = $task->due_at; $backgroundColor = $task->settings['cover']['backgroundColor'] ?? ''; $newTask['settings'] = [ 'cover' => [ 'backgroundColor' => $backgroundColor, ] ]; $newTask = Task::create($newTask); $attachmentFileService->cloneTaskFilesToBoard($task, $newTask, $boardId); if (!$task->parent_id) { $taskMap[$task->id] = $newTask->id; //group mapping $subtaskGroupMap = (new TaskService())->copySubtaskGroup($task, $newTask, $subtaskGroupMap); } else { $groupRelationOfTask = $subtaskGroupChildRelations[$task->id] ?? null; if ($groupRelationOfTask && $subtaskGroupMap[$groupRelationOfTask->value]) { TaskMeta::create([ 'task_id' => $newTask->id, 'key' => Constant::SUBTASK_GROUP_CHILD, 'value' => $subtaskGroupMap[$groupRelationOfTask->value] ]); } } } //update task count of board $totalTasks = sizeof($tasksToImport); $board = Board::findOrFail($boardId); $settings = $board->settings ?? []; if (isset($settings['tasks_count'])) { $settings['tasks_count'] += $totalTasks; } else { $settings['tasks_count'] = $totalTasks; } $board->settings = $settings; $board->save(); $dbInstance->commit(); } catch (\Exception $e) { $dbInstance->rollBack(); $attachmentFileService->rollbackCreatedFiles(); throw $e; } } public function updateStageTemplate($stage_id) { $stage = Stage::findOrFail($stage_id); $stageSettings = $stage->settings; if($stageSettings && array_key_exists('is_template', $stageSettings)) { $currentlyIsTemplate = $stageSettings['is_template']; if($currentlyIsTemplate){ $stageSettings['is_template'] = false; $stage->settings = $stageSettings; }else{ $stageSettings['is_template'] = true; $stage->settings = $stageSettings; } }else { if(!$stageSettings) { $stage->settings = [ 'is_template' => true ]; } else { $stage->settings = array_merge($stage->settings, [ 'is_template' => true ]); } } $stage->save(); return $stage; } public function moveAllTasks($oldStageId, $newStageId) { $tasks = Task::where('stage_id', $oldStageId)->whereNull('parent_id')->whereNull('archived_at')->get(); // get the last position available of that stage $position = (new TaskService())->getLastPositionOfTasks($newStageId); // update tasks stage and position foreach ($tasks as $key => $task) { // Clean up archived_by_stage meta when moving to different stage TaskMeta::where('task_id', $task->id) ->where('key', Constant::META_KEY_ARCHIVED_BY_STAGE) ->delete(); $task->stage_id = $newStageId; $task->position = $position + $key; $task->save(); } return $tasks; } public function archiveAllTasksInStage($stage_id, $boardId = null) { $tasksQuery = Task::where('stage_id', absint($stage_id)) ->whereNull('parent_id') ->whereNull('archived_at'); if ($boardId) { $tasksQuery->where('board_id', absint($boardId)); } $tasks = $tasksQuery->get(); foreach ($tasks as $task) { $task->position = 0; $task->archived_at = current_time('mysql'); $task->save(); do_action('fluent_boards/task_archived', $task); } return $tasks; } public function createRoadmapStages($board, $stagesData) { foreach ($stagesData as $index => $formStageData) { $stage = new Stage(); $stage->board_id = $board->id; $stage->title = $formStageData['title']; $stage->slug = $formStageData['slug']; $stage->position = $formStageData['position'] ? $formStageData['position'] : 1; $stage->settings = $this->roadmapStageSetting($index); $stage->save(); } return $stagesData; } /* * I have no idea what this function does, but I am doing it * to make the code work */ public function roadmapStageSetting($index) { return [ 'is_public' => $index > 0 ? true : false, 'default_task_status' => 'open', 'is_template' => false, ]; } public function createStages($board, $stageData) { $firstStage = null; foreach ($stageData as $index => $stage) { $stageToPush = array(); $stageToPush['title'] = $stage['title']; $stageToPush['board_id'] = $board->id; $stageToPush['position'] = $index + 1; $stageToPush['slug'] = $this->createSlug($stage['title']); $closedStageTitles = ['completed', 'done']; $stageToPush['settings'] = [ 'default_task_status' => in_array(strtolower(Arr::get($stage, 'title')), $closedStageTitles) ? 'closed' : 'open', 'is_template' => false ]; $stage = Stage::create($stageToPush); if($index == 0){ $firstStage = $stage; } } return $firstStage; } private function createSlug($title) { return str_replace(' ', '-', strtolower($title)); } public function stagesByBoardId($boardId) { return Stage::where('board_id', $boardId)->whereNull('archived_at')->orderBy('position', 'asc')->get(); } public function sortStageTasks($order, $orderBy, $stage_id) { $sortOptions = ['priority', 'due_at', 'position', 'created_at', 'title']; $orderOptions = ['ASC', 'DESC']; // Validate order and orderBy parameters if (!in_array($order, $sortOptions) || !in_array($orderBy, $orderOptions)) { throw new \Exception(esc_html__('Invalid sort or orderBy parameter', 'fluent-boards')); } $tasksQuery = Task::where('stage_id', $stage_id) ->whereNull('parent_id') ->whereNull('archived_at') ->with(['assignees', 'labels', 'watchers']); // Apply ordering based on the specified order and orderBy switch ($order) { case 'priority': $tasksQuery->orderByRaw("FIELD(priority, 'urgent', 'high', 'medium', 'low') {$orderBy}"); break; case 'due_at': if ($orderBy === 'ASC') { // Separate ordering for tasks with and without due dates $tasksWithDueDate = (clone $tasksQuery)->whereNotNull('due_at')->orderBy('due_at')->get(); $tasksWithoutDueDate = (clone $tasksQuery)->whereNull('due_at')->get(); $tasks = $tasksWithDueDate->merge($tasksWithoutDueDate); } else { $tasksQuery->orderBy('due_at', $orderBy); } break; default: $tasksQuery->orderBy($order, $orderBy); break; } // Fetch tasks if not already fetched if (!isset($tasks)) { $tasks = $tasksQuery->get(); } // Update tasks with additional attributes $tasks->each(function ($task, $key) { $task->position = $key + 1; $task->save(); $task->isOverdue = $task->isOverdue(); $task->isUpcoming = $task->upcoming(); $task->is_watching = $task->isWatching(); $task->contact = Task::lead_contact($task->crm_contact_id); }); return $tasks; } public function updateStage($updatedStage, $board_id, $oldStage) { $stageBeforeUpdate = clone $oldStage; $oldStage->title = sanitize_text_field(Arr::get($updatedStage, 'title')); $oldStage->bg_color = sanitize_text_field(Arr::get($updatedStage, 'cover_bg')); $oldStage->save(); do_action('fluent_boards/stage_updated', $board_id, $updatedStage, $stageBeforeUpdate); return $oldStage; } public function setDefaultAssignees($stage_id, $assignees) { $stage = Stage::findOrFail($stage_id); if ($stage) { $settings = $stage->settings; $settings['default_task_assignees'] = $assignees; $stage->settings = $settings; $stage->save(); do_action('fluent_boards/default_assignees_updated', $stage, $assignees); return $stage; } } public function setDefaultWatchers($stage_id, $watchers) { $stage = Stage::findOrFail($stage_id); if ($stage) { $settings = $stage->settings; $settings['default_task_watchers'] = $watchers; $stage->settings = $settings; $stage->save(); do_action('fluent_boards/default_watchers_updated', $stage, $watchers); return $stage; } } public function getAllParentAndSubTasksOfStages($stageIds) { // Fetch parent task IDs for the given stage_ids $parentTaskIds = Task::whereIn('stage_id', $stageIds) ->whereNull('archived_at') ->whereNull('parent_id') ->pluck('id'); // Fetch parent tasks and subtasks in a single query $tasks = Task::whereIn('stage_id', $stageIds) ->whereNull('parent_id') ->whereNull('archived_at') ->get(); $subtasks = Task::whereIn('parent_id', $parentTaskIds) ->get(); // Combine parent tasks and subtasks into a single collection return $tasks->merge($subtasks); } }