PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.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 1.40 1.41 All 42 releases
← All changes | app/Services/StageService.php +393 -89 1.222.1.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();
@@ -123,20 +232,18 @@
123 232 $stage = new Stage();
124 233 $stage->board_id = $boardId;
125 234 $stage->title = $stageData['title'];
126 235 $stage->settings = [
127 - 'default_task_status' => Arr::get($stageData, 'status') ?? 'open'
236 + 'default_task_status' => Arr::get($stageData, 'status') ?? 'open',
237 + 'default_task_assignees' => []
128 238 ];
129 - if (!Arr::get($stageData, 'position')) {
130 - $lastStagePosition = $this->getLastPositionOfStagesOfBoard($boardId);
131 - $stage->position = $lastStagePosition ? $lastStagePosition->position + 1 : 1;
132 - $stage->save();
133 - } else {
134 - $stage->position = (int)$stageData['position'];
135 - $stage->save();
136 - $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);
137 245 }
138 -
139 246 return $stage;
140 247 }
141 248
142 249 public function getLastPositionOfStagesOfBoard($boardId)
@@ -161,36 +268,110 @@
161 268 $stage->save();
162 269 }
163 270 }
164 271
165 - 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)
166 282 {
167 - $stages = Stage::where('board_id', $fromBoardId)->where('type', 'stage')->whereNull('archived_at')->get();
168 - $stageMapForCopyingTask = array();
169 - foreach($stages as $key => $stage)
170 - {
171 - $stageToSave = array();
172 - $stageToSave['title'] = $stage['title'];
173 - $stageToSave['board_id'] = $board->id;
174 - $stageToSave['slug'] = str_replace(' ', '-', strtolower($stage['title']));
175 - $stageToSave['type'] = 'stage';
176 - $stageToSave['position'] = $key + 1;
177 - $stageToSave['settings'] = [
178 - '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,
179 316 ];
180 - $newStage = Stage::create($stageToSave);
181 - $stageMapForCopyingTask[$stage['id']] = $newStage->id;
317 +
318 + $newStage = Stage::create($stageData);
319 + $stageMap[$stage->id] = $newStage->id;
182 320 }
183 - return $stageMapForCopyingTask;
321 +
322 + return $stageMap;
184 323 }
185 324
186 - public function importStagesFromBoard($board_id, $selectedStages)
325 + public function importStagesFromBoard($board_id, $selectedStages, $position = null)
187 326 {
188 - $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 +
189 371 $stageMapForCopyingTask = array();
190 372 $stageIdsToCopy = array();
191 373
192 - //need to define position of stage
193 374 $numberOfStages = Stage::where('board_id', $board_id)->whereNull('archived_at')->count();
194 375
195 376 foreach($targetStages as $key => $stage)
196 377 {
@@ -203,8 +384,11 @@
203 384 $stageToSave['settings'] = [
204 385 'default_task_status' => $stage->settings['default_task_status']
205 386 ];
206 387 $newStage = Stage::create($stageToSave);
388 + if($position) {
389 + $newStage->moveToNewPosition( (int) $position + $key );
390 + }
207 391 $stageMapForCopyingTask[$stage['id']] = $newStage->id;
208 392 $stageIdsToCopy[] = $stage['id'];
209 393 }
210 394
@@ -212,36 +396,84 @@
212 396 }
213 397
214 398 public function importTasks($boardId, $stageMapper, $stageIds)
215 399 {
216 - $tasksToImport = Task::whereIn('stage_id', $stageIds)->whereNull('archived_at')->get();
400 + $tasksToImport = $this->getAllParentAndSubTasksOfStages($stageIds);
401 + $subtaskGroupChildRelations = [];
402 + $taskIdsToImport = $tasksToImport->pluck('id')->toArray();
217 403
218 - foreach($tasksToImport as $task)
219 - {
220 - $newTask = array();
221 - $newTask['title'] = $task->title;
222 - $newTask['description'] = $task->description;
223 - $newTask['board_id'] = $boardId;
224 - $newTask['stage_id'] = $stageMapper[$task->stage_id];
225 - $newTask['status'] = $task->status;
226 - $newTask['priority'] = $task->priority;
227 - $newTask['position'] = $task->position;
228 - $newTask['due_at'] = $task->due_at;
229 - 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 + }
230 410 }
411 + $taskMap = [];
412 + $subtaskGroupMap = [];
413 + $attachmentFileService = new AttachmentFileService();
414 + $dbInstance = App::getInstance('db');
231 415
232 - //update task count of board
233 - $totalTasks = sizeof($tasksToImport);
234 - $board = Board::findOrFail($boardId);
235 - $settings = $board->settings ?? [];
416 + $dbInstance->beginTransaction();
236 417
237 - if (isset($settings['tasks_count'])) {
238 - $settings['tasks_count'] += $totalTasks;
239 - } else {
240 - $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;
241 475 }
242 - $board->settings = $settings;
243 - $board->save();
244 476 }
245 477
246 478 public function updateStageTemplate($stage_id)
247 479 {
@@ -276,28 +508,67 @@
276 508
277 509 public function moveAllTasks($oldStageId, $newStageId)
278 510 {
279 511 $tasks = Task::where('stage_id', $oldStageId)->whereNull('parent_id')->whereNull('archived_at')->get();
512 + $sourceStage = Stage::findOrFail($oldStageId);
513 + $targetStage = Stage::findOrFail($newStageId);
280 514
515 + // Load shared watcher data once; stage data is injected per task below.
516 + $tasks->load('watchers');
517 +
281 518 // get the last position available of that stage
282 519 $position = (new TaskService())->getLastPositionOfTasks($newStageId);
283 520
284 521 // update tasks stage and position
285 522 foreach ($tasks as $key => $task) {
523 + $taskOldStageId = $task->stage_id;
524 +
525 + // Clean up archived_by_stage meta when moving to different stage
526 + TaskMeta::where('task_id', $task->id)
527 + ->where('key', Constant::META_KEY_ARCHIVED_BY_STAGE)
528 + ->delete();
529 +
286 530 $task->stage_id = $newStageId;
287 531 $task->position = $position + $key;
532 + $task->setRelation('stage', $targetStage);
288 533 $task->save();
534 +
535 + if ((int) $taskOldStageId !== (int) $newStageId) {
536 + do_action('fluent_boards/task_stage_updated', $task, $taskOldStageId, [
537 + 'source' => $sourceStage,
538 + 'target' => $targetStage,
539 + ]);
540 + }
289 541 }
542 +
543 + if ($tasks->count() && (int) $sourceStage->id !== (int) $targetStage->id) {
544 + do_action(
545 + 'fluent_boards/tasks_moved_between_stages',
546 + $sourceStage->board_id,
547 + $sourceStage,
548 + $targetStage,
549 + $tasks->count()
550 + );
551 + }
552 +
290 553 return $tasks;
291 554 }
292 - public function archiveAllTasksInStage($stage_id)
555 + public function archiveAllTasksInStage($stage_id, $boardId = null)
293 556 {
294 - $tasks = Task::where('stage_id', $stage_id)->whereNull('parent_id')->whereNull('archived_at')->get();
557 + $tasksQuery = Task::where('stage_id', absint($stage_id))
558 + ->whereNull('parent_id')
559 + ->whereNull('archived_at');
560 +
561 + if ($boardId) {
562 + $tasksQuery->where('board_id', absint($boardId));
563 + }
564 +
565 + $tasks = $tasksQuery->get();
295 566 foreach ($tasks as $task) {
296 567 $task->position = 0;
297 568 $task->archived_at = current_time('mysql');
298 569 $task->save();
299 - do_action('fluent_boards/board_task_archived', $task);
570 + do_action('fluent_boards/task_archived', $task);
300 571 }
301 572 return $tasks;
302 573 }
303 574
@@ -337,16 +608,14 @@
337 608 $stageToPush['title'] = $stage['title'];
338 609 $stageToPush['board_id'] = $board->id;
339 610 $stageToPush['position'] = $index + 1;
340 611 $stageToPush['slug'] = $this->createSlug($stage['title']);
612 + $closedStageTitles = ['completed', 'done'];
613 + $stageToPush['settings'] = [
614 + 'default_task_status' => in_array(strtolower(Arr::get($stage, 'title')), $closedStageTitles) ? 'closed' : 'open',
615 + 'is_template' => false
616 + ];
341 617
342 - if (Arr::get($stage, 'title') == 'Completed') {
343 - $stageToPush['settings'] = [
344 - 'default_task_status' => 'closed',
345 - 'is_template' => false
346 - ];
347 - }
348 -
349 618 $stage = Stage::create($stageToPush);
350 619 if($index == 0){
351 620 $firstStage = $stage;
352 621 }
@@ -360,9 +629,9 @@
360 629 }
361 630
362 631 public function stagesByBoardId($boardId)
363 632 {
364 - return Stage::where('board_id', $boardId)->whereNull('archived_at')->get();
633 + return Stage::where('board_id', $boardId)->whereNull('archived_at')->orderBy('position', 'asc')->get();
365 634 }
366 635
367 636
368 637
@@ -372,9 +641,9 @@
372 641 $orderOptions = ['ASC', 'DESC'];
373 642
374 643 // Validate order and orderBy parameters
375 644 if (!in_array($order, $sortOptions) || !in_array($orderBy, $orderOptions)) {
376 - throw new \Exception(__('Invalid sort or orderBy parameter', 'fluent-boards'));
645 + throw new \Exception(esc_html__('Invalid sort or orderBy parameter', 'fluent-boards'));
377 646 }
378 647
379 648 $tasksQuery = Task::where('stage_id', $stage_id)
380 649 ->whereNull('parent_id')
@@ -383,9 +652,9 @@
383 652
384 653 // Apply ordering based on the specified order and orderBy
385 654 switch ($order) {
386 655 case 'priority':
387 - $tasksQuery->orderByRaw("FIELD(priority, 'High', 'Medium', 'Low') {$orderBy}");
656 + $tasksQuery->orderByRaw("FIELD(priority, 'urgent', 'high', 'medium', 'low') {$orderBy}");
388 657 break;
389 658
390 659 case 'due_at':
391 660 if ($orderBy === 'ASC') {
@@ -419,31 +688,66 @@
419 688 });
420 689 return $tasks;
421 690 }
422 691
423 - public function checkStageEditable($stages, $toBeUpdatedStage)
692 +
693 + public function updateStage($updatedStage, $board_id, $oldStage)
424 694 {
425 - $stageEditable = true;
426 - if (empty(sanitize_text_field($toBeUpdatedStage['title']))) {
427 - $stageEditable = false;
428 - } else {
429 - foreach ($stages as $stage) { // check if stage label is similar to any other stage label
430 - if (strtolower($stage->title) == strtolower(trim($toBeUpdatedStage['title']))) {
431 - $stageEditable = false;
432 - break;
433 - }
434 - }
695 + $stageBeforeUpdate = clone $oldStage;
696 + $oldStage->title = sanitize_text_field(Arr::get($updatedStage, 'title'));
697 + $oldStage->bg_color = sanitize_text_field(Arr::get($updatedStage, 'cover_bg'));
698 + $oldStage->save();
699 + do_action('fluent_boards/stage_updated', $board_id, $updatedStage, $stageBeforeUpdate);
700 + return $oldStage;
701 + }
702 +
703 + public function setDefaultAssignees($stage_id, $assignees)
704 + {
705 + $stage = Stage::findOrFail($stage_id);
706 + if ($stage) {
707 + $settings = $stage->settings;
708 + $settings['default_task_assignees'] = $assignees;
709 + $stage->settings = $settings;
710 + $stage->save();
711 +
712 + do_action('fluent_boards/default_assignees_updated', $stage, $assignees);
713 +
714 + return $stage;
435 715 }
716 + }
436 717
437 - return $stageEditable;
718 + public function setDefaultWatchers($stage_id, $watchers)
719 + {
720 + $stage = Stage::findOrFail($stage_id);
721 + if ($stage) {
722 + $settings = $stage->settings;
723 + $settings['default_task_watchers'] = $watchers;
724 + $stage->settings = $settings;
725 + $stage->save();
726 +
727 + do_action('fluent_boards/default_watchers_updated', $stage, $watchers);
728 +
729 + return $stage;
730 + }
438 731 }
439 732
440 - public function updateStage($updatedStage, $board_id, $oldStage)
733 + public function getAllParentAndSubTasksOfStages($stageIds)
441 734 {
442 - $oldTitle = $oldStage->title;
443 - $oldStage->title = Arr::get($updatedStage, 'title');
444 - $oldStage->bg_color = Arr::get($updatedStage, 'cover_bg');
445 - $oldStage->save();
446 - do_action('fluent_boards/board_stage_updated', $board_id, $updatedStage['title'], $oldTitle);
447 - return $oldStage;
735 + // Fetch parent task IDs for the given stage_ids
736 + $parentTaskIds = Task::whereIn('stage_id', $stageIds)
737 + ->whereNull('archived_at')
738 + ->whereNull('parent_id')
739 + ->pluck('id');
740 +
741 + // Fetch parent tasks and subtasks in a single query
742 + $tasks = Task::whereIn('stage_id', $stageIds)
743 + ->whereNull('parent_id')
744 + ->whereNull('archived_at')
745 + ->get();
746 +
747 + $subtasks = Task::whereIn('parent_id', $parentTaskIds)
748 + ->get();
749 +
750 + // Combine parent tasks and subtasks into a single collection
751 + return $tasks->merge($subtasks);
448 752 }
449 753 }