PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.40
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.40
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 1.45 All 41 releases
fluent-boards / app / Http / Controllers / TaskController.php

TaskController.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 1.40, at app/Http/Controllers/TaskController.php

644 lines 22.1 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\Http\Controllers;
4
5 use DateTimeImmutable;
6 use FluentBoards\App\Models\Stage;
7 use FluentBoards\App\Models\Task;
8 use FluentBoards\App\Models\Board;
9 use FluentBoards\App\Services\CommentService;
10 use FluentBoards\App\Services\Constant;
11 use FluentBoards\App\Services\Helper;
12 use FluentBoards\App\Services\StageService;
13 use FluentBoards\App\Services\TaskService;
14 use FluentBoards\App\Services\NotificationService;
15 use FluentBoards\App\Services\UploadService;
16 use FluentBoards\Framework\Http\Request\Request;
17 use FluentBoards\App\Services\PermissionManager;
18 use FluentBoards\Framework\Support\Arr;
19 use FluentBoardsPro\App\Services\AttachmentService;
20
21 class TaskController extends Controller
22 {
23 private TaskService $taskService;
24
25 private NotificationService $notificationService;
26
27 public function __construct(TaskService $taskService, NotificationService $notificationService)
28 {
29
30 parent::__construct();
31 $this->taskService = $taskService;
32 $this->notificationService = $notificationService;
33 }
34
35 public function getTopTasksForBoards()
36 {
37 $userId = get_current_user_id();
38 $task_ids = PermissionManager::getTaskIdsWatchByUser($userId);
39 $tasksArray = $this->taskService->getTasksForBoards(['overdue', 'upcoming'], 6, $task_ids);
40
41 return [
42 'data' => $tasksArray,
43 ];
44 }
45
46 public function getTasksByBoard($board_id)
47 {
48 $board = Board::findOrFail($board_id);
49
50 // Get stage IDs
51 $stageIds = $this->getStageIdsByBoard($board_id);
52
53 // Fetch tasks for the board
54 $tasks = Task::with(['assignees', 'labels', 'watchers', 'taskCustomFields'])
55 ->where('board_id', $board_id)
56 ->whereNull('archived_at')
57 ->whereNull('parent_id')
58 ->whereIn('stage_id', $stageIds)
59 ->orderBy('due_at', 'ASC')
60 ->get();
61
62 // Process each task
63 $this->processTasks($tasks, $board);
64
65 return [
66 'tasks' => $tasks,
67 ];
68 }
69
70 public function getTasksByBoardStage($board_id)
71 {
72 $board = Board::findOrFail($board_id);
73
74 // Get stage IDs
75 $stageIds = $this->getStageIdsByBoard($board_id);
76
77 // Initialize tasks array
78 $tasks = [];
79
80 // Fetch and process tasks for each stage
81 foreach ($stageIds as $stageId) {
82 $stageTasks = Task::with(['assignees', 'labels', 'watchers', 'taskCustomFields'])
83 ->where('board_id', $board_id)
84 ->where('stage_id', $stageId)
85 ->whereNull('archived_at')
86 ->whereNull('parent_id')
87 ->orderBy('position', 'ASC')
88 ->limit(20)
89 ->get();
90
91 // Process each stage's tasks
92 $this->processTasks($stageTasks, $board);
93 $tasks = array_merge($tasks, $stageTasks->toArray()); // Merge with the main task list
94 }
95
96 return [
97 'tasks' => $tasks,
98 ];
99 }
100
101 /**
102 * Get Stage IDs by Board ID.
103 *
104 * @param int $board_id
105 * @return array
106 */
107 private function getStageIdsByBoard($board_id)
108 {
109 return Stage::where('board_id', $board_id)
110 ->whereNull('archived_at')
111 ->pluck('id')
112 ->toArray();
113 }
114
115 /**
116 * Process and append extra information for each task.
117 *
118 * @param \Illuminate\Database\Eloquent\Collection $tasks
119 * @param \App\Models\Board $board
120 */
121 private function processTasks($tasks, $board)
122 {
123 foreach ($tasks as $task) {
124 $task->isOverdue = $task->isOverdue();
125 $task->isUpcoming = $task->upcoming();
126 $task->contact = Helper::crm_contact($task->crm_contact_id); // Handle possible null contact
127 $task->is_watching = $task->isWatching();
128 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
129 $task->watchers = Helper::sanitizeUserCollections($task->watchers);
130 $task->notifications = $this->notificationService->getUnreadNotificationsOfTasks($task);
131
132 // If the board type is 'roadmap', calculate popularity
133 if ($board->type === 'roadmap') {
134 $task->popular = $task->getPopularCount();
135 }
136 }
137 }
138
139
140 public function create(Request $request, $board_id)
141 {
142 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
143 'title' => 'required|string',
144 'board_id' => 'required|numeric',
145 'stage_id' => 'required|numeric',
146 'priority' => 'nullable|string',
147 'crm_contact_id' => 'nullable|numeric',
148 'is_template' => 'string',
149 ]);
150
151 try {
152 if ($taskData['board_id'] != $board_id) {
153 throw new \Exception(__('Board id is not valid', 'fluent-boards'));
154 }
155
156 $task = $this->taskService->createTask($taskData, $board_id);
157
158 return $this->sendSuccess([
159 'task' => $task,
160 'message' => __('Task has been successfully created', 'fluent-boards'),
161 'updatedTasks' => $this->taskService->getLastOneMinuteUpdatedTasks($task->board_id)
162 ], 201);
163 } catch (\Exception $e) {
164 return $this->sendError($e->getMessage(), 400);
165 }
166 }
167
168 public function find($board_id, $task_id)
169 {
170 try {
171
172 $stageService = new StageService();
173
174 $task = Task::findOrFail($task_id);
175
176 if (isset($task->parent_id)) {
177 $task = Task::findOrFail($task->parent_id);
178 }
179
180 if(!$task) {
181 throw new \Exception(__('Task not found', 'fluent-boards'));
182 }
183
184 if (defined('FLUENT_BOARDS_PRO')) {
185 $task->load(['attachments']);
186 }
187
188 $task->load(['board', 'stage', 'labels', 'assignees']);
189
190 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
191
192 $task->isOverdue = $task->isOverdue();
193 $task->contact = Task::lead_contact($task->crm_contact_id);
194 $task->board->stages = $stageService->stagesByBoardId($board_id);
195 $task->is_watching = $this->notificationService->isCurrentUserObservingTask($task);
196
197 $task = $this->taskService->loadNextStage($task);
198
199 if ($task->type == 'roadmap') {
200 $task->vote_statistics = $this->taskService->getIdeaVoteStatistics($task_id);
201 }
202
203 return [
204 'task' => $task
205 ];
206
207 } catch (\Exception $e ) {
208 return $this->sendError($e->getMessage(), 400);
209 }
210
211
212 }
213
214 public function getStageType(Request $request)
215 {
216 $stage = Stage::findOrFail($request->stage_id);
217
218 return [
219 'stage' => $stage,
220 ];
221 }
222
223 public function getActivities(Request $request, $board_id, $task_id)
224 {
225 $filter = $request->getSafe('filter');
226 $per_page = 15; // Apparently, let's use a fixed number of items per page.
227
228 return [
229 'activities' => $this->taskService->getActivities($task_id, $per_page, $filter)
230 ];
231
232 }
233
234 public function getArchivedTasks(Request $request, $board_id)
235 {
236 $tasks = $this->taskService->getArchivedTasks($request->all(), $board_id);
237
238 foreach ($tasks as $task) {
239 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
240 }
241
242 return [
243 'tasks' => $tasks
244 ];
245 }
246
247 public function updateTaskProperties(Request $request, $board_id, $task_id)
248 {
249 $col = $request->getSafe('property', 'sanitize_text_field');
250 $value = $request->get('value');
251
252 $validatedData = $this->updateTaskPropValidationAndSanitation($col, $value);
253 $task = Task::with(['board', 'labels', 'assignees'])->findOrFail($task_id);
254
255 if ($task->parent_id && !$task->board_id) {
256 $task->board_id = $board_id;
257 $task->save();
258 }
259
260 $task = $this->taskService->updateTaskProperty($col, $validatedData[$col], $task);
261 $task->isOverdue = $task->isOverdue();
262 $task->isUpcoming = $task->upcoming();
263 $task->contact = Helper::crm_contact($task->crm_contact_id);
264 $task->is_watching = $task->isWatching();
265 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
266
267 // A recent update to a task might impact other tasks on the board.
268 $updatedTasks = $this->taskService->getLastOneMinuteUpdatedTasks($board_id);
269
270 return [
271 'message' => __('Task has been updated', 'fluent-boards'),
272 'task' => $task,
273 'updatedTasks' => $updatedTasks
274 ];
275 }
276
277 public function updateTaskDates(Request $request, $board_id, $task_id)
278 {
279 $task = Task::findOrFail($task_id);
280
281 $startAt = $request->getSafe('started_at', 'sanitize_text_field', NULL);
282 $dueAt = $request->getSafe('due_at', 'sanitize_text_field', NULL);
283
284 if ($startAt && $dueAt) {
285 if (strtotime($startAt) > strtotime($dueAt)) {
286 $startAt = gmdate('Y-m-d 00:00:00', strtotime($dueAt));
287 }
288 }
289
290 $task = $this->taskService->updateTaskProperty('started_at', $startAt, $task);
291 $task = $this->taskService->updateTaskProperty('due_at', $dueAt, $task);
292
293 return [
294 'task' => $task,
295 'message' => __('Dates has been updated', 'fluent-boards'),
296 'updatedTasks' => $this->taskService->getLastOneMinuteUpdatedTasks($board_id),
297 ];
298 }
299
300 public function updateTaskCoverPhoto(Request $request, $board_id, $task_id)
301 {
302 $imagePath = $request->thumbnail;
303 $task = $this->taskService->taskCoverPhotoUpdate($task_id, $imagePath);
304
305 return [
306 'message' => __('Task cover photo has been updated', 'fluent-boards'),
307 'task' => $task,
308 ];
309
310 }
311
312 public function taskStatusUpdate(Request $request, $board_id, $task_id)
313 {
314 return [
315 'message' => __('Task status has been updated', 'fluent-boards'),
316 'task' => $this->taskService->taskStatusUpdate($task_id, $request->integrationType),
317 ];
318 }
319
320 public function deleteTask($board_id, $task_id)
321 {
322 $task = Task::findOrFail($task_id);
323 $options = null;
324 //if we need to do something before a task is deleted
325 do_action('fluent_boards/before_task_deleted', $task, $options);
326
327 $this->taskService->deleteTask($task);
328
329 return [
330 'updatedTasks' => $this->taskService->getLastOneMinuteUpdatedTasks($board_id),
331 'message' => __('Task has been deleted', 'fluent-boards'),
332 ];
333 }
334
335 private function taskSanitizeAndValidate($data, array $rules = [])
336 {
337 $data = Helper::sanitizeTask($data);
338
339 return $this->validate($data, $rules);
340 }
341
342 private function updateTaskPropValidationAndSanitation($col, $value)
343 {
344 $rules = [
345 'title' => 'required|string',
346 'board_id' => 'required',
347 'parent_id' => 'required',
348 'crm_contact_id' => 'nullable',
349 'type' => 'nullable|string',
350 'status' => 'nullable|string',
351 'stage_id' => 'required',
352 'reminder_type' => 'nullable|string',
353 'priority' => 'nullable|string',
354 'lead_value' => 'nullable|numeric|between:0,9999999.99',
355 'remind_at' => 'nullable|string',
356 'scope' => 'nullable|string',
357 'source' => 'nullable|string',
358 'description' => 'nullable|string',
359 'due_at' => 'nullable|string',
360 'started_at' => 'nullable|string',
361 'start_at' => 'nullable|string',
362 'log_minutes' => 'nullable|integer|unsigned',
363 'last_completed' => 'nullable|date',
364 'assignees' => 'nullable|integer',
365 'archived_at' => 'nullable|string',
366 'is_watching' => 'nullable|string',
367 'is_template' => 'string',
368 'last_completed_at' => 'nullable',
369 'settings' => 'nullable|array',
370 ];
371 if (array_key_exists($col, $rules)) {
372 $rule = $rules[$col];
373 if ('assignees' == $col && is_array($value)) {
374 $sanitizedAndValidatedValue = [];
375 foreach ($value as $val) {
376 $sanitizeData = Helper::sanitizeTask([$col => $val]);
377 $validatedData = $this->validate($sanitizeData, [
378 $col => $rule,
379 ]);
380 array_push($sanitizedAndValidatedValue, $validatedData[$col]);
381 }
382
383 return [$col => $sanitizedAndValidatedValue];
384 }
385 $data = Helper::sanitizeTask([$col => $value]);
386
387 return $this->validate($data, [
388 $col => $rule,
389 ]);
390 }
391 }
392
393 public function getLabelsByTask($task_id)
394 {
395 $labels = $this->taskService->getLabelsByTask($task_id);
396
397 return $this->sendSuccess([
398 'labels' => $labels,
399 ], 200);
400 }
401
402 public function getStageByTask($task_id)
403 {
404 $stage = $this->taskService->getStageByTask($task_id);
405
406 return [
407 'stage' => $stage,
408 ];
409 }
410
411 public function assignYourselfInTask($board_id, $task_id)
412 {
413 $task = $this->taskService->assignYourselfInTask($board_id, $task_id);
414 $task->is_watching = $task->isWatching();
415
416 return [
417 'task' => $task,
418 ];
419 }
420
421 public function detachYourselfFromTask($board_id, $task_id)
422 {
423 $task = $this->taskService->detachYourselfFromTask($board_id, $task_id);
424 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
425 $task->is_watching = $task->isWatching();
426
427 return [
428 'task' => $task,
429 ];
430 }
431
432 private function taskMetaSanitizeAndValidate($data, array $rules = [])
433 {
434 $data = Helper::sanitizeTaskMeta($data);
435
436 return $this->validate($data, $rules);
437 }
438
439 public function moveTaskToNextStage($board_id, $task_id)
440 {
441 $task = $this->taskService->moveTaskToNextStage($task_id);
442
443 return [
444 'task' => $task
445 ];
446 }
447
448 /**
449 * @throws \Exception
450 */
451 public function moveTask(Request $request, $board_id, $task_id)
452 {
453 $task = Task::findOrFail($task_id);
454 $oldStageId = $task->stage_id;
455 $newStageId = $request->getSafe('newStageId', 'intval');
456 $newIndex = $request->getSafe('newIndex', 'intval');
457 $newBoardId = $request->getSafe('newBoardId', 'intval');
458
459 if ((!is_numeric($newStageId) || $newStageId == 0)) {
460 throw new \Exception(__('Invalid Stage', 'fluent-boards'));
461 }
462 if ((!is_numeric($newIndex) || $newIndex == 0)) {
463 throw new \Exception(__('Invalid Value', 'fluent-boards'));
464 }
465 if ($newBoardId) {
466 if ((!is_numeric($newBoardId) || $newBoardId == 0)) {
467 throw new \Exception(__('Invalid Board', 'fluent-boards'));
468 }
469 $task = $this->taskService->changeBoardByTask($task, $newBoardId);
470 }
471
472 $task->stage_id = $newStageId;
473 $task = $task->moveToNewPosition($newIndex);
474
475 if ($oldStageId != $newStageId) {
476
477 $this->taskService->manageDefaultAssignees($task, $newStageId);
478
479 $defaultPosition = $task->stage->defaultTaskStatus();
480
481 if ($defaultPosition == 'closed' && $task->status != 'closed') {
482 $task = $task->close();
483 }
484
485 // do_action('fluent_boards/task_moved_to_new_stage', $task, $oldStageId);
486
487 do_action('fluent_boards/task_stage_updated', $task, $oldStageId);
488
489 $usersToSendEmail = $this->notificationService->filterAssigneeToSendEmail($task->id, Constant::BOARD_EMAIL_STAGE_CHANGE);
490 $this->taskService->sendMailAfterTaskModify('stage_change', $usersToSendEmail, $task->id);
491 }
492
493 do_action('fluent_boards/task_updated', $task, 'position');
494
495 $updatedTasks = $this->taskService->getLastOneMinuteUpdatedTasks($task->board_id, $request->get('last_boards_updated'));
496
497 return [
498 'new_position' => $task,
499 'message' => __('Task has been updated', 'fluent-boards'),
500 'task' => $task,
501 'updatedTasks' => $updatedTasks,
502 'last_updated' => current_time('mysql')
503 ];
504 }
505
506 /**
507 * Get comments and activities for a task, merged into a single array, sorted by creation date, and paginated.
508 *
509 * @param Request $request The HTTP request instance.
510 * @param int $board_id The ID of the board.
511 * @param int $task_id The ID of the task.
512 * @return \WP_REST_Response The response containing paginated comments and activities, total count, current page, and items per page.
513 */
514 public function getCommentsAndActivities( Request $request, $board_id, $task_id)
515 {
516 try {
517 // Pagination parameters
518 $page = $request->get('page', 1);
519 $perPage = $request->get('per_page', 10);
520 $filter = $request->get('filter', 'newest'); // Filter for comments and activities
521
522 $commentsAndActivities = $this->taskService->getCommentsAndActivities($task_id, $perPage, $page, $filter);
523 // Return the response with the task, paginated comments and activities, total count, current page, and items per page
524 return $this->sendSuccess([
525 'comments_and_activities' => $commentsAndActivities,
526 ]);
527 } catch (\Exception $e) {
528 return $this->sendError($e->getMessage(), 500);
529 }
530 }
531
532 public function sendMailAfterStageChange($usersToSendEmail, $taskId)
533 {
534 $current_user_id = get_current_user_id();
535
536 /* this will run in background as soon as possible */
537 /* sending Model or Model Instance won't work here */
538 as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_stage_change', [$taskId, $usersToSendEmail, $current_user_id], 'fluent-boards');
539 }
540 public function getAssociatedTasks($associated_id)
541 {
542 return [
543 'tasks' => $this->taskService->getAssociatedTasks($associated_id)
544 ];
545 }
546
547 /**
548 * @param Request $request
549 * @param $board_id
550 * @param $task_id
551 * @return \WP_REST_Response
552 */
553 public function uploadMediaFileFromWpEditor(Request $request, $board_id, $task_id)
554 {
555 try {
556
557
558 $file = Arr::get($request->files(), 'file')->toArray();
559 (new \FluentBoards\App\Services\UploadService)->validateFile($file);
560
561 $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
562
563 $fileData = $uploadInfo[0];
564 $fileUploadedData = $this->taskService->uploadMediaFileFromWpEditor($task_id, $fileData, Constant::TASK_DESCRIPTION);
565 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
566 $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
567 $fileUploadedData['driver'] = $mediaData['driver'];
568 $fileUploadedData['file_path'] = $mediaData['file_path'];
569 $fileUploadedData['full_url'] = $mediaData['full_url'];
570 $fileUploadedData->save();
571 }
572 $fileUploadedData['public_url'] = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
573
574 return $this->sendSuccess([
575 'message' => __('Image has been uploaded', 'fluent-boards'),
576 'file' => $fileUploadedData
577 ], 200);
578
579
580 } catch (\Exception $e) {
581 return $this->sendError($e->getMessage(), 400);
582 }
583 }
584
585
586 public function handleTaskCoverImageUpload(Request $request, $board_id, $task_id)
587 {
588 try {
589
590 $file = Arr::get($request->files(), 'file')->toArray();
591 (new \FluentBoards\App\Services\UploadService)->validateFile($file);
592
593 $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
594
595 $fileData = $uploadInfo[0];
596 $fileUploadedData = $this->taskService->uploadMediaFileFromWpEditor($task_id, $fileData, Constant::TASK_DESCRIPTION);
597 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
598 $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
599 $fileUploadedData['driver'] = $mediaData['driver'];
600 $fileUploadedData['file_path'] = $mediaData['file_path'];
601 $fileUploadedData['full_url'] = $mediaData['full_url'];
602 $fileUploadedData->save();
603 }
604
605 $task = Task::find($task_id);
606 $settings = $task->settings;
607 $this->taskService->deleteTaskCoverImage($settings);
608 $publicUrl = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
609 $settings['cover'] = [
610 'imageId' => $fileUploadedData['id'],
611 'backgroundImage' => $publicUrl,
612 ];
613 $task->settings = $settings;
614 $task->save();
615
616 return $this->sendSuccess([
617 'message' => __('Image has been uploaded', 'fluent-boards'),
618 'public_url' => $publicUrl
619 ], 200);
620
621
622 } catch (\Exception $e) {
623 return $this->sendError($e->getMessage(), 400);
624 }
625 }
626 public function removeTaskCover($board_id, $task_id)
627 {
628 try {
629 $task = Task::find($task_id);
630 $settings = $task->settings;
631 $this->taskService->deleteTaskCoverImage($settings);
632 unset($settings['cover']);
633 $task->settings = $settings;
634 $task->save();
635 return $this->sendSuccess([
636 'task' => $task,
637 'message' => __('Task Cover removed successfully', 'fluent-boards'),
638 ]);
639 } catch (\Exception $e) {
640 return $this->sendError($e->getMessage(), 400);
641 }
642 }
643 }
644