PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
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
← All changes | app/Services/UserService.php +355 -35 1.112.1.0 View file →
@@ -3,8 +3,9 @@
3 3 namespace FluentBoards\App\Services;
4 4
5 5 use FluentBoards\App\Models\Activity;
6 6 use FluentBoards\App\Models\Meta;
7 +use FluentBoards\App\Models\Notification;
7 8 use FluentBoards\App\Models\Task;
8 9 use FluentBoards\App\Models\User;
9 10 use FluentBoards\App\Models\Board;
10 11 use FluentBoards\App\Models\Relation;
@@ -44,9 +45,13 @@
44 45 $accessBoardIds[] = $boardObject->object_id;
45 46
46 47 $boardUserMaps[$boardObject->foreign_id][] = [
47 48 'board_id' => $boardObject->object_id,
48 - 'role' => Arr::get($boardObject->settings, 'is_admin') ? 'admin' : 'member'
49 + 'role' => Arr::get($boardObject->settings, 'is_admin')
50 + ? 'admin'
51 + : (Arr::has($boardObject->settings, 'is_viewer_only') && Arr::get($boardObject->settings, 'is_viewer_only')
52 + ? 'viewer'
53 + : 'member')
49 54 ];
50 55 }
51 56
52 57 $accessBoardIds = array_unique($accessBoardIds);
@@ -103,20 +108,46 @@
103 108
104 109 return $boardUsers;
105 110 }
106 111
112 + /**
113 + * Return associated users and roles only from boards shared with the requester.
114 + */
107 115 public function memberAssociatedTaskUsers($userId)
108 116 {
109 117 $user = User::find($userId);
110 - $boards = $user->whichBoards->pluck('id');
111 - $boardUsers = Relation::whereIn('object_id', $boards)
118 + $memberBoardIds = array_values(array_unique(array_filter(array_map(
119 + 'intval',
120 + $user->whichBoards->pluck('id')->toArray()
121 + ))));
122 + $requesterBoardIds = array_values(array_unique(array_filter(array_map(
123 + 'intval',
124 + PermissionManager::getBoardIdsForUser(get_current_user_id())
125 + ))));
126 + $sharedBoardIds = array_values(array_intersect($memberBoardIds, $requesterBoardIds));
127 +
128 + if (!$sharedBoardIds) {
129 + return [
130 + 'uniqueUsers' => [],
131 + 'userWiseBoardDesignation' => [],
132 + ];
133 + }
134 +
135 + $boardUsers = Relation::whereIn('object_id', $sharedBoardIds)
112 136 ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
113 137 ->pluck('foreign_id')->toArray();
114 138 $uniqueUsersIds = array_unique($boardUsers);
115 - $uniqueUsers = User::whereIn('ID', $uniqueUsersIds)->with('whichBoards')->get();
139 + $boardTable = (new Board())->getTable();
140 + $uniqueUsers = User::whereIn('ID', $uniqueUsersIds)
141 + ->with(['whichBoards' => function ($query) use ($boardTable, $sharedBoardIds) {
142 + $query->whereIn($boardTable . '.id', $sharedBoardIds);
143 + }])
144 + ->get();
116 145
117 146 $userWiseBoardDesignation = Relation::query()->whereIn('foreign_id', $uniqueUsersIds)
118 - ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)->get();
147 + ->whereIn('object_id', $sharedBoardIds)
148 + ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
149 + ->get();
119 150
120 151 $data = array();
121 152 $data['userWiseBoardDesignation'] = $userWiseBoardDesignation;
122 153
@@ -130,9 +161,9 @@
130 161 } elseif (PermissionManager::isFluentBoardsAdmin($uniqueUser['ID'])) {
131 162 $uniqueUser['is_super'] = true;
132 163 $uniqueUser['is_wpadmin'] = false;
133 164 } else {
134 - $uniqueUser['all_boards'] = Arr::get($uniqueUser, 'boards');
165 + $uniqueUser['all_boards'] = $uniqueUser->getRelation('whichBoards');
135 166 $uniqueUser['is_super'] = false;
136 167 $uniqueUser['is_wpadmin'] = false;
137 168 }
138 169 }
@@ -207,64 +238,353 @@
207 238 }
208 239
209 240 return $searchResult;
210 241 }
242 +
211 243
212 - public function getMemberAssociatedTasks($user_id, $page)
244 + public function getMemberAssociatedTasks($user_id, $requestData)
213 245 {
214 - // get the user
246 + $taskType = $requestData['taskType'] ?? null;
247 + $boardIds = !empty($requestData['boardIds']) ? $requestData['boardIds'] : [];
248 + $orderBy = $requestData['orderBy'] ?? 'created_at';
249 + $order = strtoupper($requestData['order'] ?? 'ASC');
250 + $per_page = $requestData['per_page'] ?? 15;
251 + $page = $requestData['page'] ?? 1;
215 252 $user = User::find($user_id);
216 - // get the task created by the user
217 - $tasks = Task::query()->where('created_by', $user_id)
218 - ->orderBy('created_at', 'desc')
219 - ->with(['stage', 'board'])
220 - ->paginate(10, ['*'], 'page', $page);
221 - // get the task assigned to the user
222 - $assignedTasks = $user->tasks()
223 - ->orderBy('created_at', 'desc')
224 - ->with(['stage', 'board'])
225 - ->paginate(10, ['*'], 'page', $page);
253 +
254 + $loggedInUserId = get_current_user_id();
255 +
256 + $allowedBoardIds = PermissionManager::getBoardIdsForUser($loggedInUserId);
257 +
258 + if (!$user || empty($allowedBoardIds)) {
259 + return [
260 + 'tasks' => [],
261 + 'paginationInfo' => [
262 + 'current_page' => 1,
263 + 'last_page' => 1,
264 + 'per_page' => (int) $per_page,
265 + 'total' => 0,
266 + ],
267 + ];
268 + }
269 +
270 + // Table view needs the labels column; list view ignores the extra relation.
271 + $taskRelations = ['stage', 'board', 'labels'];
272 +
273 + if ($taskType == 'assigned') {
274 + $tasksQuery = $user->assignedTasks()
275 + ->with($taskRelations)
276 + ->whereNull('archived_at')
277 + ->whereNull('parent_id')
278 + ->whereIn('board_id', $allowedBoardIds)
279 + ->where('status', '!=', 'closed')
280 + ->onActiveAvailableBoards();
281 + } else if ($taskType == 'mentioned') {
282 + $tasksQuery = $user->mentionedTasks()
283 + ->with($taskRelations)
284 + ->whereNull('archived_at')
285 + ->whereNull('parent_id')
286 + ->whereIn('board_id', $allowedBoardIds)
287 + ->onActiveAvailableBoards();
288 + } else {
289 + // Watched tasks power the date-based and completed profile tabs.
290 + $tasksQuery = $user->tasks()
291 + ->with($taskRelations)
292 + ->whereNull('archived_at')
293 + ->whereIn('board_id', $allowedBoardIds)
294 + ->onActiveAvailableBoards();
295 +
296 + switch ($taskType) {
297 + case 'upcoming':
298 + $tasksQuery->upcoming()
299 + ->whereIn('board_id', $allowedBoardIds);
300 + break;
301 + case 'due_today':
302 + $tasksQuery->dueToday()
303 + ->whereIn('board_id', $allowedBoardIds);
304 + break;
305 + case 'overdue':
306 + $tasksQuery->overdue()
307 + ->whereIn('board_id', $allowedBoardIds);
308 + break;
309 + case 'completed':
310 + $tasksQuery->where('status', 'closed')
311 + ->whereIn('board_id', $allowedBoardIds);
312 + break;
313 + default:
314 + $tasksQuery->whereNull('due_at')
315 + ->whereIn('board_id', $allowedBoardIds);
316 + break;
317 + }
318 + }
319 +
320 + $currentUserId = get_current_user_id();
321 + if ($currentUserId != $user->ID && !PermissionManager::isAdmin()) {
322 + $currentUser = User::find($currentUserId);
323 + $currentUserBoardIds = $currentUser->boards->pluck('id')->toArray();
324 +
325 + if (empty($boardIds)) {
326 + $boardIds = $currentUserBoardIds;
327 + } else {
328 + // Get the intersection of the two arrays
329 + $boardIds = array_intersect($boardIds, $currentUserBoardIds);
330 + }
331 + }
332 +
333 + if (!empty($boardIds)) {
334 + $tasksQuery->whereIn('board_id', $boardIds);
335 + }
336 +
337 + $sortOptions = ['priority', 'due_at', 'position', 'created_at', 'title'];
338 + $orderOptions = ['ASC', 'DESC'];
339 +
340 + // Validate order and orderBy parameters
341 + if (!in_array($order, $orderOptions) || !in_array($orderBy, $sortOptions)) {
342 + throw new \Exception(esc_html__('Invalid sort or orderBy parameter', 'fluent-boards'));
343 + }
344 +
345 + // Apply ordering based on the specified order and orderBy
346 + if ($orderBy === 'priority') {
347 + $tasksQuery->orderByRaw("FIELD(priority, 'urgent', 'high', 'medium', 'low') {$order}");
348 + } else if ($orderBy === 'due_at') {
349 + $tasksQuery->orderByRaw("ISNULL(due_at), due_at {$order}");
350 + } else {
351 + $tasksQuery->orderBy($orderBy, $order);
352 + }
353 +
354 + $tasks = $tasksQuery->paginate($per_page, ['*'], 'page', $page);
355 +
226 356 return [
227 - 'tasks' => $tasks->merge($assignedTasks)->sortByDesc('created_at')->values()->toArray(),
357 + 'tasks' => $tasks->values()->toArray(),
228 358 'paginationInfo' => [
229 359 'current_page' => $tasks->currentPage(),
230 - 'last_page' => $tasks->lastPage() > $assignedTasks->lastPage() ? $tasks->lastPage() : $assignedTasks->lastPage()
231 - ]
360 + 'last_page' => $tasks->lastPage(),
361 + 'per_page' => (int) $tasks->perPage(),
362 + 'total' => $tasks->total(),
363 + ],
232 364 ];
365 + }
233 366
367 + /**
368 + * Get totals for each task category available in the profile task tabs.
369 + */
370 + public function getMemberTaskCounts($userId, $boardIds = [])
371 + {
372 + $user = User::find($userId);
373 + $currentUserId = get_current_user_id();
374 + $allowedBoardIds = PermissionManager::getBoardIdsForUser($currentUserId);
375 +
376 + if (!$user || empty($allowedBoardIds)) {
377 + return array_fill_keys(
378 + ['due_today', 'assigned', 'upcoming', 'overdue', 'mentioned', 'completed', 'others'],
379 + 0
380 + );
381 + }
382 +
383 + if ($currentUserId != $user->ID && !PermissionManager::isAdmin()) {
384 + $currentUser = User::find($currentUserId);
385 + $currentUserBoardIds = $currentUser->boards->pluck('id')->toArray();
386 + $boardIds = empty($boardIds)
387 + ? $currentUserBoardIds
388 + : array_intersect($boardIds, $currentUserBoardIds);
389 + }
390 +
391 + // Keep the count queries aligned with the profile list without loading task models or relations.
392 + $applyTaskScope = function ($query) use ($allowedBoardIds, $boardIds) {
393 + $query->whereNull('archived_at')
394 + ->whereNull('parent_id')
395 + ->whereIn('board_id', $allowedBoardIds)
396 + ->onActiveAvailableBoards();
397 +
398 + if (!empty($boardIds)) {
399 + $query->whereIn('board_id', $boardIds);
400 + }
401 +
402 + return $query;
403 + };
404 + $watchedTasks = function () use ($user, $applyTaskScope) {
405 + return $applyTaskScope($user->tasks());
406 + };
407 +
408 + return [
409 + 'due_today' => (int) $watchedTasks()->dueToday()->count(),
410 + 'assigned' => (int) $applyTaskScope($user->assignedTasks())
411 + ->where('status', '!=', 'closed')
412 + ->count(),
413 + 'upcoming' => (int) $watchedTasks()->upcoming()->count(),
414 + 'overdue' => (int) $watchedTasks()->overdue()->count(),
415 + 'mentioned' => (int) $applyTaskScope($user->mentionedTasks())->count(),
416 + 'completed' => (int) $watchedTasks()->where('status', 'closed')->count(),
417 + 'others' => (int) $watchedTasks()->whereNull('due_at')->count(),
418 + ];
234 419 }
235 420
421 +
422 + /**
423 + * Get a member's activities scoped to boards the requesting user can access.
424 + */
236 425 public function getMemberRelatedAcitivies($user_id, $page)
237 426 {
238 - $activities = Activity::query()->where('created_by', $user_id)
427 + $perPage = 40;
428 + $user_id = absint($user_id);
429 + $page = max(1, absint($page));
430 + $allowedBoardIds = array_values(array_filter(array_map(
431 + 'intval',
432 + PermissionManager::getBoardIdsForUser(get_current_user_id())
433 + )));
434 +
435 + if (empty($allowedBoardIds)) {
436 + return [
437 + 'activities' => [],
438 + 'pagination' => $this->getEmptyPaginationInfo($page, $perPage),
439 + ];
440 + }
441 +
442 + $allowedTaskIds = Task::query()
443 + ->select('id')
444 + ->whereIn('board_id', $allowedBoardIds);
445 +
446 + $activities = Activity::query()
447 + ->where('created_by', $user_id)
448 + ->where(function ($query) use ($allowedBoardIds, $allowedTaskIds) {
449 + $query->where(function ($boardQuery) use ($allowedBoardIds) {
450 + $boardQuery->where('object_type', Constant::ACTIVITY_BOARD)
451 + ->whereIn('object_id', $allowedBoardIds);
452 + })->orWhere(function ($taskQuery) use ($allowedTaskIds) {
453 + $taskQuery->where('object_type', Constant::ACTIVITY_TASK)
454 + ->whereIn('object_id', $allowedTaskIds);
455 + });
456 + })
239 457 ->orderBy('created_at', 'desc')
240 - ->with('user')->paginate(40, ['*'], 'page', $page);
458 + ->with('user')
459 + ->paginate($perPage, ['*'], 'page', $page);
241 460
242 - $activitiesToShow = array();
461 + $activitiesToShow = [];
243 462
244 463 foreach ($activities as $activity) {
245 464 if ($activity->object_type == Constant::ACTIVITY_BOARD) {
246 465 $activity->load('board');
247 -// if($activity->settings && $activity->settings['task_id']){
248 -// $activity->task = Task::findOrFail($activity->settings['task_id']);
249 -// }
250 - if (PermissionManager::userHasPermission($activity->board_id, get_current_user_id())) {
251 - $activitiesToShow[] = $activity;
252 - }
466 + $activitiesToShow[] = $activity;
253 467 } elseif ($activity->object_type == Constant::ACTIVITY_TASK) {
254 468 $activity->load('task');
255 - if ($activity->task && PermissionManager::userHasPermission($activity->task->board_id, get_current_user_id())) {
256 - $activitiesToShow[] = $activity;
257 - }
469 + $activitiesToShow[] = $activity;
258 470 }
259 471 }
472 +
260 473 return [
261 - 'activities' => $activitiesToShow,
262 - 'pagination' => $activities->toArray(),
474 + 'activities' => $activitiesToShow,
475 + 'pagination' => $this->getPaginationInfo($activities),
263 476 ];
264 477 }
265 478
479 + /**
480 + * Return pagination metadata without duplicating serialized row data.
481 + */
482 + private function getPaginationInfo($paginator)
483 + {
484 + return [
485 + 'current_page' => $paginator->currentPage(),
486 + 'last_page' => $paginator->lastPage(),
487 + 'per_page' => (int) $paginator->perPage(),
488 + 'total' => $paginator->total(),
489 + ];
490 + }
491 +
492 + /**
493 + * Return an empty pagination payload for callers without accessible boards.
494 + */
495 + private function getEmptyPaginationInfo($page, $perPage)
496 + {
497 + return [
498 + 'current_page' => max(1, (int) $page),
499 + 'last_page' => 1,
500 + 'per_page' => (int) $perPage,
501 + 'total' => 0,
502 + ];
503 + }
504 +
266 505 private function getTaskById($taskId)
267 506 {
268 507 return Task::findOrFail($taskId);
508 + }
509 +
510 + public function getMemberBoards($user_id)
511 + {
512 + if(!$user_id) {
513 + return [];
514 + }
515 + $user = User::find($user_id);
516 + $currentUserId = get_current_user_id();
517 +
518 + if ($currentUserId == $user->ID || PermissionManager::isAdmin($currentUserId)) {
519 + return $user->whichBoards;
520 + }
521 +
522 + $currentUser = User::find($currentUserId);
523 + $boardIds = $currentUser ? $currentUser->whichBoards->pluck('id')->toArray() : [];
524 +
525 + if (empty($boardIds)) {
526 + return [];
527 + }
528 +
529 + return $user->whichBoards()->whereIn('fbs_boards.id', $boardIds)->get();
530 + }
531 +
532 + /**
533 + * Returns four aggregate counts for a member's stats widget.
534 + *
535 + * Cross-user access is scoped to boards shared with the requesting user,
536 + * matching the contract of getMemberBoards() and getMemberAssociatedTasks().
537 + * Unread notifications are personal and only returned for self or admin.
538 + */
539 + public function getMemberStats(int $user_id): array
540 + {
541 + $empty = ['assigned_tasks' => 0, 'completed_tasks' => 0, 'total_boards' => 0, 'unread_notifications' => 0];
542 +
543 + $user = User::find($user_id);
544 + if (!$user) {
545 + return $empty;
546 + }
547 +
548 + $currentUserId = get_current_user_id();
549 + $isSelfOrAdmin = ($currentUserId === $user_id) || PermissionManager::isAdmin($currentUserId);
550 +
551 + // For cross-user access, restrict counts to boards the requesting user can also see.
552 + $allowedBoardIds = null;
553 + if (!$isSelfOrAdmin) {
554 + $currentUser = User::find($currentUserId);
555 + $allowedBoardIds = $currentUser ? $currentUser->whichBoards->pluck('id')->toArray() : [];
556 + if (empty($allowedBoardIds)) {
557 + return $empty;
558 + }
559 + }
560 +
561 + $assignedBase = $user->assignedTasks()->whereNull('archived_at')->whereNull('parent_id');
562 + if ($allowedBoardIds !== null) {
563 + $assignedBase->whereIn('board_id', $allowedBoardIds);
564 + }
565 +
566 + $boardsQuery = $user->whichBoards();
567 + if ($allowedBoardIds !== null) {
568 + $boardsQuery->whereIn('fbs_boards.id', $allowedBoardIds);
569 + }
570 +
571 + // Use Notification model with object_type guard — matches NotificationService::newNotificationNumber().
572 + // Notifications are personal; return 0 when a non-admin views another member's profile.
573 + $unreadNotifications = 0;
574 + if ($isSelfOrAdmin) {
575 + $unreadNotifications = Notification::query()
576 + ->where('object_type', Constant::OBJECT_TYPE_BOARD_NOTIFICATION)
577 + ->whereHas('users', function ($q) use ($user_id) {
578 + $q->where('user_id', $user_id)->whereNull('marked_read_at');
579 + })
580 + ->count();
581 + }
582 +
583 + return [
584 + 'assigned_tasks' => (int) (clone $assignedBase)->where('status', '!=', 'closed')->count(),
585 + 'completed_tasks' => (int) (clone $assignedBase)->where('status', 'closed')->count(),
586 + 'total_boards' => (int) $boardsQuery->count(),
587 + 'unread_notifications' => (int) $unreadNotifications,
588 + ];
269 589 }
270 590 }