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

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

1,198 lines 44.4 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\Meta;
7 use FluentBoards\App\Models\Stage;
8 use FluentBoards\App\Models\Task;
9 use FluentBoards\App\Models\Board;
10 use FluentBoards\App\Models\TaskMeta;
11 use FluentBoards\App\Services\CommentService;
12 use FluentBoards\App\Services\Constant;
13 use FluentBoards\App\Services\Helper;
14 use FluentBoards\App\Services\StageService;
15 use FluentBoards\App\Services\TaskService;
16 use FluentBoards\App\Services\NotificationService;
17 use FluentBoards\App\Services\UploadService;
18 use FluentBoards\Framework\Http\Request\Request;
19 use FluentBoards\App\Services\PermissionManager;
20 use FluentBoards\Framework\Support\Arr;
21 use FluentBoardsPro\App\Services\AttachmentService;
22 use FluentCrm\App\Models\Subscriber;
23
24 class TaskController extends Controller
25 {
26 private TaskService $taskService;
27
28 private NotificationService $notificationService;
29
30 public function __construct(TaskService $taskService, NotificationService $notificationService)
31 {
32
33 parent::__construct();
34 $this->taskService = $taskService;
35 $this->notificationService = $notificationService;
36 }
37
38 public function getTopTasksForBoards()
39 {
40 $userId = get_current_user_id();
41 $task_ids = PermissionManager::getTaskIdsWatchByUser($userId);
42 $tasksArray = $this->taskService->getTasksForBoards(['overdue', 'upcoming'], 6, $task_ids);
43
44 return [
45 'data' => $tasksArray,
46 ];
47 }
48
49 public function getTasksByBoard($board_id)
50 {
51 $board_id = absint($board_id);
52 $board = Board::findOrFail($board_id);
53
54 // Get stage IDs
55 $stageIds = $this->getStageIdsByBoard($board_id);
56
57 // Fetch tasks for the board
58 $tasks = Task::with(['assignees', 'labels', 'watchers', 'taskCustomFields'])
59 ->where('board_id', $board_id)
60 ->whereNull('archived_at')
61 ->whereNull('parent_id')
62 ->whereIn('stage_id', $stageIds)
63 ->orderBy('due_at', 'ASC')
64 ->get();
65
66 // Process each task
67 $this->processTasks($tasks, $board);
68
69 if ($board->type === 'roadmap') {
70 foreach ($tasks as $task) {
71 $task->vote_statistics = $this->taskService->getIdeaVoteStatistics($task->id);
72 }
73 }
74
75 return [
76 'tasks' => $tasks,
77 ];
78 }
79
80 public function getTasksByBoardStage($board_id)
81 {
82 $board_id = absint($board_id);
83 $board = Board::findOrFail($board_id);
84
85 // Get stage IDs
86 $stageIds = $this->getStageIdsByBoard($board_id);
87
88 // Initialize tasks array
89 $tasks = [];
90
91 // Fetch and process tasks for each stage
92 foreach ($stageIds as $stageId) {
93 $stageTasks = Task::with(['assignees', 'labels', 'watchers', 'taskCustomFields'])
94 ->where('board_id', $board_id)
95 ->where('stage_id', $stageId)
96 ->whereNull('archived_at')
97 ->whereNull('parent_id')
98 ->orderBy('position', 'ASC')
99 ->limit(20)
100 ->get();
101
102 // Process each stage's tasks
103 $this->processTasks($stageTasks, $board);
104 $tasks = array_merge($tasks, $stageTasks->toArray()); // Merge with the main task list
105 }
106
107 return [
108 'tasks' => $tasks,
109 ];
110 }
111
112 /**
113 * Get Stage IDs by Board ID.
114 *
115 * @param int $board_id
116 * @return array
117 */
118 private function getStageIdsByBoard($board_id)
119 {
120 return Stage::where('board_id', $board_id)
121 ->whereNull('archived_at')
122 ->pluck('id')
123 ->toArray();
124 }
125
126 /**
127 * Process and append extra information for each task.
128 *
129 * @param \Illuminate\Database\Eloquent\Collection $tasks
130 * @param \App\Models\Board $board
131 */
132 private function processTasks($tasks, $board)
133 {
134 foreach ($tasks as $task) {
135 $task->isOverdue = $task->isOverdue();
136 $task->isUpcoming = $task->upcoming();
137 $task->contact = Helper::crm_contact($task->crm_contact_id); // Handle possible null contact
138 $task->is_watching = $task->isWatching();
139 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
140 $task->watchers = Helper::sanitizeUserCollections($task->watchers);
141 $task->notifications = $this->notificationService->getUnreadNotificationsOfTasks($task);
142
143 // If the board type is 'roadmap', calculate popularity
144 if ($board->type === 'roadmap') {
145 $task->popular = $task->getPopularCount();
146 }
147 }
148 }
149
150
151 public function create(Request $request, $board_id)
152 {
153 $board_id = absint($board_id);
154 $taskData = $this->taskSanitizeAndValidate($request->getSafe('task'), [
155 'title' => 'required|string',
156 'board_id' => 'required|numeric',
157 'stage_id' => 'required|numeric',
158 'priority' => 'nullable|string',
159 'crm_contact_id' => 'nullable|numeric',
160 'is_template' => 'string',
161 ]);
162
163 try {
164 if ($taskData['board_id'] != $board_id) {
165 throw new \Exception(esc_html__('Board id is not valid', 'fluent-boards'));
166 }
167
168 $task = $this->taskService->createTask($taskData, $board_id);
169
170 return $this->sendSuccess([
171 'task' => $task,
172 'message' => __('Task has been successfully created', 'fluent-boards'),
173 'updatedTasks' => $this->taskService->getLastOneMinuteUpdatedTasks($task->board_id)
174 ], 201);
175 } catch (\Exception $e) {
176 return $this->sendError($e->getMessage(), 400);
177 }
178 }
179
180 public function find($board_id, $task_id)
181 {
182 $board_id = absint($board_id);
183 $task_id = absint($task_id);
184 try {
185
186 $stageService = new StageService();
187
188 $task = Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
189
190 if ($task->parent_id) {
191 $task = Task::where('board_id', $board_id)->where('id', $task->parent_id)->firstOrFail();
192 }
193
194 if(!$task) {
195 throw new \Exception(esc_html__('Task not found', 'fluent-boards'));
196 }
197
198 if (defined('FLUENT_BOARDS_PRO')) {
199 $task->load(['attachments']);
200 }
201
202 $task->load(['board', 'stage', 'labels', 'assignees','watchers']);
203
204 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
205
206 $task->isOverdue = $task->isOverdue();
207 $task->contact = Task::lead_contact($task->crm_contact_id);
208 $task->board->stages = $stageService->stagesByBoardId($board_id);
209 $task->is_watching = $this->notificationService->isCurrentUserObservingTask($task);
210
211 $task = $this->taskService->loadNextStage($task);
212
213 if ($task->type == 'roadmap') {
214 $task->vote_statistics = $this->taskService->getIdeaVoteStatistics($task_id);
215 }
216
217 return [
218 'task' => $task
219 ];
220
221 } catch (\Exception $e ) {
222 return $this->sendError($e->getMessage(), 400);
223 }
224
225
226 }
227
228 public function getStageType(Request $request)
229 {
230 $stage_id = $request->getSafe('stage_id', 'intval');
231 $stage = Stage::findOrFail($stage_id);
232
233 return [
234 'stage' => $stage,
235 ];
236 }
237
238 public function getActivities(Request $request, $board_id, $task_id)
239 {
240 $board_id = absint($board_id);
241 $task_id = absint($task_id);
242 Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
243 $filter = $request->getSafe('filter', 'sanitize_text_field');
244 $per_page = 15; // Apparently, let's use a fixed number of items per page.
245
246 return [
247 'activities' => $this->taskService->getActivities($task_id, $per_page, $filter)
248 ];
249
250 }
251
252 public function getArchivedTasks(Request $request, $board_id)
253 {
254 $board_id = absint($board_id);
255 // Sanitize request parameters before passing to service
256 $sanitizedParams = [
257 'per_page' => $request->getSafe('per_page', 'intval', 20),
258 'page' => $request->getSafe('page', 'intval', 1),
259 ];
260 $tasks = $this->taskService->getArchivedTasks($sanitizedParams, $board_id);
261
262 foreach ($tasks as $task) {
263 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
264 }
265
266 return [
267 'tasks' => $tasks
268 ];
269 }
270
271 public function bulkRestoreTasks(Request $request, $board_id)
272 {
273 $board_id = absint($board_id);
274 try {
275 $rawTaskIds = $request->getSafe('task_ids');
276 // Sanitize task_ids array to integers
277 $task_ids = [];
278 if (is_array($rawTaskIds)) {
279 $task_ids = array_filter(array_map('intval', $rawTaskIds));
280 }
281
282 if (empty($task_ids)) {
283 return $this->response->sendError('No task IDs provided', 400);
284 }
285
286 $tasks = Task::where('board_id', $board_id)
287 ->whereIn('id', $task_ids)
288 ->whereNotNull('archived_at')
289 ->get();
290
291 if ($tasks->isEmpty()) {
292 return $this->response->sendError('No archived tasks found with provided IDs', 404);
293 }
294
295 $restored_count = 0;
296 $failed_count = 0;
297 $failed_tasks = [];
298
299 foreach ($tasks as $task) {
300 try {
301 // Use TaskService to properly restore the task (same as single task restoration)
302 $this->taskService->updateTaskProperty('archived_at', null, $task);
303
304 // Prepare task for response (same as single task update)
305 $task->isOverdue = $task->isOverdue();
306 $task->isUpcoming = $task->upcoming();
307 $task->contact = Helper::crm_contact($task->crm_contact_id);
308 $task->is_watching = $task->isWatching();
309 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
310
311 $restored_count++;
312 } catch (\Exception $e) {
313 // Track failed tasks but continue processing others
314 $failed_count++;
315 $failed_tasks[] = [
316 'id' => $task->id,
317 'title' => $task->title,
318 'error' => $e->getMessage()
319 ];
320 }
321 }
322
323 // Get recently updated tasks (same as single task operations)
324 $recentlyUpdatedTasks = $this->taskService->getLastOneMinuteUpdatedTasks($board_id);
325
326 // Build response with detailed results
327 $response = [
328 'restored_count' => $restored_count,
329 'failed_count' => $failed_count,
330 'updatedTasks' => $recentlyUpdatedTasks
331 ];
332
333 if ($failed_count > 0) {
334 $response['failed_tasks'] = $failed_tasks;
335 if ($restored_count > 0) {
336 $response['message'] = $restored_count . ' ' . ($restored_count === 1 ? 'task' : 'tasks') . ' restored successfully, ' . $failed_count . ' ' . ($failed_count === 1 ? 'task' : 'tasks') . ' failed';
337 } else {
338 $response['message'] = 'Failed to restore ' . $failed_count . ' ' . ($failed_count === 1 ? 'task' : 'tasks');
339 }
340 } else {
341 $response['message'] = $restored_count . ' ' . ($restored_count === 1 ? 'task' : 'tasks') . ' restored successfully';
342 }
343
344 return $this->response->sendSuccess($response, 200);
345
346 } catch (\Exception $e) {
347 return $this->response->sendError($e->getMessage(), 500);
348 }
349 }
350
351 public function bulkDeleteTasks(Request $request, $board_id)
352 {
353 $board_id = absint($board_id);
354 try {
355 $rawTaskIds = $request->getSafe('task_ids');
356 // Sanitize task_ids array to integers
357 $task_ids = [];
358 if (is_array($rawTaskIds)) {
359 $task_ids = array_filter(array_map('intval', $rawTaskIds));
360 }
361
362 if (empty($task_ids)) {
363 return $this->response->sendError('No task IDs provided', 400);
364 }
365
366 $tasks = Task::where('board_id', $board_id)
367 ->whereIn('id', $task_ids)
368 ->get();
369
370 if ($tasks->isEmpty()) {
371 return $this->response->sendError('No tasks found with provided IDs', 404);
372 }
373
374 $deleted_count = 0;
375 $failed_count = 0;
376 $failed_tasks = [];
377 $options = null;
378
379 foreach ($tasks as $task) {
380 try {
381 // This handles all cleanup: subtasks, watchers, assignees, labels, notifications, attachments, etc.
382 $this->taskService->deleteTaskForBulk($task);
383 $deleted_count++;
384 } catch (\Exception $e) {
385 // Track failed tasks but continue processing others
386 $failed_count++;
387 $failed_tasks[] = [
388 'id' => $task->id,
389 'title' => $task->title,
390 'error' => $e->getMessage()
391 ];
392 }
393 }
394
395 // Get recently updated tasks (same as single task operations)
396 $recentlyUpdatedTasks = $this->taskService->getLastOneMinuteUpdatedTasks($board_id);
397
398 // Build response with detailed results
399 $response = [
400 'deleted_count' => $deleted_count,
401 'failed_count' => $failed_count,
402 'updatedTasks' => $recentlyUpdatedTasks
403 ];
404
405 if ($failed_count > 0) {
406 $response['failed_tasks'] = $failed_tasks;
407 if ($deleted_count > 0) {
408 $response['message'] = $deleted_count . ' ' . ($deleted_count === 1 ? 'task' : 'tasks') . ' deleted successfully, ' . $failed_count . ' ' . ($failed_count === 1 ? 'task' : 'tasks') . ' failed';
409 } else {
410 $response['message'] = 'Failed to delete ' . $failed_count . ' ' . ($failed_count === 1 ? 'task' : 'tasks');
411 }
412 } else {
413 $response['message'] = $deleted_count . ' ' . ($deleted_count === 1 ? 'task' : 'tasks') . ' deleted successfully';
414 }
415
416 return $this->response->sendSuccess($response, 200);
417
418 } catch (\Exception $e) {
419 return $this->response->sendError($e->getMessage(), 500);
420 }
421 }
422
423 public function updateTaskProperties(Request $request, $board_id, $task_id)
424 {
425 $board_id = absint($board_id);
426 $task_id = absint($task_id);
427 //Properties in col: settings, assignees,crm_contact_id, archived_at(AUTO_SET_TIMESTAMP) , status, title, description, priority, is_watching, is_template
428 $col = $request->getSafe('property', 'sanitize_text_field');
429 if ($col === 'description') {
430 $value = $request->getSafe('value', 'wp_kses_post');
431 } elseif ($col === 'settings') {
432 $value = $request->get('value');
433 if (is_array($value) && isset($value['cover']) && is_array($value['cover'])) {
434 if (isset($value['cover']['backgroundColor'])) {
435 $value['cover']['backgroundColor'] = sanitize_text_field($value['cover']['backgroundColor']);
436 }
437 }
438 } else {
439 $value = $request->getSafe('value', 'sanitize_text_field');
440 }
441
442 $validatedData = $this->updateTaskPropValidationAndSanitation($col, $value);
443 $task = Task::with(['board', 'labels', 'assignees'])->where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
444
445 $oldDateValue = null;
446 if (in_array($col, ['due_at', 'started_at'])) {
447 $oldDateValue = $task->{$col};
448 }
449
450 if ($task->parent_id && !$task->board_id) {
451 $task->board_id = $board_id;
452 $task->save();
453 }
454
455 $task = $this->taskService->updateTaskProperty($col, $validatedData[$col], $task);
456 $task->isOverdue = $task->isOverdue();
457 $task->isUpcoming = $task->upcoming();
458 $task->contact = Helper::crm_contact($task->crm_contact_id);
459 $task->is_watching = $task->isWatching();
460 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
461
462 if ($task->parent_id) {
463 $task->subtask_group_id = TaskMeta::where('task_id', $task->id)->where('key', Constant::SUBTASK_GROUP_CHILD)->value('value');
464 }
465
466 // A recent update to a task might impact other tasks on the board.
467 $updatedTasks = $this->taskService->getLastOneMinuteUpdatedTasks($board_id);
468 $taskExists = false;
469 foreach ($updatedTasks as $index => $updatedTask) {
470 if ($updatedTask->id === $task->id) {
471 $updatedTasks[$index] = $task; // Replace the existing task
472 $taskExists = true;
473 break;
474 }
475 }
476
477 if (!$taskExists) {
478 $updatedTasks[] = $task;
479 }
480
481 return [
482 'message' => __('Task has been updated', 'fluent-boards'),
483 'task' => $task,
484 'updatedTasks' => $updatedTasks
485 ];
486 }
487
488 public function updateTaskDates(Request $request, $board_id, $task_id)
489 {
490 $board_id = absint($board_id);
491 $task_id = absint($task_id);
492 $task = Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
493
494 // Capture old dates before updating
495 $oldDates = [
496 'due_at' => $task->due_at,
497 'started_at' => $task->started_at,
498 ];
499
500 $startAt = $request->getSafe('started_at', 'sanitize_text_field', NULL);
501 $dueAt = $request->getSafe('due_at', 'sanitize_text_field', NULL);
502 $reminderType = $request->getSafe('reminder_type', 'sanitize_text_field', NULL);
503 $remindAt = $request->getSafe('remind_at', 'sanitize_text_field', NULL);
504
505 if ($startAt && $dueAt) {
506 if (strtotime($startAt) > strtotime($dueAt)) {
507 $startAt = gmdate('Y-m-d 00:00:00', strtotime($dueAt));
508 }
509 }
510
511 $task = $this->taskService->updateTaskProperty('started_at', $startAt, $task);
512 $task = $this->taskService->updateTaskProperty('due_at', $dueAt, $task);
513
514 // Handle task reminder for all tasks (both tasks and subtasks)
515 $task = $this->taskService->updateTaskProperty('reminder_type', $reminderType, $task);
516 $task = $this->taskService->updateTaskProperty('remind_at', $remindAt, $task);
517
518 $datesChanged = false;
519 $changedDates = [];
520
521 if ($oldDates['due_at'] !== $task->due_at) {
522 $datesChanged = true;
523 $changedDates['due_at'] = $oldDates['due_at'];
524 }
525
526 if ($oldDates['started_at'] !== $task->started_at) {
527 $datesChanged = true;
528 $changedDates['started_at'] = $oldDates['started_at'];
529 }
530
531 if ($datesChanged) {
532 do_action('fluent_boards/task_date_changed', $task, $changedDates);
533 }
534
535 return [
536 'task' => $task,
537 'message' => __('Dates have been updated', 'fluent-boards'),
538 'updatedTasks' => $this->taskService->getLastOneMinuteUpdatedTasks($board_id),
539 ];
540 }
541
542 public function updateTaskCoverPhoto(Request $request, $board_id, $task_id)
543 {
544 $board_id = absint($board_id);
545 $task_id = absint($task_id);
546 $imagePath = $request->getSafe('thumbnail', 'sanitize_text_field');
547 $task = $this->taskService->taskCoverPhotoUpdate($task_id, $imagePath);
548
549 return [
550 'message' => __('Task cover photo has been updated', 'fluent-boards'),
551 'task' => $task,
552 ];
553
554 }
555
556 public function taskStatusUpdate(Request $request, $board_id, $task_id)
557 {
558 $board_id = absint($board_id);
559 $task_id = absint($task_id);
560 Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
561 $integrationType = $request->getSafe('integrationType', 'sanitize_text_field');
562 return [
563 'message' => __('Task status has been updated', 'fluent-boards'),
564 'task' => $this->taskService->taskStatusUpdate($task_id, $integrationType),
565 ];
566 }
567
568 public function deleteTask($board_id, $task_id)
569 {
570 $board_id = absint($board_id);
571 $task_id = absint($task_id);
572 $task = Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
573 $options = null;
574 //if we need to do something before a task is deleted
575 do_action('fluent_boards/before_task_deleted', $task, $options);
576
577 $this->taskService->deleteTask($task);
578
579 return [
580 'updatedTasks' => $this->taskService->getLastOneMinuteUpdatedTasks($board_id),
581 'message' => __('Task has been deleted', 'fluent-boards'),
582 ];
583 }
584
585 private function taskSanitizeAndValidate($data, array $rules = [])
586 {
587 $data = Helper::sanitizeTask($data);
588
589 return $this->validate($data, $rules);
590 }
591
592 private function updateTaskPropValidationAndSanitation($col, $value)
593 {
594 $rules = [
595 'title' => 'required|string',
596 'board_id' => 'required',
597 'parent_id' => 'required',
598 'crm_contact_id' => 'nullable',
599 'type' => 'nullable|string',
600 'status' => 'nullable|string',
601 'stage_id' => 'required',
602 'reminder_type' => 'nullable|string',
603 'priority' => 'nullable|string',
604 'lead_value' => 'nullable|numeric|between:0,9999999.99',
605 'remind_at' => 'nullable|string',
606 'scope' => 'nullable|string',
607 'source' => 'nullable|string',
608 'description' => 'nullable|string',
609 'due_at' => 'nullable|string',
610 'started_at' => 'nullable|string',
611 'start_at' => 'nullable|string',
612 'log_minutes' => 'nullable|integer|unsigned',
613 'last_completed' => 'nullable|date',
614 'assignees' => 'nullable|integer',
615 'archived_at' => 'nullable|string',
616 'is_watching' => 'nullable',
617 'is_template' => 'string',
618 'last_completed_at' => 'nullable',
619 'settings' => 'nullable|array',
620 ];
621 if (array_key_exists($col, $rules)) {
622 $rule = $rules[$col];
623 if ('assignees' == $col && is_array($value)) {
624 $sanitizedAndValidatedValue = [];
625 foreach ($value as $val) {
626 $sanitizeData = Helper::sanitizeTask([$col => $val]);
627 $validatedData = $this->validate($sanitizeData, [
628 $col => $rule,
629 ]);
630 array_push($sanitizedAndValidatedValue, $validatedData[$col]);
631 }
632
633 return [$col => $sanitizedAndValidatedValue];
634 }
635 $data = Helper::sanitizeTask([$col => $value]);
636
637 return $this->validate($data, [
638 $col => $rule,
639 ]);
640 }
641
642 // If the column is not found in the rules array, throw an exception
643 // translators: %s is the property name
644 throw new \Exception(sprintf(esc_html__('Invalid property: %s', 'fluent-boards'), esc_html($col)));
645 }
646
647 public function getLabelsByTask($task_id)
648 {
649 $task_id = absint($task_id);
650 $labels = $this->taskService->getLabelsByTask($task_id);
651
652 return $this->sendSuccess([
653 'labels' => $labels,
654 ], 200);
655 }
656
657 public function getStageByTask($task_id)
658 {
659 $task_id = absint($task_id);
660 $stage = $this->taskService->getStageByTask($task_id);
661
662 return [
663 'stage' => $stage,
664 ];
665 }
666
667 public function assignYourselfInTask($board_id, $task_id)
668 {
669 $board_id = absint($board_id);
670 $task_id = absint($task_id);
671 $task = $this->taskService->assignYourselfInTask($board_id, $task_id);
672 $task->is_watching = $task->isWatching();
673
674 return [
675 'task' => $task,
676 ];
677 }
678
679 public function detachYourselfFromTask($board_id, $task_id)
680 {
681 $board_id = absint($board_id);
682 $task_id = absint($task_id);
683 $task = $this->taskService->detachYourselfFromTask($board_id, $task_id);
684 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
685 $task->is_watching = $task->isWatching();
686
687 return [
688 'task' => $task,
689 ];
690 }
691
692 private function taskMetaSanitizeAndValidate($data, array $rules = [])
693 {
694 $data = Helper::sanitizeTaskMeta($data);
695
696 return $this->validate($data, $rules);
697 }
698
699 public function moveTaskToNextStage($board_id, $task_id)
700 {
701 $board_id = absint($board_id);
702 $task_id = absint($task_id);
703 Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
704 $task = $this->taskService->moveTaskToNextStage($task_id);
705
706 return [
707 'task' => $task
708 ];
709 }
710
711 /**
712 * @throws \Exception
713 */
714 public function moveTask(Request $request, $board_id, $task_id)
715 {
716 $board_id = absint($board_id);
717 $task_id = absint($task_id);
718 $task = Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
719 $oldStageId = $task->stage_id;
720 $newStageId = $request->getSafe('newStageId', 'intval');
721 $newIndex = $request->getSafe('newIndex', 'intval');
722 $newBoardId = $request->getSafe('newBoardId', 'intval');
723
724 if ((!is_numeric($newStageId) || $newStageId == 0)) {
725 throw new \Exception(esc_html__('Invalid Stage', 'fluent-boards'));
726 }
727 // if ((!is_numeric($newIndex) || $newIndex == 0)) {
728 // throw new \Exception(__('Invalid Value', 'fluent-boards'));
729 // }
730 if ($newBoardId) {
731 if ((!is_numeric($newBoardId) || $newBoardId == 0)) {
732 throw new \Exception(esc_html__('Invalid Board', 'fluent-boards'));
733 }
734 $task = $this->taskService->changeBoardByTask($task, $newBoardId);
735 // Load relationships to ensure frontend gets updated data after board move
736 $task->load(['assignees', 'labels', 'watchers', 'attachments']);
737 }
738
739 $task->stage_id = $newStageId;
740 $task = $task->moveToNewPosition($newIndex);
741
742 if ($oldStageId != $newStageId) {
743
744 $this->taskService->manageDefaultAssignees($task, $newStageId);
745
746 $defaultPosition = $task->stage->defaultTaskStatus();
747
748 if ($defaultPosition == 'closed' && $task->status != 'closed') {
749 $task = $task->close();
750 }
751
752 // do_action('fluent_boards/task_moved_to_new_stage', $task, $oldStageId);
753
754 do_action('fluent_boards/task_stage_updated', $task, $oldStageId);
755
756 $usersToSendEmail = $this->notificationService->filterAssigneeToSendEmail($task->id, Constant::BOARD_EMAIL_STAGE_CHANGE);
757 $this->taskService->sendMailAfterTaskModify('stage_change', $usersToSendEmail, $task->id);
758 }
759
760 do_action('fluent_boards/task_updated', $task, 'position');
761
762 $lastBoardsUpdated = $request->getSafe('last_boards_updated', 'sanitize_text_field');
763 $updatedTasks = $this->taskService->getLastOneMinuteUpdatedTasks($task->board_id, $lastBoardsUpdated);
764
765 return [
766 'message' => __('Task has been updated', 'fluent-boards'),
767 'task' => $task,
768 'updatedTasks' => $updatedTasks,
769 'last_updated' => current_time('mysql')
770 ];
771 }
772
773 /**
774 * Get comments and activities for a task, merged into a single array, sorted by creation date, and paginated.
775 *
776 * @param Request $request The HTTP request instance.
777 * @param int $board_id The ID of the board.
778 * @param int $task_id The ID of the task.
779 * @return \WP_REST_Response The response containing paginated comments and activities, total count, current page, and items per page.
780 */
781 public function getCommentsAndActivities( Request $request, $board_id, $task_id)
782 {
783 $board_id = absint($board_id);
784 $task_id = absint($task_id);
785 Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
786 try {
787 // Pagination parameters
788 $page = $request->getSafe('page', 'intval', 1);
789 $perPage = $request->getSafe('per_page', 'intval', 10);
790 $filter = $request->getSafe('filter', 'sanitize_text_field', 'newest'); // Filter for comments and activities
791
792 $commentsAndActivities = $this->taskService->getCommentsAndActivities($task_id, $perPage, $page, $filter);
793 // Return the response with the task, paginated comments and activities, total count, current page, and items per page
794 return $this->sendSuccess([
795 'comments_and_activities' => $commentsAndActivities,
796 ]);
797 } catch (\Exception $e) {
798 return $this->sendError($e->getMessage(), 500);
799 }
800 }
801
802 public function sendMailAfterStageChange($usersToSendEmail, $taskId)
803 {
804 $current_user_id = get_current_user_id();
805
806 /* this will run in background as soon as possible */
807 /* sending Model or Model Instance won't work here */
808 as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_stage_change', [$taskId, $usersToSendEmail, $current_user_id], 'fluent-boards');
809 }
810 public function getAssociatedTasks($associated_id)
811 {
812 $associated_id = absint($associated_id);
813 return [
814 'tasks' => $this->taskService->getAssociatedTasks($associated_id)
815 ];
816 }
817
818 /**
819 * @param Request $request
820 * @param $board_id
821 * @param $task_id
822 * @return \WP_REST_Response
823 */
824 public function uploadMediaFileFromWpEditor(Request $request, $board_id, $task_id)
825 {
826 $board_id = absint($board_id);
827 $task_id = absint($task_id);
828 Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
829 try {
830
831
832 $file = Arr::get($request->files(), 'file')->toArray();
833 (new \FluentBoards\App\Services\UploadService)->validateFile($file);
834
835 $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
836
837 $fileData = $uploadInfo[0];
838 $fileUploadedData = $this->taskService->uploadMediaFileFromWpEditor($task_id, $fileData, Constant::TASK_DESCRIPTION);
839 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
840 $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
841 $fileUploadedData['driver'] = $mediaData['driver'];
842 $fileUploadedData['file_path'] = $mediaData['file_path'];
843 $fileUploadedData['full_url'] = $mediaData['full_url'];
844 $fileUploadedData->save();
845 }
846 $fileUploadedData['public_url'] = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
847
848 return $this->sendSuccess([
849 'message' => __('Image has been uploaded', 'fluent-boards'),
850 'file' => $fileUploadedData
851 ], 200);
852
853
854 } catch (\Exception $e) {
855 return $this->sendError($e->getMessage(), 400);
856 }
857 }
858
859 public function createTaskFromImage(Request $request, $board_id)
860 {
861 $board_id = absint($board_id);
862 $stageId = $request->getSafe('stage_id', 'intval');
863 $file = Arr::get($request->files(), 'file')->toArray();
864 (new \FluentBoards\App\Services\UploadService)->validateFile($file);
865
866 $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
867 $task = $this->taskService->createTaskFromImage($board_id, $stageId, $uploadInfo, $file);
868 return $this->sendSuccess([
869 'task' => $task,
870 'updatedTasks' => $this->taskService->getLastOneMinuteUpdatedTasks($board_id),
871 'message' => __('Task has been created', 'fluent-boards'),
872 ], 200);
873
874 }
875
876 public function handleTaskCoverImageUpload(Request $request, $board_id, $task_id)
877 {
878 $board_id = absint($board_id);
879 $task_id = absint($task_id);
880 Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
881 try {
882
883 $file = Arr::get($request->files(), 'file')->toArray();
884 (new \FluentBoards\App\Services\UploadService)->validateFile($file);
885
886 $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
887
888 $fileData = $uploadInfo[0];
889 $fileUploadedData = $this->taskService->uploadMediaFileFromWpEditor($task_id, $fileData, Constant::TASK_DESCRIPTION);
890 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
891 $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
892 $fileUploadedData['driver'] = $mediaData['driver'];
893 $fileUploadedData['file_path'] = $mediaData['file_path'];
894 $fileUploadedData['full_url'] = $mediaData['full_url'];
895 $fileUploadedData->save();
896 }
897
898 $task = Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
899 $settings = $task->settings;
900 $this->taskService->deleteTaskCoverImage($settings);
901 $publicUrl = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
902
903 $settings['cover'] = [
904 'imageId' => $fileUploadedData['id'],
905 'backgroundImage' => $publicUrl,
906 ];
907 $task->settings = $settings;
908 $task->save();
909
910 return $this->sendSuccess([
911 'message' => __('Image has been uploaded', 'fluent-boards'),
912 'public_url' => $publicUrl
913 ], 200);
914
915
916 } catch (\Exception $e) {
917 return $this->sendError($e->getMessage(), 400);
918 }
919 }
920 public function removeTaskCover($board_id, $task_id)
921 {
922 $board_id = absint($board_id);
923 $task_id = absint($task_id);
924 try {
925 $task = Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
926 $settings = $task->settings;
927 $this->taskService->deleteTaskCoverImage($settings);
928 unset($settings['cover']);
929 $task->settings = $settings;
930 $task->save();
931 return $this->sendSuccess([
932 'task' => $task,
933 'message' => __('Task Cover removed successfully', 'fluent-boards'),
934 ]);
935 } catch (\Exception $e) {
936 return $this->sendError($e->getMessage(), 400);
937 }
938 }
939
940 /**
941 * Get task tabs configuration
942 */
943 public function getTaskTabsConfig()
944 {
945 $default_config = [
946 [
947 'name' => 'assigned',
948 'label' => __('Assigned', 'fluent-boards'),
949 'visible' => 'true',
950 'order' => 1
951 ],
952 [
953 'name' => 'upcoming',
954 'label' => __('Upcoming', 'fluent-boards'),
955 'visible' => 'true',
956 'order' => 2
957 ],
958 [
959 'name' => 'overdue',
960 'label' => __('Overdue', 'fluent-boards'),
961 'visible' => 'true',
962 'order' => 3
963 ],
964 [
965 'name' => 'mentioned',
966 'label' => __('Mentioned', 'fluent-boards'),
967 'visible' => 'true',
968 'order' => 4
969 ],
970 [
971 'name' => 'completed',
972 'label' => __('Completed', 'fluent-boards'),
973 'visible' => 'true',
974 'order' => 5
975 ],
976 [
977 'name' => 'others',
978 'label' => __('Others', 'fluent-boards'),
979 'visible' => 'true',
980 'order' => 6
981 ]
982 ];
983
984 $existConfig = Meta::where('object_id', get_current_user_id())->where('key', Constant::FBS_TASK_TABS_CONFIG)->first();
985 $config = $default_config;
986
987 if ($existConfig && !empty($existConfig->value)) {
988 $config = $existConfig->value;
989 $existingNames = array_column($config, 'name');
990 $missingTabs = [];
991 foreach ($default_config as $defaultTab) {
992 if (!in_array($defaultTab['name'], $existingNames)) {
993 $missingTabs[] = $defaultTab;
994 }
995 }
996
997 if (!empty($missingTabs)) {
998 $newConfig = [];
999 $order = 1;
1000 $addedAssigned = false;
1001 foreach ($config as $tab) {
1002 if ($tab['name'] === 'upcoming' && !$addedAssigned) {
1003 $assignedTab = array_filter($missingTabs, fn($t) => $t['name'] === 'assigned');
1004 if (!empty($assignedTab)) {
1005 $assignedTab = reset($assignedTab);
1006 $assignedTab['order'] = $order++;
1007 $newConfig[] = $assignedTab;
1008 $addedAssigned = true;
1009 }
1010 }
1011 $tab['order'] = $order++;
1012 $newConfig[] = $tab;
1013 }
1014 foreach ($missingTabs as $missingTab) {
1015 if ($missingTab['name'] !== 'assigned') {
1016 $missingTab['order'] = $order++;
1017 $newConfig[] = $missingTab;
1018 }
1019 }
1020 $config = $newConfig;
1021 $existConfig->value = $config;
1022 $existConfig->save();
1023 }
1024 }
1025
1026 return $this->sendSuccess([
1027 'data' => $config
1028 ]);
1029 }
1030
1031 /**
1032 * Save task tabs configuration
1033 */
1034 public function saveTaskTabsConfig(Request $request)
1035 {
1036 $rawConfig = $request->getSafe('tabs');
1037
1038 if (empty($rawConfig) || !is_array($rawConfig)) {
1039 return $this->sendError([
1040 'message' => __('Invalid data format', 'fluent-boards')
1041 ], 400);
1042 }
1043
1044 // Sanitize config array
1045 $config = [];
1046 foreach ($rawConfig as $tab) {
1047 if (!is_array($tab)) {
1048 continue;
1049 }
1050 $sanitizedTab = [
1051 'name' => isset($tab['name']) ? sanitize_text_field($tab['name']) : '',
1052 'label' => isset($tab['label']) ? sanitize_text_field($tab['label']) : '',
1053 'visible' => isset($tab['visible']) ? sanitize_text_field($tab['visible']) : 'false',
1054 'order' => isset($tab['order']) ? absint($tab['order']) : 0,
1055 ];
1056 $config[] = $sanitizedTab;
1057 }
1058
1059 if (count(array_filter($config, fn($tab) => $tab['visible'] == 'true')) == 0) {
1060 return $this->sendError([
1061 'message' => __('At least one tab must be visible', 'fluent-boards')
1062 ], 400);
1063 }
1064
1065 $userId = get_current_user_id();
1066
1067 $exit = Meta::where('object_id', $userId)->where('key', 'fbs_task_tabs_config')->first();
1068
1069 if ($exit) {
1070 $exit->value = $config;
1071 $exit->save();
1072 } else {
1073 $exit = Meta::create([
1074 'object_id' => $userId,
1075 'object_type' => 'option',
1076 'key' => Constant::FBS_TASK_TABS_CONFIG,
1077 'value' => $config
1078 ]);
1079 }
1080 $config = $exit->value;
1081
1082 return $this->sendSuccess([
1083 'message' => __('Configuration saved successfully', 'fluent-boards'),
1084 'config' => $config
1085 ]);
1086 }
1087 public function getAssociatedCrmContacts($board_id)
1088 {
1089 $board_id = absint($board_id);
1090 $contactsInTasks = Task::where('board_id', $board_id)
1091 ->whereNotNull('crm_contact_id')
1092 ->get();
1093
1094 if ($contactsInTasks->isEmpty()) {
1095 return $this->sendSuccess([]);
1096 }
1097
1098 $contactIds = $contactsInTasks->pluck('crm_contact_id')
1099 ->unique()
1100 ->toArray();
1101
1102 $allContacts = Subscriber::whereIn('id', $contactIds)->get();
1103
1104 if ($allContacts->isEmpty()) {
1105 return $this->sendSuccess([]);
1106 }
1107
1108 $formattedContacts = [];
1109 foreach ($allContacts as $contact) {
1110 $name = trim($contact->first_name . ' ' . $contact->last_name);
1111
1112 $formattedContacts[] = [
1113 'id' => $contact->id,
1114 'display_name' => $name,
1115 'email' => $contact->email,
1116 'photo' => fluent_boards_user_avatar($contact->user_email, $name),
1117 ];
1118 }
1119 if (!empty($formattedContacts)) {
1120 usort($formattedContacts, function ($a, $b) {
1121 return strcmp($a['display_name'], $b['display_name']);
1122 });
1123 }
1124
1125 return $this->sendSuccess($formattedContacts);
1126 }
1127
1128 public function cloneTask(Request $request, $board_id, $task_id)
1129 {
1130 $board_id = absint($board_id);
1131 $task_id = absint($task_id);
1132 $taskData = $this->taskSanitizeAndValidate($request->only(['title', 'stage_id', 'assignee', 'subtask', 'label', 'attachment', 'comment']), [
1133 'title' => 'required|string',
1134 'stage_id' => 'required|numeric',
1135 'assignee' => 'required',
1136 'subtask' => 'required',
1137 'label' => 'required',
1138 'attachment' => 'required',
1139 'comment' => 'required',
1140 ]);
1141 try {
1142 Task::where('board_id', $board_id)->where('id', $task_id)->firstOrFail();
1143 $taskData = fluent_boards_string_to_bool($taskData);
1144 $clonedTask = $this->taskService->cloneTask($task_id, $taskData);
1145
1146 return $this->sendSuccess([
1147 'message' => __('Task has been cloned successfully', 'fluent-boards'),
1148 'task' => $clonedTask,
1149 'updatedTasks' => $this->taskService->getLastOneMinuteUpdatedTasks($clonedTask->board_id)
1150 ], 200);
1151 } catch (\Exception $e) {
1152 return $this->sendError($e->getMessage(), 400);
1153 }
1154 }
1155
1156 public function bulkActions(Request $request, $board_id)
1157 {
1158 $board_id = absint($board_id);
1159 try {
1160 $rawTaskIds = $request->getSafe('task_ids');
1161 // Sanitize task_ids array to integers
1162 $taskIds = [];
1163 if (is_array($rawTaskIds)) {
1164 $taskIds = array_filter(array_map('intval', $rawTaskIds));
1165 }
1166 $action = $request->getSafe('action', 'sanitize_text_field');
1167 // Sanitize params array
1168 $rawParams = $request->except(['task_ids', 'action']);
1169 // Ensure rawParams is sanitized
1170 if (!is_array($rawParams)) {
1171 $rawParams = [];
1172 }
1173 $params = [];
1174 foreach ($rawParams as $key => $value) {
1175 $sanitizedKey = sanitize_text_field($key);
1176 if (is_array($value)) {
1177 $params[$sanitizedKey] = array_map('sanitize_text_field', $value);
1178 } else {
1179 $params[$sanitizedKey] = sanitize_text_field($value);
1180 }
1181 }
1182
1183 $result = $this->taskService->bulkActions($taskIds, $action, $params, $board_id);
1184
1185 // Process successful tasks the same way as getTasksByBoard
1186 if (!empty($result['successful_tasks'])) {
1187 $board = Board::findOrFail($board_id);
1188 $this->processTasks($result['successful_tasks'], $board);
1189 }
1190
1191 return $this->sendSuccess($result);
1192
1193 } catch (\Exception $e) {
1194 return $this->sendError($e->getMessage(), 500);
1195 }
1196 }
1197 }
1198