PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.95.3
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.95.3
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
fluent-boards / app / Services / StageService.php

StageService.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 1.95.3, at app/Services/StageService.php

608 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Services;
4
5 use FluentBoards\App\Models\Task;
6 use FluentBoards\App\Models\Board;
7 use FluentBoards\App\Models\Stage;
8 use FluentBoards\App\Models\TaskMeta;
9 use FluentBoards\App\Models\Relation;
10 use FluentBoards\App\App;
11 use FluentBoards\Framework\Http\Request\Request;
12 use FluentBoards\Framework\Support\Arr;
13 use FluentBoards\App\Services\Constant;
14 use FluentBoards\App\Services\AttachmentFileService;
15 use FluentBoards\App\Services\PermissionManager;
16
17 class StageService
18 {
19 public function createDefaultStages($board)
20 {
21 $stages = $this->defaultStages($board);
22 foreach ($stages as $stage) {
23 Stage::create($stage);
24 }
25 }
26
27 public function defaultStages($board)
28 {
29 return $this->defaultStagesForTodos($board->id);
30 }
31
32 public function defaultStagesForTodos($boardId)
33 {
34 return [
35 [
36 'board_id' => $boardId,
37 'title' => 'Open',
38 'position' => 1,
39 'slug' => 'open',
40 'settings' => [
41 'default_task_status' => 'open'
42 ]
43 ],
44 [
45 'board_id' => $boardId,
46 'title' => 'In Progress',
47 'position' => 2,
48 'slug' => 'in-progress',
49 'settings' => [
50 'default_task_status' => 'open'
51 ]
52 ],
53 [
54 'board_id' => $boardId,
55 'title' => 'Completed',
56 'position' => 3,
57 'slug' => 'completed',
58 'settings' => [
59 'default_task_status' => 'closed'
60 ]
61 ]
62 ];
63 }
64 public function updateStageProperty($col, $value, $stageId)
65 {
66 $stage = Stage::findOrFail($stageId);
67
68 if ('title' == $col) {
69 $stage = $this->updateTitle($value, $stage);
70 } elseif ('status' == $col) {
71 $stage = $this->updateStatus($value, $stage);
72 } elseif ('color' == $col) {
73 $stage = $this->updateColor($value, $stage);
74 } elseif ('bg_color' == $col) {
75 $stage = $this->updateBackgroundColor($value, $stage);
76 } elseif ('archived_at' == $col) {
77 $stage = $this->updateArchivedAt($value, $stage);
78 }
79 return $stage;
80 }
81
82 public function getLastOneMinuteUpdatedStages($boardId, $lastUpdated = null, $includeArchived = true)
83 {
84 if (!$lastUpdated) {
85 $oneMinuteAgoTimestamp = current_time('timestamp') - 60;
86 $lastUpdated = date_i18n('Y-m-d H:i:s', $oneMinuteAgoTimestamp);
87 }
88
89 $stagesQuery = Stage::where('board_id', $boardId)
90 ->where('updated_at', '>=', $lastUpdated);
91
92 if (!$includeArchived) {
93 $stagesQuery->whereNull('archived_at');
94 }
95
96 return $stagesQuery->get();
97 }
98
99 private function updateTitle($value, $stage)
100 {
101 $stage->title = $value;
102 $stage->save();
103 return $stage;
104 }
105
106 private function updateStatus($value, $stage)
107 {
108 $oldSettings = $stage->settings;
109 $oldSettings['default_task_status'] = $value;
110 $stage->settings = $oldSettings;
111 $stage->save();
112 return $stage;
113 }
114
115 private function updateColor($value, $stage)
116 {
117 $stage->color = $value;
118 $stage->save();
119 return $stage;
120 }
121
122 private function updateBackgroundColor($value, $stage)
123 {
124 $stage->bg_color = $value;
125 $stage->save();
126 return $stage;
127 }
128
129 private function updateArchivedAt($value, $stage)
130 {
131 $stage->archived_at = $value;
132 $stage->save();
133 return $stage;
134 }
135
136 public function createStage($stageData, $boardId)
137 {
138 $stage = new Stage();
139 $stage->board_id = $boardId;
140 $stage->title = $stageData['title'];
141 $stage->settings = [
142 'default_task_status' => Arr::get($stageData, 'status') ?? 'open',
143 'default_task_assignees' => []
144 ];
145 $providerPosition = Arr::get($stageData, 'position');
146 $lastStagePosition = $this->getLastPositionOfStagesOfBoard($boardId);
147 $stage->position = $lastStagePosition ? $lastStagePosition->position + 1 : 1;
148 $stage->save();
149 if($providerPosition){
150 $stage->moveToNewPosition($providerPosition);
151 }
152 return $stage;
153 }
154
155 public function getLastPositionOfStagesOfBoard($boardId)
156 {
157 // return last position of stages of board
158 return Stage::where('board_id', $boardId)
159 ->whereNull('archived_at')
160 ->orderBy('position', 'desc')
161 ->first();
162 }
163
164 protected function moveOtherStages($stage)
165 {
166
167 $stages = Stage::where('board_id', $stage->board_id)
168 ->where('position', '>=', $stage->position)
169 ->whereNotIn('id', [$stage->id])
170 ->whereNull('archived_at')->get();
171
172 foreach ($stages as $stage) {
173 $stage->position = $stage->position + 1;
174 $stage->save();
175 }
176 }
177
178 public function copyStagesOfBoard($board, $fromBoardId, $isWithTemplates='no')
179 {
180 $stages = Stage::where('board_id', $fromBoardId)->where('type', 'stage')->whereNull('archived_at')->orderBy('position', 'asc')->get();
181 $stageMapForCopyingTask = array();
182 foreach($stages as $key => $stage)
183 {
184 $stageToSave = array();
185 $stageToSave['title'] = $stage['title'];
186 $stageToSave['board_id'] = $board->id;
187 $stageToSave['slug'] = str_replace(' ', '-', strtolower($stage['title']));
188 $stageToSave['type'] = 'stage';
189 $stageToSave['position'] = $key + 1;
190 $stageToSave['bg_color'] = $stage['bg_color'];
191 $stageToSave['settings'] = [
192 'default_task_status' => $stage->settings['default_task_status']
193 ];
194 if (!empty($stage->settings['is_template']) && $isWithTemplates == 'yes') {
195 $stageToSave['settings']['is_template'] = $stage->settings['is_template'];
196 }
197 $newStage = Stage::create($stageToSave);
198 $stageMapForCopyingTask[$stage['id']] = $newStage->id;
199 }
200 return $stageMapForCopyingTask;
201 }
202
203 public function importStagesFromBoard($board_id, $selectedStages, $position = null)
204 {
205 $board_id = absint($board_id);
206 $selectedStages = array_values(array_unique(array_filter(array_map('absint', (array) $selectedStages))));
207
208 if (!$selectedStages) {
209 return;
210 }
211
212 $targetStages = Stage::whereIn('id', $selectedStages)
213 ->whereNull('archived_at')
214 ->get();
215
216 if (count($targetStages) !== count($selectedStages)) {
217 throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
218 }
219
220 $sourceBoardIds = array_values(array_unique(array_map('absint', $targetStages->pluck('board_id')->toArray())));
221
222 if (in_array(0, $sourceBoardIds, true)) {
223 throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
224 }
225
226 $userId = get_current_user_id();
227
228 if (!$userId) {
229 throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
230 }
231
232 if (!PermissionManager::isAdmin($userId)) {
233 $allowedBoardIds = Relation::where('foreign_id', $userId)
234 ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
235 ->whereIn('object_id', $sourceBoardIds)
236 ->pluck('object_id')
237 ->toArray();
238
239 $allowedBoardIds = array_map('intval', $allowedBoardIds);
240 $unauthorizedBoardIds = array_filter($sourceBoardIds, function ($sourceBoardId) use ($allowedBoardIds) {
241 return !in_array($sourceBoardId, $allowedBoardIds, true);
242 });
243
244 if ($unauthorizedBoardIds) {
245 throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
246 }
247 }
248
249 $stageMapForCopyingTask = array();
250 $stageIdsToCopy = array();
251
252 $numberOfStages = Stage::where('board_id', $board_id)->whereNull('archived_at')->count();
253
254 foreach($targetStages as $key => $stage)
255 {
256 $stageToSave = array();
257 $stageToSave['title'] = $stage['title'] . ' - imported';
258 $stageToSave['board_id'] = $board_id;
259 $stageToSave['slug'] = str_replace(' ', '-', strtolower($stage['title']));
260 $stageToSave['type'] = 'stage';
261 $stageToSave['position'] = $numberOfStages + $key + 1;
262 $stageToSave['settings'] = [
263 'default_task_status' => $stage->settings['default_task_status']
264 ];
265 $newStage = Stage::create($stageToSave);
266 if($position) {
267 $newStage->moveToNewPosition( (int) $position + $key );
268 }
269 $stageMapForCopyingTask[$stage['id']] = $newStage->id;
270 $stageIdsToCopy[] = $stage['id'];
271 }
272
273 $this->importTasks($board_id, $stageMapForCopyingTask, $stageIdsToCopy);
274 }
275
276 public function importTasks($boardId, $stageMapper, $stageIds)
277 {
278 $tasksToImport = $this->getAllParentAndSubTasksOfStages($stageIds);
279 $subtaskGroupChildRelations = [];
280 $taskIdsToImport = $tasksToImport->pluck('id')->toArray();
281
282 if ($taskIdsToImport) {
283 foreach (TaskMeta::where('key', Constant::SUBTASK_GROUP_CHILD)
284 ->whereIn('task_id', $taskIdsToImport)
285 ->get() as $relation) {
286 $subtaskGroupChildRelations[$relation->task_id] = $relation;
287 }
288 }
289 $taskMap = [];
290 $subtaskGroupMap = [];
291 $attachmentFileService = new AttachmentFileService();
292 $dbInstance = App::getInstance('db');
293
294 $dbInstance->beginTransaction();
295
296 try {
297 foreach($tasksToImport as $task)
298 {
299 $newTask = array();
300 $newTask['title'] = $task->title;
301 $newTask['parent_id'] = $task->parent_id ? $taskMap[$task->parent_id] : null;
302 $newTask['description'] = $task->description;
303 $newTask['board_id'] = $boardId;
304 $newTask['stage_id'] = $stageMapper[$task->stage_id];
305 $newTask['status'] = $task->status;
306 $newTask['priority'] = $task->priority;
307 $newTask['position'] = $task->position;
308 $newTask['due_at'] = $task->due_at;
309 $backgroundColor = $task->settings['cover']['backgroundColor'] ?? '';
310 $newTask['settings'] = [
311 'cover' => [
312 'backgroundColor' => $backgroundColor,
313 ]
314 ];
315 $newTask = Task::create($newTask);
316 $attachmentFileService->cloneTaskFilesToBoard($task, $newTask, $boardId);
317
318 if (!$task->parent_id) {
319 $taskMap[$task->id] = $newTask->id;
320 //group mapping
321 $subtaskGroupMap = (new TaskService())->copySubtaskGroup($task, $newTask, $subtaskGroupMap);
322 } else {
323 $groupRelationOfTask = $subtaskGroupChildRelations[$task->id] ?? null;
324
325 if ($groupRelationOfTask && $subtaskGroupMap[$groupRelationOfTask->value]) {
326 TaskMeta::create([
327 'task_id' => $newTask->id,
328 'key' => Constant::SUBTASK_GROUP_CHILD,
329 'value' => $subtaskGroupMap[$groupRelationOfTask->value]
330 ]);
331 }
332 }
333 }
334
335 //update task count of board
336 $totalTasks = sizeof($tasksToImport);
337 $board = Board::findOrFail($boardId);
338 $settings = $board->settings ?? [];
339
340 if (isset($settings['tasks_count'])) {
341 $settings['tasks_count'] += $totalTasks;
342 } else {
343 $settings['tasks_count'] = $totalTasks;
344 }
345 $board->settings = $settings;
346 $board->save();
347
348 $dbInstance->commit();
349 } catch (\Exception $e) {
350 $dbInstance->rollBack();
351 $attachmentFileService->rollbackCreatedFiles();
352 throw $e;
353 }
354 }
355
356 public function updateStageTemplate($stage_id)
357 {
358 $stage = Stage::findOrFail($stage_id);
359 $stageSettings = $stage->settings;
360 if($stageSettings && array_key_exists('is_template', $stageSettings))
361 {
362 $currentlyIsTemplate = $stageSettings['is_template'];
363 if($currentlyIsTemplate){
364 $stageSettings['is_template'] = false;
365 $stage->settings = $stageSettings;
366 }else{
367 $stageSettings['is_template'] = true;
368 $stage->settings = $stageSettings;
369 }
370 }else {
371 if(!$stageSettings) {
372 $stage->settings = [
373 'is_template' => true
374 ];
375 } else {
376 $stage->settings = array_merge($stage->settings, [
377 'is_template' => true
378 ]);
379 }
380
381 }
382 $stage->save();
383
384 return $stage;
385 }
386
387 public function moveAllTasks($oldStageId, $newStageId)
388 {
389 $tasks = Task::where('stage_id', $oldStageId)->whereNull('parent_id')->whereNull('archived_at')->get();
390
391 // get the last position available of that stage
392 $position = (new TaskService())->getLastPositionOfTasks($newStageId);
393
394 // update tasks stage and position
395 foreach ($tasks as $key => $task) {
396 // Clean up archived_by_stage meta when moving to different stage
397 TaskMeta::where('task_id', $task->id)
398 ->where('key', Constant::META_KEY_ARCHIVED_BY_STAGE)
399 ->delete();
400
401 $task->stage_id = $newStageId;
402 $task->position = $position + $key;
403 $task->save();
404 }
405 return $tasks;
406 }
407 public function archiveAllTasksInStage($stage_id, $boardId = null)
408 {
409 $tasksQuery = Task::where('stage_id', absint($stage_id))
410 ->whereNull('parent_id')
411 ->whereNull('archived_at');
412
413 if ($boardId) {
414 $tasksQuery->where('board_id', absint($boardId));
415 }
416
417 $tasks = $tasksQuery->get();
418 foreach ($tasks as $task) {
419 $task->position = 0;
420 $task->archived_at = current_time('mysql');
421 $task->save();
422 do_action('fluent_boards/task_archived', $task);
423 }
424 return $tasks;
425 }
426
427 public function createRoadmapStages($board, $stagesData)
428 {
429 foreach ($stagesData as $index => $formStageData) {
430 $stage = new Stage();
431 $stage->board_id = $board->id;
432 $stage->title = $formStageData['title'];
433 $stage->slug = $formStageData['slug'];
434 $stage->position = $formStageData['position'] ? $formStageData['position'] : 1;
435 $stage->settings = $this->roadmapStageSetting($index);
436 $stage->save();
437 }
438 return $stagesData;
439 }
440
441 /*
442 * I have no idea what this function does, but I am doing it
443 * to make the code work
444 */
445 public function roadmapStageSetting($index)
446 {
447 return [
448 'is_public' => $index > 0 ? true : false,
449 'default_task_status' => 'open',
450 'is_template' => false,
451 ];
452 }
453
454
455 public function createStages($board, $stageData)
456 {
457 $firstStage = null;
458 foreach ($stageData as $index => $stage) {
459 $stageToPush = array();
460 $stageToPush['title'] = $stage['title'];
461 $stageToPush['board_id'] = $board->id;
462 $stageToPush['position'] = $index + 1;
463 $stageToPush['slug'] = $this->createSlug($stage['title']);
464
465 if (Arr::get($stage, 'title') == 'Completed') {
466 $stageToPush['settings'] = [
467 'default_task_status' => 'closed',
468 'is_template' => false
469 ];
470 }
471
472 $stage = Stage::create($stageToPush);
473 if($index == 0){
474 $firstStage = $stage;
475 }
476 }
477 return $firstStage;
478 }
479
480 private function createSlug($title)
481 {
482 return str_replace(' ', '-', strtolower($title));
483 }
484
485 public function stagesByBoardId($boardId)
486 {
487 return Stage::where('board_id', $boardId)->whereNull('archived_at')->orderBy('position', 'asc')->get();
488 }
489
490
491
492 public function sortStageTasks($order, $orderBy, $stage_id)
493 {
494 $sortOptions = ['priority', 'due_at', 'position', 'created_at', 'title'];
495 $orderOptions = ['ASC', 'DESC'];
496
497 // Validate order and orderBy parameters
498 if (!in_array($order, $sortOptions) || !in_array($orderBy, $orderOptions)) {
499 throw new \Exception(esc_html__('Invalid sort or orderBy parameter', 'fluent-boards'));
500 }
501
502 $tasksQuery = Task::where('stage_id', $stage_id)
503 ->whereNull('parent_id')
504 ->whereNull('archived_at')
505 ->with(['assignees', 'labels', 'watchers']);
506
507 // Apply ordering based on the specified order and orderBy
508 switch ($order) {
509 case 'priority':
510 $tasksQuery->orderByRaw("FIELD(priority, 'High', 'Medium', 'Low') {$orderBy}");
511 break;
512
513 case 'due_at':
514 if ($orderBy === 'ASC') {
515 // Separate ordering for tasks with and without due dates
516 $tasksWithDueDate = (clone $tasksQuery)->whereNotNull('due_at')->orderBy('due_at')->get();
517 $tasksWithoutDueDate = (clone $tasksQuery)->whereNull('due_at')->get();
518 $tasks = $tasksWithDueDate->merge($tasksWithoutDueDate);
519 } else {
520 $tasksQuery->orderBy('due_at', $orderBy);
521 }
522 break;
523
524 default:
525 $tasksQuery->orderBy($order, $orderBy);
526 break;
527 }
528
529 // Fetch tasks if not already fetched
530 if (!isset($tasks)) {
531 $tasks = $tasksQuery->get();
532 }
533
534 // Update tasks with additional attributes
535 $tasks->each(function ($task, $key) {
536 $task->position = $key + 1;
537 $task->save();
538 $task->isOverdue = $task->isOverdue();
539 $task->isUpcoming = $task->upcoming();
540 $task->is_watching = $task->isWatching();
541 $task->contact = Task::lead_contact($task->crm_contact_id);
542 });
543 return $tasks;
544 }
545
546
547 public function updateStage($updatedStage, $board_id, $oldStage)
548 {
549 $stageBeforeUpdate = clone $oldStage;
550 $oldStage->title = sanitize_text_field(Arr::get($updatedStage, 'title'));
551 $oldStage->bg_color = sanitize_text_field(Arr::get($updatedStage, 'cover_bg'));
552 $oldStage->save();
553 do_action('fluent_boards/stage_updated', $board_id, $updatedStage, $stageBeforeUpdate);
554 return $oldStage;
555 }
556
557 public function setDefaultAssignees($stage_id, $assignees)
558 {
559 $stage = Stage::findOrFail($stage_id);
560 if ($stage) {
561 $settings = $stage->settings;
562 $settings['default_task_assignees'] = $assignees;
563 $stage->settings = $settings;
564 $stage->save();
565
566 do_action('fluent_boards/default_assignees_updated', $stage, $assignees);
567
568 return $stage;
569 }
570 }
571
572 public function setDefaultWatchers($stage_id, $watchers)
573 {
574 $stage = Stage::findOrFail($stage_id);
575 if ($stage) {
576 $settings = $stage->settings;
577 $settings['default_task_watchers'] = $watchers;
578 $stage->settings = $settings;
579 $stage->save();
580
581 do_action('fluent_boards/default_watchers_updated', $stage, $watchers);
582
583 return $stage;
584 }
585 }
586
587 public function getAllParentAndSubTasksOfStages($stageIds)
588 {
589 // Fetch parent task IDs for the given stage_ids
590 $parentTaskIds = Task::whereIn('stage_id', $stageIds)
591 ->whereNull('archived_at')
592 ->whereNull('parent_id')
593 ->pluck('id');
594
595 // Fetch parent tasks and subtasks in a single query
596 $tasks = Task::whereIn('stage_id', $stageIds)
597 ->whereNull('parent_id')
598 ->whereNull('archived_at')
599 ->get();
600
601 $subtasks = Task::whereIn('parent_id', $parentTaskIds)
602 ->get();
603
604 // Combine parent tasks and subtasks into a single collection
605 return $tasks->merge($subtasks);
606 }
607 }
608