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

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