PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / trunk
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration vtrunk
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
← All changes | app/Services/UserService.php +284 -128 1.20trunk 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,87 +238,10 @@
207 238 }
208 239
209 240 return $searchResult;
210 241 }
242 +
211 243
212 -// public function getMemberAssociatedTasks($user_id, $requestData)
213 -// {
214 -// $taskType = $requestData['taskType'];
215 -// $boardIds = !empty($requestData['boardIds']) ? $requestData['boardIds'] : [];
216 -// $orderBy = $requestData['orderBy'];
217 -// $order = $requestData['order'];
218 -// $per_page = 15;
219 -// $user = User::find($user_id);
220 -//
221 -// // get the task assigned to the user
222 -// $tasksQuery = $user->tasks()->with(['stage', 'board'])->whereNull('archived_at');
223 -// if ($taskType == 'upcoming') {
224 -// $tasksQuery = $tasksQuery->upcoming();
225 -// } elseif ($taskType == 'overdue') {
226 -// $tasksQuery = $tasksQuery->overdue();
227 -// } elseif ($taskType == 'completed') {
228 -// $tasksQuery = $tasksQuery->where('status', 'closed');
229 -// } else {
230 -// $tasksQuery = $tasksQuery->whereNull('due_at');
231 -// }
232 -//
233 -// $currentUserId = get_current_user_id();
234 -// if($currentUserId != $user->ID) {
235 -// if(!PermissionManager::isAdmin()){
236 -// $currentUser = User::find($currentUserId);
237 -// $currentUserBoardIds = $currentUser->whichBoards->pluck('id')->toArray();
238 -// if (empty($boardIds)) {
239 -// $boardIds = $currentUserBoardIds;
240 -// } else {
241 -// // Get the intersection of the two arrays
242 -// $boardIds = array_intersect($boardIds, $currentUserBoardIds);
243 -// }
244 -// }
245 -// }
246 -//
247 -// if($boardIds){
248 -// $tasksQuery = $tasksQuery->whereIn('board_id', $boardIds);
249 -// }
250 -//
251 -// $sortOptions = ['priority', 'due_at', 'position', 'created_at', 'title'];
252 -// $orderOptions = ['ASC', 'DESC'];
253 -//
254 -// // Validate order and orderBy parameters
255 -// if (!in_array($order, $sortOptions) || !in_array($orderBy, $orderOptions)) {
256 -// throw new \Exception(__('Invalid sort or orderBy parameter', 'fluent-boards'));
257 -// }
258 -//
259 -// // Apply ordering based on the specified order and orderBy
260 -// switch ($order) {
261 -// case 'priority':
262 -// $tasksQuery->orderByRaw("FIELD(priority, 'High', 'Medium', 'Low') {$orderBy}");
263 -// break;
264 -//
265 -// case 'due_at':
266 -// if ($orderBy === 'ASC') {
267 -// // Separate ordering for tasks with and without due dates
268 -// $tasksQuery = $tasksQuery->orderBy('due_at');
269 -// } else {
270 -// $tasksQuery->orderBy('due_at', $orderBy);
271 -// }
272 -// break;
273 -//
274 -// default:
275 -// $tasksQuery->orderBy($order, $orderBy);
276 -// break;
277 -// }
278 -//
279 -// $tasks = $tasksQuery->paginate($per_page, ['*'], 'page', $requestData->page);
280 -// return [
281 -// 'tasks' => $tasks->values()->toArray(),
282 -// 'paginationInfo' => [
283 -// 'current_page' => $tasks->currentPage(),
284 -// 'last_page' => $tasks->lastPage(),
285 -// 'total' => $tasks->total(),
286 -// ],
287 -// ];
288 -//
289 -// }
290 244 public function getMemberAssociatedTasks($user_id, $requestData)
291 245 {
292 246 $taskType = $requestData['taskType'] ?? null;
293 247 $boardIds = !empty($requestData['boardIds']) ? $requestData['boardIds'] : [];
@@ -296,35 +250,72 @@
296 250 $per_page = $requestData['per_page'] ?? 15;
297 251 $page = $requestData['page'] ?? 1;
298 252 $user = User::find($user_id);
299 253
300 - if (!$user) {
254 + $loggedInUserId = get_current_user_id();
255 +
256 + $allowedBoardIds = PermissionManager::getBoardIdsForUser($loggedInUserId);
257 +
258 + if (!$user || empty($allowedBoardIds)) {
301 259 return [
302 260 'tasks' => [],
303 261 'paginationInfo' => [
304 262 'current_page' => 1,
305 263 'last_page' => 1,
264 + 'per_page' => (int) $per_page,
306 265 'total' => 0,
307 266 ],
308 267 ];
309 268 }
310 269
311 - // Get the task assigned to the user
312 - $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'];
313 272
314 - switch ($taskType) {
315 - case 'upcoming':
316 - $tasksQuery->upcoming();
317 - break;
318 - case 'overdue':
319 - $tasksQuery->overdue();
320 - break;
321 - case 'completed':
322 - $tasksQuery->where('status', 'closed');
323 - break;
324 - default:
325 - $tasksQuery->whereNull('due_at');
326 - 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 + }
327 318 }
328 319
329 320 $currentUserId = get_current_user_id();
330 321 if ($currentUserId != $user->ID && !PermissionManager::isAdmin()) {
@@ -347,14 +338,14 @@
347 338 $orderOptions = ['ASC', 'DESC'];
348 339
349 340 // Validate order and orderBy parameters
350 341 if (!in_array($order, $orderOptions) || !in_array($orderBy, $sortOptions)) {
351 - throw new \Exception(__('Invalid sort or orderBy parameter', 'fluent-boards'));
342 + throw new \Exception(esc_html__('Invalid sort or orderBy parameter', 'fluent-boards'));
352 343 }
353 344
354 345 // Apply ordering based on the specified order and orderBy
355 346 if ($orderBy === 'priority') {
356 - $tasksQuery->orderByRaw("FIELD(priority, 'High', 'Medium', 'Low') {$order}");
347 + $tasksQuery->orderByRaw("FIELD(priority, 'urgent', 'high', 'medium', 'low') {$order}");
357 348 } else if ($orderBy === 'due_at') {
358 349 $tasksQuery->orderByRaw("ISNULL(due_at), due_at {$order}");
359 350 } else {
360 351 $tasksQuery->orderBy($orderBy, $order);
@@ -366,44 +357,152 @@
366 357 'tasks' => $tasks->values()->toArray(),
367 358 'paginationInfo' => [
368 359 'current_page' => $tasks->currentPage(),
369 360 'last_page' => $tasks->lastPage(),
361 + 'per_page' => (int) $tasks->perPage(),
370 362 'total' => $tasks->total(),
371 363 ],
372 364 ];
373 365 }
374 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 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 + */
376 425 public function getMemberRelatedAcitivies($user_id, $page)
377 426 {
378 - $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 + })
379 457 ->orderBy('created_at', 'desc')
380 - ->with('user')->paginate(40, ['*'], 'page', $page);
458 + ->with('user')
459 + ->paginate($perPage, ['*'], 'page', $page);
381 460
382 - $activitiesToShow = array();
461 + $activitiesToShow = [];
383 462
384 463 foreach ($activities as $activity) {
385 464 if ($activity->object_type == Constant::ACTIVITY_BOARD) {
386 465 $activity->load('board');
387 -// if($activity->settings && $activity->settings['task_id']){
388 -// $activity->task = Task::findOrFail($activity->settings['task_id']);
389 -// }
390 - if (PermissionManager::userHasPermission($activity->board_id, get_current_user_id())) {
391 - $activitiesToShow[] = $activity;
392 - }
466 + $activitiesToShow[] = $activity;
393 467 } elseif ($activity->object_type == Constant::ACTIVITY_TASK) {
394 468 $activity->load('task');
395 - if ($activity->task && PermissionManager::userHasPermission($activity->task->board_id, get_current_user_id())) {
396 - $activitiesToShow[] = $activity;
397 - }
469 + $activitiesToShow[] = $activity;
398 470 }
399 471 }
472 +
400 473 return [
401 - 'activities' => $activitiesToShow,
402 - 'pagination' => $activities->toArray(),
474 + 'activities' => $activitiesToShow,
475 + 'pagination' => $this->getPaginationInfo($activities),
403 476 ];
404 477 }
405 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 +
406 505 private function getTaskById($taskId)
407 506 {
408 507 return Task::findOrFail($taskId);
409 508 }
@@ -412,23 +511,80 @@
412 511 {
413 512 if(!$user_id) {
414 513 return [];
415 514 }
416 - $boardIds = [];
417 - $boards = [];
418 515 $user = User::find($user_id);
419 516 $currentUserId = get_current_user_id();
420 - if($currentUserId != $user->ID) {
421 - if(!PermissionManager::isAdmin()){
422 - $currentUser = User::find($currentUserId);
423 - $currentUserBoardIds = $currentUser->whichBoards->pluck('id')->toArray();
424 - $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;
425 558 }
426 559 }
427 - if (!empty($boardIds)) {
428 - $boards = $user->whichBoards()->whereIn('fbs_boards.id', $boardIds)->get();
429 - } else {
430 - $boards = $user->whichBoards;
560 +
561 + $assignedBase = $user->assignedTasks()->whereNull('archived_at')->whereNull('parent_id');
562 + if ($allowedBoardIds !== null) {
563 + $assignedBase->whereIn('board_id', $allowedBoardIds);
431 564 }
432 - 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 + ];
433 589 }
434 590 }