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 +279 -127 1.412.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;
@@ -107,20 +108,46 @@
107 108
108 109 return $boardUsers;
109 110 }
110 111
112 + /**
113 + * Return associated users and roles only from boards shared with the requester.
114 + */
111 115 public function memberAssociatedTaskUsers($userId)
112 116 {
113 117 $user = User::find($userId);
114 - $boards = $user->whichBoards->pluck('id');
115 - $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)
116 136 ->where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
117 137 ->pluck('foreign_id')->toArray();
118 138 $uniqueUsersIds = array_unique($boardUsers);
119 - $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();
120 145
121 146 $userWiseBoardDesignation = Relation::query()->whereIn('foreign_id', $uniqueUsersIds)
122 - ->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();
123 150
124 151 $data = array();
125 152 $data['userWiseBoardDesignation'] = $userWiseBoardDesignation;
126 153
@@ -134,9 +161,9 @@
134 161 } elseif (PermissionManager::isFluentBoardsAdmin($uniqueUser['ID'])) {
135 162 $uniqueUser['is_super'] = true;
136 163 $uniqueUser['is_wpadmin'] = false;
137 164 } else {
138 - $uniqueUser['all_boards'] = Arr::get($uniqueUser, 'boards');
165 + $uniqueUser['all_boards'] = $uniqueUser->getRelation('whichBoards');
139 166 $uniqueUser['is_super'] = false;
140 167 $uniqueUser['is_wpadmin'] = false;
141 168 }
142 169 }
@@ -211,87 +238,10 @@
211 238 }
212 239
213 240 return $searchResult;
214 241 }
242 +
215 243
216 -// public function getMemberAssociatedTasks($user_id, $requestData)
217 -// {
218 -// $taskType = $requestData['taskType'];
219 -// $boardIds = !empty($requestData['boardIds']) ? $requestData['boardIds'] : [];
220 -// $orderBy = $requestData['orderBy'];
221 -// $order = $requestData['order'];
222 -// $per_page = 15;
223 -// $user = User::find($user_id);
224 -//
225 -// // get the task assigned to the user
226 -// $tasksQuery = $user->tasks()->with(['stage', 'board'])->whereNull('archived_at');
227 -// if ($taskType == 'upcoming') {
228 -// $tasksQuery = $tasksQuery->upcoming();
229 -// } elseif ($taskType == 'overdue') {
230 -// $tasksQuery = $tasksQuery->overdue();
231 -// } elseif ($taskType == 'completed') {
232 -// $tasksQuery = $tasksQuery->where('status', 'closed');
233 -// } else {
234 -// $tasksQuery = $tasksQuery->whereNull('due_at');
235 -// }
236 -//
237 -// $currentUserId = get_current_user_id();
238 -// if($currentUserId != $user->ID) {
239 -// if(!PermissionManager::isAdmin()){
240 -// $currentUser = User::find($currentUserId);
241 -// $currentUserBoardIds = $currentUser->whichBoards->pluck('id')->toArray();
242 -// if (empty($boardIds)) {
243 -// $boardIds = $currentUserBoardIds;
244 -// } else {
245 -// // Get the intersection of the two arrays
246 -// $boardIds = array_intersect($boardIds, $currentUserBoardIds);
247 -// }
248 -// }
249 -// }
250 -//
251 -// if($boardIds){
252 -// $tasksQuery = $tasksQuery->whereIn('board_id', $boardIds);
253 -// }
254 -//
255 -// $sortOptions = ['priority', 'due_at', 'position', 'created_at', 'title'];
256 -// $orderOptions = ['ASC', 'DESC'];
257 -//
258 -// // Validate order and orderBy parameters
259 -// if (!in_array($order, $sortOptions) || !in_array($orderBy, $orderOptions)) {
260 -// throw new \Exception(__('Invalid sort or orderBy parameter', 'fluent-boards'));
261 -// }
262 -//
263 -// // Apply ordering based on the specified order and orderBy
264 -// switch ($order) {
265 -// case 'priority':
266 -// $tasksQuery->orderByRaw("FIELD(priority, 'High', 'Medium', 'Low') {$orderBy}");
267 -// break;
268 -//
269 -// case 'due_at':
270 -// if ($orderBy === 'ASC') {
271 -// // Separate ordering for tasks with and without due dates
272 -// $tasksQuery = $tasksQuery->orderBy('due_at');
273 -// } else {
274 -// $tasksQuery->orderBy('due_at', $orderBy);
275 -// }
276 -// break;
277 -//
278 -// default:
279 -// $tasksQuery->orderBy($order, $orderBy);
280 -// break;
281 -// }
282 -//
283 -// $tasks = $tasksQuery->paginate($per_page, ['*'], 'page', $requestData->page);
284 -// return [
285 -// 'tasks' => $tasks->values()->toArray(),
286 -// 'paginationInfo' => [
287 -// 'current_page' => $tasks->currentPage(),
288 -// 'last_page' => $tasks->lastPage(),
289 -// 'total' => $tasks->total(),
290 -// ],
291 -// ];
292 -//
293 -// }
294 244 public function getMemberAssociatedTasks($user_id, $requestData)
295 245 {
296 246 $taskType = $requestData['taskType'] ?? null;
297 247 $boardIds = !empty($requestData['boardIds']) ? $requestData['boardIds'] : [];
@@ -300,35 +250,72 @@
300 250 $per_page = $requestData['per_page'] ?? 15;
301 251 $page = $requestData['page'] ?? 1;
302 252 $user = User::find($user_id);
303 253
304 - if (!$user) {
254 + $loggedInUserId = get_current_user_id();
255 +
256 + $allowedBoardIds = PermissionManager::getBoardIdsForUser($loggedInUserId);
257 +
258 + if (!$user || empty($allowedBoardIds)) {
305 259 return [
306 260 'tasks' => [],
307 261 'paginationInfo' => [
308 262 'current_page' => 1,
309 263 'last_page' => 1,
264 + 'per_page' => (int) $per_page,
310 265 'total' => 0,
311 266 ],
312 267 ];
313 268 }
314 269
315 - // Get the task assigned to the user
316 - $tasksQuery = $user->tasks()->with(['stage', 'board'])->whereNull('archived_at');
270 + // Table view needs the labels column; list view ignores the extra relation.
271 + $taskRelations = ['stage', 'board', 'labels'];
317 272
318 - switch ($taskType) {
319 - case 'upcoming':
320 - $tasksQuery->upcoming();
321 - break;
322 - case 'overdue':
323 - $tasksQuery->overdue();
324 - break;
325 - case 'completed':
326 - $tasksQuery->where('status', 'closed');
327 - break;
328 - default:
329 - $tasksQuery->whereNull('due_at');
330 - break;
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 + }
331 318 }
332 319
333 320 $currentUserId = get_current_user_id();
334 321 if ($currentUserId != $user->ID && !PermissionManager::isAdmin()) {
@@ -351,14 +338,14 @@
351 338 $orderOptions = ['ASC', 'DESC'];
352 339
353 340 // Validate order and orderBy parameters
354 341 if (!in_array($order, $orderOptions) || !in_array($orderBy, $sortOptions)) {
355 - throw new \Exception(__('Invalid sort or orderBy parameter', 'fluent-boards'));
342 + throw new \Exception(esc_html__('Invalid sort or orderBy parameter', 'fluent-boards'));
356 343 }
357 344
358 345 // Apply ordering based on the specified order and orderBy
359 346 if ($orderBy === 'priority') {
360 - $tasksQuery->orderByRaw("FIELD(priority, 'High', 'Medium', 'Low') {$order}");
347 + $tasksQuery->orderByRaw("FIELD(priority, 'urgent', 'high', 'medium', 'low') {$order}");
361 348 } else if ($orderBy === 'due_at') {
362 349 $tasksQuery->orderByRaw("ISNULL(due_at), due_at {$order}");
363 350 } else {
364 351 $tasksQuery->orderBy($orderBy, $order);
@@ -370,44 +357,152 @@
370 357 'tasks' => $tasks->values()->toArray(),
371 358 'paginationInfo' => [
372 359 'current_page' => $tasks->currentPage(),
373 360 'last_page' => $tasks->lastPage(),
361 + 'per_page' => (int) $tasks->perPage(),
374 362 'total' => $tasks->total(),
375 363 ],
376 364 ];
377 365 }
378 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);
379 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 + ];
419 + }
420 +
421 +
422 + /**
423 + * Get a member's activities scoped to boards the requesting user can access.
424 + */
380 425 public function getMemberRelatedAcitivies($user_id, $page)
381 426 {
382 - $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 + })
383 457 ->orderBy('created_at', 'desc')
384 - ->with('user')->paginate(40, ['*'], 'page', $page);
458 + ->with('user')
459 + ->paginate($perPage, ['*'], 'page', $page);
385 460
386 - $activitiesToShow = array();
461 + $activitiesToShow = [];
387 462
388 463 foreach ($activities as $activity) {
389 464 if ($activity->object_type == Constant::ACTIVITY_BOARD) {
390 465 $activity->load('board');
391 -// if($activity->settings && $activity->settings['task_id']){
392 -// $activity->task = Task::findOrFail($activity->settings['task_id']);
393 -// }
394 - if (PermissionManager::userHasPermission($activity->board_id, get_current_user_id())) {
395 - $activitiesToShow[] = $activity;
396 - }
466 + $activitiesToShow[] = $activity;
397 467 } elseif ($activity->object_type == Constant::ACTIVITY_TASK) {
398 468 $activity->load('task');
399 - if ($activity->task && PermissionManager::userHasPermission($activity->task->board_id, get_current_user_id())) {
400 - $activitiesToShow[] = $activity;
401 - }
469 + $activitiesToShow[] = $activity;
402 470 }
403 471 }
472 +
404 473 return [
405 - 'activities' => $activitiesToShow,
406 - 'pagination' => $activities->toArray(),
474 + 'activities' => $activitiesToShow,
475 + 'pagination' => $this->getPaginationInfo($activities),
407 476 ];
408 477 }
409 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 +
410 505 private function getTaskById($taskId)
411 506 {
412 507 return Task::findOrFail($taskId);
413 508 }
@@ -416,23 +511,80 @@
416 511 {
417 512 if(!$user_id) {
418 513 return [];
419 514 }
420 - $boardIds = [];
421 - $boards = [];
422 515 $user = User::find($user_id);
423 516 $currentUserId = get_current_user_id();
424 - if($currentUserId != $user->ID) {
425 - if(!PermissionManager::isAdmin()){
426 - $currentUser = User::find($currentUserId);
427 - $currentUserBoardIds = $currentUser->whichBoards->pluck('id')->toArray();
428 - $boardIds = $currentUserBoardIds;
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;
429 558 }
430 559 }
431 - if (!empty($boardIds)) {
432 - $boards = $user->whichBoards()->whereIn('fbs_boards.id', $boardIds)->get();
433 - } else {
434 - $boards = $user->whichBoards;
560 +
561 + $assignedBase = $user->assignedTasks()->whereNull('archived_at')->whereNull('parent_id');
562 + if ($allowedBoardIds !== null) {
563 + $assignedBase->whereIn('board_id', $allowedBoardIds);
435 564 }
436 - return $boards;
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 + ];
437 589 }
438 590 }