PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
2.2.1 2.2.0 2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 All 44 releases
← All changes | app/Services/StageService.php +407 -72 1.35 → 2.2.0 View file →
@@ -4,10 +4,17 @@
4 4
5 5 use FluentBoards\App\Models\Task;
6 6 use FluentBoards\App\Models\Board;
7 7 use FluentBoards\App\Models\Stage;
8 +use FluentBoards\App\Models\TaskMeta;
9 +use FluentBoards\App\Models\Relation;
10 +use FluentBoards\App\Models\User;
11 +use FluentBoards\App\App;
8 12 use FluentBoards\Framework\Http\Request\Request;
9 13 use FluentBoards\Framework\Support\Arr;
14 +use FluentBoards\App\Services\Constant;
15 +use FluentBoards\App\Services\AttachmentFileService;
16 +use FluentBoards\App\Services\PermissionManager;
10 17
11 18 class StageService
12 19 {
13 20 public function createDefaultStages($board)
@@ -54,11 +61,21 @@
54 61 ]
55 62 ]
56 63 ];
57 64 }
65 +
66 + /**
67 + * Update one stage property and emit side effects for real title transitions.
68 + *
69 + * @param string $col
70 + * @param mixed $value
71 + * @param int $stageId
72 + * @return Stage
73 + */
58 74 public function updateStageProperty($col, $value, $stageId)
59 75 {
60 76 $stage = Stage::findOrFail($stageId);
77 + $stageBeforeUpdate = clone $stage;
61 78
62 79 if ('title' == $col) {
63 80 $stage = $this->updateTitle($value, $stage);
64 81 } elseif ('status' == $col) {
@@ -69,19 +86,111 @@
69 86 $stage = $this->updateBackgroundColor($value, $stage);
70 87 } elseif ('archived_at' == $col) {
71 88 $stage = $this->updateArchivedAt($value, $stage);
72 89 }
90 +
91 + if ('title' == $col && $stageBeforeUpdate->title !== $stage->title) {
92 + do_action('fluent_boards/stage_updated', $stage->board_id, [
93 + 'title' => $stage->title,
94 + 'cover_bg' => $stage->bg_color
95 + ], $stageBeforeUpdate);
96 + }
97 +
73 98 return $stage;
74 99 }
75 100
76 - public function getLastOneMinuteUpdatedStages($boardId)
101 + public function getLastOneMinuteUpdatedStages($boardId, $lastUpdated = null, $includeArchived = true)
77 102 {
78 - $oneMinuteAgoTimestamp = current_time('timestamp') - 60;
79 - return Stage::where('board_id', $boardId)
80 - ->where('updated_at', '>=', date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp))
81 - ->get();
103 + if (!$lastUpdated) {
104 + $oneMinuteAgoTimestamp = current_time('timestamp') - 60;
105 + $lastUpdated = date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp);
106 + }
107 +
108 + $stagesQuery = Stage::where('board_id', $boardId)
109 + ->where('updated_at', '>=', $lastUpdated);
110 +
111 + if (!$includeArchived) {
112 + $stagesQuery->whereNull('archived_at');
113 + }
114 +
115 + return $stagesQuery->get();
82 116 }
83 117
118 + /**
119 + * Get archived stages newest-first with archive actor metadata.
120 + *
121 + * @param array $data
122 + * @param int $boardId
123 + * @return mixed
124 + * @throws \Exception
125 + */
126 + public function getArchivedStages($data, $boardId)
127 + {
128 + if (!$boardId) {
129 + throw new \Exception(esc_html__('Board id is required', 'fluent-boards'));
130 + }
131 +
132 + $noPagination = !empty($data['noPagination']);
133 + $perPage = max(1, min(50, absint($data['per_page'] ?? 30)));
134 + $page = max(1, absint($data['page'] ?? 1));
135 +
136 + $stagesQuery = Stage::where('board_id', $boardId)
137 + ->whereNotNull('archived_at')
138 + ->orderBy('archived_at', 'DESC')
139 + ->orderBy('id', 'DESC');
140 +
141 + if ($noPagination) {
142 + $stages = $stagesQuery->get();
143 + } else {
144 + $stages = $stagesQuery->paginate($perPage, ['*'], 'page', $page);
145 + }
146 +
147 + $this->attachArchivedStageActors($stages);
148 +
149 + return $stages;
150 + }
151 +
152 + /**
153 + * Attach nullable archived-by fields from stage settings without per-stage user queries.
154 + *
155 + * @param mixed $stages
156 + * @return void
157 + */
158 + private function attachArchivedStageActors($stages)
159 + {
160 + $archivedByIds = [];
161 +
162 + foreach ($stages as $stage) {
163 + $stage->archived_by_id = null;
164 + $stage->archived_by = null;
165 +
166 + $settings = is_array($stage->settings) ? $stage->settings : [];
167 + $archivedById = !empty($settings['archived_by_id']) ? absint($settings['archived_by_id']) : 0;
168 +
169 + if ($archivedById) {
170 + $stage->archived_by_id = $archivedById;
171 + $archivedByIds[] = $archivedById;
172 + }
173 + }
174 +
175 + $archivedByIds = array_values(array_unique(array_filter($archivedByIds)));
176 +
177 + if (empty($archivedByIds)) {
178 + return;
179 + }
180 +
181 + $users = User::whereIn('ID', $archivedByIds)->get()->keyBy('ID');
182 +
183 + foreach ($stages as $stage) {
184 + if (!$stage->archived_by_id) {
185 + continue;
186 + }
187 +
188 + $user = $users->get($stage->archived_by_id);
189 + $stage->archived_by = $user ? Helper::sanitizeUserCollections($user) : null;
190 + }
191 + }
192 +
84 193 private function updateTitle($value, $stage)
85 194 {
86 195 $stage->title = $value;
87 196 $stage->save();
@@ -126,18 +235,15 @@
126 235 $stage->settings = [
127 236 'default_task_status' => Arr::get($stageData, 'status') ?? 'open',
128 237 'default_task_assignees' => []
129 238 ];
130 - if (!Arr::get($stageData, 'position')) {
131 - $lastStagePosition = $this->getLastPositionOfStagesOfBoard($boardId);
132 - $stage->position = $lastStagePosition ? $lastStagePosition->position + 1 : 1;
133 - $stage->save();
134 - } else {
135 - $stage->position = (int)$stageData['position'];
136 - $stage->save();
137 - $this->moveOtherStages($stage);
239 + $providerPosition = Arr::get($stageData, 'position');
240 + $lastStagePosition = $this->getLastPositionOfStagesOfBoard($boardId);
241 + $stage->position = $lastStagePosition ? $lastStagePosition->position + 1 : 1;
242 + $stage->save();
243 + if($providerPosition){
244 + $stage->moveToNewPosition($providerPosition);
138 245 }
139 -
140 246 return $stage;
141 247 }
142 248
143 249 public function getLastPositionOfStagesOfBoard($boardId)
@@ -162,36 +268,110 @@
162 268 $stage->save();
163 269 }
164 270 }
165 271
166 - public function copyStagesOfBoard($board, $fromBoardId)
272 + /**
273 + * Copy all active stages and their settings to another board in display order.
274 + *
275 + * @param Board $board
276 + * @param int $fromBoardId
277 + * @param string $isWithTemplates
278 + * @param array|null $stageIds
279 + * @return array
280 + */
281 + public function copyStagesOfBoard($board, $fromBoardId, $isWithTemplates = 'no', $stageIds = null)
167 282 {
168 - $stages = Stage::where('board_id', $fromBoardId)->where('type', 'stage')->whereNull('archived_at')->get();
169 - $stageMapForCopyingTask = array();
170 - foreach($stages as $key => $stage)
171 - {
172 - $stageToSave = array();
173 - $stageToSave['title'] = $stage['title'];
174 - $stageToSave['board_id'] = $board->id;
175 - $stageToSave['slug'] = str_replace(' ', '-', strtolower($stage['title']));
176 - $stageToSave['type'] = 'stage';
177 - $stageToSave['position'] = $key + 1;
178 - $stageToSave['settings'] = [
179 - 'default_task_status' => $stage->settings['default_task_status']
283 + $stageQuery = Stage::where('board_id', $fromBoardId)
284 + ->where('type', 'stage')
285 + ->whereNull('archived_at');
286 +
287 + if (is_array($stageIds)) {
288 + $stageIds = array_values(array_filter(array_map('absint', $stageIds)));
289 +
290 + if (!$stageIds) {
291 + return [];
292 + }
293 +
294 + $stageQuery->whereIn('id', $stageIds);
295 + }
296 +
297 + $stages = $stageQuery->orderBy('position', 'asc')->get();
298 + $stageMap = [];
299 +
300 + foreach ($stages as $key => $stage) {
301 + $settings = $stage->settings ?: [];
302 +
303 + // Board copies must not turn their stages into reusable stage templates.
304 + if ($isWithTemplates !== 'yes') {
305 + unset($settings['is_template']);
306 + }
307 +
308 + $stageData = [
309 + 'title' => $stage->title,
310 + 'board_id' => $board->id,
311 + 'slug' => str_replace(' ', '-', strtolower($stage->title)),
312 + 'type' => 'stage',
313 + 'position' => $key + 1,
314 + 'bg_color' => $stage->bg_color,
315 + 'settings' => $settings,
180 316 ];
181 - $newStage = Stage::create($stageToSave);
182 - $stageMapForCopyingTask[$stage['id']] = $newStage->id;
317 +
318 + $newStage = Stage::create($stageData);
319 + $stageMap[$stage->id] = $newStage->id;
183 320 }
184 - return $stageMapForCopyingTask;
321 +
322 + return $stageMap;
185 323 }
186 324
187 - public function importStagesFromBoard($board_id, $selectedStages)
325 + public function importStagesFromBoard($board_id, $selectedStages, $position = null)
188 326 {
189 - $targetStages = Stage::whereIn('id', $selectedStages)->get();
327 + $board_id = absint($board_id);
328 + $selectedStages = array_values(array_unique(array_filter(array_map('absint', (array) $selectedStages))));
329 +
330 + if (!$selectedStages) {
331 + return;
332 + }
333 +
334 + $targetStages = Stage::whereIn('id', $selectedStages)
335 + ->whereNull('archived_at')
336 + ->get();
337 +
338 + if (count($targetStages) !== count($selectedStages)) {
339 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
340 + }
341 +
342 + $sourceBoardIds = array_values(array_unique(array_map('absint', $targetStages->pluck('board_id')->toArray())));
343 +
344 + if (in_array(0, $sourceBoardIds, true)) {
345 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
346 + }
347 +
348 + $userId = get_current_user_id();
349 +
350 + if (!$userId) {
351 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
352 + }
353 +
354 + if (!PermissionManager::isAdmin($userId)) {
355 + $allowedBoardIds = Relation::where('foreign_id', $userId)
356 + ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
357 + ->whereIn('object_id', $sourceBoardIds)
358 + ->pluck('object_id')
359 + ->toArray();
360 +
361 + $allowedBoardIds = array_map('intval', $allowedBoardIds);
362 + $unauthorizedBoardIds = array_filter($sourceBoardIds, function ($sourceBoardId) use ($allowedBoardIds) {
363 + return !in_array($sourceBoardId, $allowedBoardIds, true);
364 + });
365 +
366 + if ($unauthorizedBoardIds) {
367 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
368 + }
369 + }
370 +
190 371 $stageMapForCopyingTask = array();
191 372 $stageIdsToCopy = array();
192 373
193 - //need to define position of stage
194 374 $numberOfStages = Stage::where('board_id', $board_id)->whereNull('archived_at')->count();
195 375
196 376 foreach($targetStages as $key => $stage)
197 377 {
@@ -204,8 +384,11 @@
204 384 $stageToSave['settings'] = [
205 385 'default_task_status' => $stage->settings['default_task_status']
206 386 ];
207 387 $newStage = Stage::create($stageToSave);
388 + if($position) {
389 + $newStage->moveToNewPosition( (int) $position + $key );
390 + }
208 391 $stageMapForCopyingTask[$stage['id']] = $newStage->id;
209 392 $stageIdsToCopy[] = $stage['id'];
210 393 }
211 394
@@ -213,36 +396,84 @@
213 396 }
214 397
215 398 public function importTasks($boardId, $stageMapper, $stageIds)
216 399 {
217 - $tasksToImport = Task::whereIn('stage_id', $stageIds)->whereNull('archived_at')->get();
400 + $tasksToImport = $this->getAllParentAndSubTasksOfStages($stageIds);
401 + $subtaskGroupChildRelations = [];
402 + $taskIdsToImport = $tasksToImport->pluck('id')->toArray();
218 403
219 - foreach($tasksToImport as $task)
220 - {
221 - $newTask = array();
222 - $newTask['title'] = $task->title;
223 - $newTask['description'] = $task->description;
224 - $newTask['board_id'] = $boardId;
225 - $newTask['stage_id'] = $stageMapper[$task->stage_id];
226 - $newTask['status'] = $task->status;
227 - $newTask['priority'] = $task->priority;
228 - $newTask['position'] = $task->position;
229 - $newTask['due_at'] = $task->due_at;
230 - Task::create($newTask);
404 + if ($taskIdsToImport) {
405 + foreach (TaskMeta::where('key', Constant::SUBTASK_GROUP_CHILD)
406 + ->whereIn('task_id', $taskIdsToImport)
407 + ->get() as $relation) {
408 + $subtaskGroupChildRelations[$relation->task_id] = $relation;
409 + }
231 410 }
411 + $taskMap = [];
412 + $subtaskGroupMap = [];
413 + $attachmentFileService = new AttachmentFileService();
414 + $dbInstance = App::getInstance('db');
232 415
233 - //update task count of board
234 - $totalTasks = sizeof($tasksToImport);
235 - $board = Board::findOrFail($boardId);
236 - $settings = $board->settings ?? [];
416 + $dbInstance->beginTransaction();
237 417
238 - if (isset($settings['tasks_count'])) {
239 - $settings['tasks_count'] += $totalTasks;
240 - } else {
241 - $settings['tasks_count'] = $totalTasks;
418 + try {
419 + foreach($tasksToImport as $task)
420 + {
421 + $newTask = array();
422 + $newTask['title'] = $task->title;
423 + $newTask['parent_id'] = $task->parent_id ? $taskMap[$task->parent_id] : null;
424 + $newTask['description'] = $task->description;
425 + $newTask['board_id'] = $boardId;
426 + $newTask['stage_id'] = $stageMapper[$task->stage_id];
427 + $newTask['status'] = $task->status;
428 + $newTask['priority'] = $task->priority;
429 + $newTask['position'] = $task->position;
430 + $newTask['due_at'] = $task->due_at;
431 + $backgroundColor = $task->settings['cover']['backgroundColor'] ?? '';
432 + $newTask['settings'] = [
433 + 'cover' => [
434 + 'backgroundColor' => $backgroundColor,
435 + ]
436 + ];
437 + $newTask = Task::create($newTask);
438 + $attachmentFileService->cloneTaskFilesToBoard($task, $newTask, $boardId);
439 +
440 + if (!$task->parent_id) {
441 + $taskMap[$task->id] = $newTask->id;
442 + //group mapping
443 + $subtaskGroupMap = (new TaskService())->copySubtaskGroup($task, $newTask, $subtaskGroupMap);
444 + } else {
445 + $groupRelationOfTask = $subtaskGroupChildRelations[$task->id] ?? null;
446 +
447 + if ($groupRelationOfTask && $subtaskGroupMap[$groupRelationOfTask->value]) {
448 + TaskMeta::create([
449 + 'task_id' => $newTask->id,
450 + 'key' => Constant::SUBTASK_GROUP_CHILD,
451 + 'value' => $subtaskGroupMap[$groupRelationOfTask->value]
452 + ]);
453 + }
454 + }
455 + }
456 +
457 + //update task count of board
458 + $totalTasks = sizeof($tasksToImport);
459 + $board = Board::findOrFail($boardId);
460 + $settings = $board->settings ?? [];
461 +
462 + if (isset($settings['tasks_count'])) {
463 + $settings['tasks_count'] += $totalTasks;
464 + } else {
465 + $settings['tasks_count'] = $totalTasks;
466 + }
467 + $board->settings = $settings;
468 + $board->save();
469 +
470 + $dbInstance->commit();
471 + } catch (\Exception $e) {
472 + $dbInstance->rollBack();
473 + $attachmentFileService->rollbackCreatedFiles();
474 + throw $e;
242 475 }
243 - $board->settings = $settings;
244 - $board->save();
245 476 }
246 477
247 478 public function updateStageTemplate($stage_id)
248 479 {
@@ -277,23 +508,93 @@
277 508
278 509 public function moveAllTasks($oldStageId, $newStageId)
279 510 {
280 511 $tasks = Task::where('stage_id', $oldStageId)->whereNull('parent_id')->whereNull('archived_at')->get();
512 + $sourceStage = Stage::findOrFail($oldStageId);
513 + $targetStage = Stage::findOrFail($newStageId);
281 514
515 + // Reuse task-user relations unless default assignments change them.
516 + $tasks->load(['assignees', 'watchers']);
517 +
282 518 // get the last position available of that stage
283 - $position = (new TaskService())->getLastPositionOfTasks($newStageId);
519 + $taskService = new TaskService();
520 + $position = $taskService->getLastPositionOfTasks($newStageId);
521 + $notificationService = new NotificationService();
522 + $currentUserId = get_current_user_id();
523 + // Stage-email eligibility per watcher, resolved in batches and shared across tasks.
524 + $emailEligibility = [];
284 525
285 526 // update tasks stage and position
286 527 foreach ($tasks as $key => $task) {
528 + $taskOldStageId = $task->stage_id;
529 +
530 + // Clean up archived_by_stage meta when moving to different stage
531 + TaskMeta::where('task_id', $task->id)
532 + ->where('key', Constant::META_KEY_ARCHIVED_BY_STAGE)
533 + ->delete();
534 +
287 535 $task->stage_id = $newStageId;
288 536 $task->position = $position + $key;
537 + $task->setRelation('stage', $targetStage);
289 538 $task->save();
539 +
540 + if ((int) $taskOldStageId !== (int) $newStageId) {
541 + $assignmentsChanged = $taskService->manageDefaultAssignees($task, $newStageId, $targetStage);
542 + if ($targetStage->defaultTaskStatus() === 'closed' && $task->status !== 'closed') {
543 + $task = $task->close();
544 + }
545 + // Assignment hooks may change assignees and watchers before notification.
546 + if ($assignmentsChanged) {
547 + $task->load(['assignees', 'watchers']);
548 + }
549 +
550 + do_action('fluent_boards/task_stage_updated', $task, $taskOldStageId, [
551 + 'source' => $sourceStage,
552 + 'target' => $targetStage,
553 + ]);
554 +
555 + $watcherIds = array_diff(array_map('intval', $task->watchers->pluck('ID')->toArray()), [$currentUserId]);
556 + $unknownIds = array_diff($watcherIds, array_keys($emailEligibility));
557 + if ($unknownIds) {
558 + $emailEligibility += array_fill_keys($unknownIds, false);
559 + $enabledIds = $notificationService->getEmailEnabledUserIds($task->board_id, $unknownIds, Constant::BOARD_EMAIL_STAGE_CHANGE);
560 + foreach ($enabledIds as $enabledId) {
561 + $emailEligibility[$enabledId] = true;
562 + }
563 + }
564 + $recipients = [];
565 + foreach ($task->watchers as $watcher) {
566 + if (!empty($emailEligibility[(int) $watcher->ID])) {
567 + $recipients[] = $watcher->user_email;
568 + }
569 + }
570 + $taskService->sendMailAfterTaskModify('stage_change', $recipients, $task->id);
571 + }
290 572 }
573 +
574 + if ($tasks->count() && (int) $sourceStage->id !== (int) $targetStage->id) {
575 + do_action(
576 + 'fluent_boards/tasks_moved_between_stages',
577 + $sourceStage->board_id,
578 + $sourceStage,
579 + $targetStage,
580 + $tasks->count()
581 + );
582 + }
583 +
291 584 return $tasks;
292 585 }
293 - public function archiveAllTasksInStage($stage_id)
586 + public function archiveAllTasksInStage($stage_id, $boardId = null)
294 587 {
295 - $tasks = Task::where('stage_id', $stage_id)->whereNull('parent_id')->whereNull('archived_at')->get();
588 + $tasksQuery = Task::where('stage_id', absint($stage_id))
589 + ->whereNull('parent_id')
590 + ->whereNull('archived_at');
591 +
592 + if ($boardId) {
593 + $tasksQuery->where('board_id', absint($boardId));
594 + }
595 +
596 + $tasks = $tasksQuery->get();
296 597 foreach ($tasks as $task) {
297 598 $task->position = 0;
298 599 $task->archived_at = current_time('mysql');
299 600 $task->save();
@@ -338,16 +639,14 @@
338 639 $stageToPush['title'] = $stage['title'];
339 640 $stageToPush['board_id'] = $board->id;
340 641 $stageToPush['position'] = $index + 1;
341 642 $stageToPush['slug'] = $this->createSlug($stage['title']);
643 + $closedStageTitles = ['completed', 'done'];
644 + $stageToPush['settings'] = [
645 + 'default_task_status' => in_array(strtolower(Arr::get($stage, 'title')), $closedStageTitles) ? 'closed' : 'open',
646 + 'is_template' => false
647 + ];
342 648
343 - if (Arr::get($stage, 'title') == 'Completed') {
344 - $stageToPush['settings'] = [
345 - 'default_task_status' => 'closed',
346 - 'is_template' => false
347 - ];
348 - }
349 -
350 649 $stage = Stage::create($stageToPush);
351 650 if($index == 0){
352 651 $firstStage = $stage;
353 652 }
@@ -361,9 +660,9 @@
361 660 }
362 661
363 662 public function stagesByBoardId($boardId)
364 663 {
365 - return Stage::where('board_id', $boardId)->whereNull('archived_at')->get();
664 + return Stage::where('board_id', $boardId)->whereNull('archived_at')->orderBy('position', 'asc')->get();
366 665 }
367 666
368 667
369 668
@@ -373,9 +672,9 @@
373 672 $orderOptions = ['ASC', 'DESC'];
374 673
375 674 // Validate order and orderBy parameters
376 675 if (!in_array($order, $sortOptions) || !in_array($orderBy, $orderOptions)) {
377 - throw new \Exception(__('Invalid sort or orderBy parameter', 'fluent-boards'));
676 + throw new \Exception(esc_html__('Invalid sort or orderBy parameter', 'fluent-boards'));
378 677 }
379 678
380 679 $tasksQuery = Task::where('stage_id', $stage_id)
381 680 ->whereNull('parent_id')
@@ -384,9 +683,9 @@
384 683
385 684 // Apply ordering based on the specified order and orderBy
386 685 switch ($order) {
387 686 case 'priority':
388 - $tasksQuery->orderByRaw("FIELD(priority, 'High', 'Medium', 'Low') {$orderBy}");
687 + $tasksQuery->orderByRaw("FIELD(priority, 'urgent', 'high', 'medium', 'low') {$orderBy}");
389 688 break;
390 689
391 690 case 'due_at':
392 691 if ($orderBy === 'ASC') {
@@ -435,11 +734,11 @@
435 734 public function setDefaultAssignees($stage_id, $assignees)
436 735 {
437 736 $stage = Stage::findOrFail($stage_id);
438 737 if ($stage) {
439 - $oldSettings = $stage->settings;
440 - $oldSettings['default_task_assignees'] = $assignees;
441 - $stage->settings = $oldSettings;
738 + $settings = $stage->settings;
739 + $settings['default_task_assignees'] = $assignees;
740 + $stage->settings = $settings;
442 741 $stage->save();
443 742
444 743 do_action('fluent_boards/default_assignees_updated', $stage, $assignees);
445 744
@@ -444,6 +743,42 @@
444 743 do_action('fluent_boards/default_assignees_updated', $stage, $assignees);
445 744
446 745 return $stage;
447 746 }
747 + }
748 +
749 + public function setDefaultWatchers($stage_id, $watchers)
750 + {
751 + $stage = Stage::findOrFail($stage_id);
752 + if ($stage) {
753 + $settings = $stage->settings;
754 + $settings['default_task_watchers'] = $watchers;
755 + $stage->settings = $settings;
756 + $stage->save();
757 +
758 + do_action('fluent_boards/default_watchers_updated', $stage, $watchers);
759 +
760 + return $stage;
761 + }
762 + }
763 +
764 + public function getAllParentAndSubTasksOfStages($stageIds)
765 + {
766 + // Fetch parent task IDs for the given stage_ids
767 + $parentTaskIds = Task::whereIn('stage_id', $stageIds)
768 + ->whereNull('archived_at')
769 + ->whereNull('parent_id')
770 + ->pluck('id');
771 +
772 + // Fetch parent tasks and subtasks in a single query
773 + $tasks = Task::whereIn('stage_id', $stageIds)
774 + ->whereNull('parent_id')
775 + ->whereNull('archived_at')
776 + ->get();
777 +
778 + $subtasks = Task::whereIn('parent_id', $parentTaskIds)
779 + ->get();
780 +
781 + // Combine parent tasks and subtasks into a single collection
782 + return $tasks->merge($subtasks);
448 783 }
449 784 }