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.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 All 43 releases
← All changes | app/Services/StageService.php +425 -90 1.23 → 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();
@@ -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,98 @@
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 + // Reuse task-user relations unless default assignments change them.
516 + $tasks->load(['assignees', 'watchers']);
517 +
281 518 // get the last position available of that stage
282 - $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 = [];
283 525
284 526 // update tasks stage and position
285 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 +
286 535 $task->stage_id = $newStageId;
287 536 $task->position = $position + $key;
537 + $task->setRelation('stage', $targetStage);
288 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 + }
289 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 +
290 584 return $tasks;
291 585 }
292 - public function archiveAllTasksInStage($stage_id)
586 + public function archiveAllTasksInStage($stage_id, $boardId = null)
293 587 {
294 - $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();
295 597 foreach ($tasks as $task) {
296 598 $task->position = 0;
297 599 $task->archived_at = current_time('mysql');
298 600 $task->save();
299 - do_action('fluent_boards/board_task_archived', $task);
601 + do_action('fluent_boards/task_archived', $task);
300 602 }
301 603 return $tasks;
302 604 }
303 605
@@ -337,16 +639,14 @@
337 639 $stageToPush['title'] = $stage['title'];
338 640 $stageToPush['board_id'] = $board->id;
339 641 $stageToPush['position'] = $index + 1;
340 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 + ];
341 648
342 - if (Arr::get($stage, 'title') == 'Completed') {
343 - $stageToPush['settings'] = [
344 - 'default_task_status' => 'closed',
345 - 'is_template' => false
346 - ];
347 - }
348 -
349 649 $stage = Stage::create($stageToPush);
350 650 if($index == 0){
351 651 $firstStage = $stage;
352 652 }
@@ -360,9 +660,9 @@
360 660 }
361 661
362 662 public function stagesByBoardId($boardId)
363 663 {
364 - return Stage::where('board_id', $boardId)->whereNull('archived_at')->get();
664 + return Stage::where('board_id', $boardId)->whereNull('archived_at')->orderBy('position', 'asc')->get();
365 665 }
366 666
367 667
368 668
@@ -372,9 +672,9 @@
372 672 $orderOptions = ['ASC', 'DESC'];
373 673
374 674 // Validate order and orderBy parameters
375 675 if (!in_array($order, $sortOptions) || !in_array($orderBy, $orderOptions)) {
376 - throw new \Exception(__('Invalid sort or orderBy parameter', 'fluent-boards'));
676 + throw new \Exception(esc_html__('Invalid sort or orderBy parameter', 'fluent-boards'));
377 677 }
378 678
379 679 $tasksQuery = Task::where('stage_id', $stage_id)
380 680 ->whereNull('parent_id')
@@ -383,9 +683,9 @@
383 683
384 684 // Apply ordering based on the specified order and orderBy
385 685 switch ($order) {
386 686 case 'priority':
387 - $tasksQuery->orderByRaw("FIELD(priority, 'High', 'Medium', 'Low') {$orderBy}");
687 + $tasksQuery->orderByRaw("FIELD(priority, 'urgent', 'high', 'medium', 'low') {$orderBy}");
388 688 break;
389 689
390 690 case 'due_at':
391 691 if ($orderBy === 'ASC') {
@@ -419,31 +719,66 @@
419 719 });
420 720 return $tasks;
421 721 }
422 722
423 - public function checkStageEditable($stages, $toBeUpdatedStage)
723 +
724 + public function updateStage($updatedStage, $board_id, $oldStage)
424 725 {
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 - }
726 + $stageBeforeUpdate = clone $oldStage;
727 + $oldStage->title = sanitize_text_field(Arr::get($updatedStage, 'title'));
728 + $oldStage->bg_color = sanitize_text_field(Arr::get($updatedStage, 'cover_bg'));
729 + $oldStage->save();
730 + do_action('fluent_boards/stage_updated', $board_id, $updatedStage, $stageBeforeUpdate);
731 + return $oldStage;
732 + }
733 +
734 + public function setDefaultAssignees($stage_id, $assignees)
735 + {
736 + $stage = Stage::findOrFail($stage_id);
737 + if ($stage) {
738 + $settings = $stage->settings;
739 + $settings['default_task_assignees'] = $assignees;
740 + $stage->settings = $settings;
741 + $stage->save();
742 +
743 + do_action('fluent_boards/default_assignees_updated', $stage, $assignees);
744 +
745 + return $stage;
435 746 }
747 + }
436 748
437 - return $stageEditable;
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 + }
438 762 }
439 763
440 - public function updateStage($updatedStage, $board_id, $oldStage)
764 + public function getAllParentAndSubTasksOfStages($stageIds)
441 765 {
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;
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 }