PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.35
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.35
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.35, at app/Http/Controllers/TaskController.php

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