user()->loadMissing(['roles.permissions', 'dept', 'departments']); $permissions = $user->roles ->flatMap(fn ($role) => $role->permissions->pluck('name')) ->unique() ->values(); return response()->json([ 'success' => true, 'data' => [ 'user' => [ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'phone' => $user->phone, 'job_title' => $user->job_title, 'department' => $user->dept?->name ?: $user->department, 'status' => $user->status ?: 'active', 'avatar' => $user->avatar, 'avatar_url' => $user->avatar ? asset('storage/'.$user->avatar) : null, 'roles' => $user->roles->map(fn ($role) => [ 'id' => $role->id, 'name' => $role->name, 'display_name' => $role->display_name, ])->values(), 'primary_role' => $user->roles->first()?->display_name ?: $user->roles->first()?->name, 'departments' => $user->departments->map(fn ($department) => [ 'id' => $department->id, 'name' => $department->name, 'role_in_team' => $department->pivot?->role_in_team, 'is_primary' => (bool) $department->pivot?->is_primary, ])->values(), ], 'access_summary' => $this->profileAccessSummary($permissions, $user), 'capabilities' => [ 'can_change_password' => true, 'can_update_profile' => true, 'can_update_avatar' => true, 'push_backend_ready' => false, ], ], 'message' => 'پروفایل موبایل', ]); } public function projects(Request $request): JsonResponse { $user = $request->user()->loadMissing(['roles.permissions']); $query = $this->visibleProjectsQuery($user) ->with(['projectManager:id,name,job_title,avatar', 'members:id,name,job_title,avatar']) ->withCount([ 'tasks', 'tasks as done_tasks_count' => fn ($taskQuery) => $taskQuery->where('status', 'done'), 'tasks as overdue_tasks_count' => fn ($taskQuery) => $taskQuery ->whereDate('due_date', '<', now()->toDateString()) ->whereNotIn('status', ['done', 'canceled', 'cancelled']), ]); $filter = $request->input('filter', 'all'); match ($filter) { 'active' => $query->where('status', 'active'), 'waiting' => $query->whereIn('status', ['waiting', 'pending']), 'completed' => $query->whereIn('status', ['completed', 'done']), 'delayed' => $query->whereHas('tasks', fn ($taskQuery) => $taskQuery ->whereDate('due_date', '<', now()->toDateString()) ->whereNotIn('status', ['done', 'canceled', 'cancelled'])), 'mine' => $query->where(function ($scopeQuery) use ($user) { $scopeQuery->where('project_manager_id', $user->id) ->orWhereHas('members', fn ($memberQuery) => $memberQuery->where('users.id', $user->id)); }), default => null, }; if ($request->filled('search')) { $search = trim($request->input('search')); $query->where('title', 'like', "%{$search}%"); } $projects = $query ->where(function ($scopeQuery) { $scopeQuery->where('is_archived', false)->orWhereNull('is_archived'); }) ->orderByRaw('case when end_date is null then 1 else 0 end') ->orderBy('end_date') ->latest() ->paginate(min((int) $request->input('per_page', 20), 50)); return response()->json([ 'success' => true, 'data' => collect($projects->items())->map(fn (Project $project) => $this->projectCard($project))->values(), 'meta' => [ 'current_page' => $projects->currentPage(), 'last_page' => $projects->lastPage(), 'per_page' => $projects->perPage(), 'total' => $projects->total(), ], 'message' => 'پروژه‌های موبایل', ]); } public function projectDetail(Request $request, Project $project): JsonResponse { $this->abortUnlessProjectVisible($request, $project); $today = now()->toDateString(); $project->load([ 'projectManager:id,name,job_title,avatar', 'members:id,name,job_title,avatar', 'tasks' => fn ($taskQuery) => $taskQuery ->with(['assignee:id,name,job_title']) ->orderByRaw("case priority when 'urgent' then 0 when 'high' then 1 when 'medium' then 2 else 3 end") ->orderByRaw('case when due_date is null then 1 else 0 end') ->orderBy('due_date') ->limit(8), ])->loadCount([ 'tasks', 'tasks as done_tasks_count' => fn ($taskQuery) => $taskQuery->where('status', 'done'), 'tasks as overdue_tasks_count' => fn ($taskQuery) => $taskQuery ->whereDate('due_date', '<', $today) ->whereNotIn('status', ['done', 'canceled', 'cancelled']), 'tasks as today_tasks_count' => fn ($taskQuery) => $taskQuery->whereDate('due_date', $today), 'members', ]); $activities = ActivityLog::query() ->where('project_id', $project->id) ->latest() ->limit(6) ->get(['id', 'action', 'description', 'created_at']) ->map(fn (ActivityLog $activity) => [ 'id' => $activity->id, 'action' => $activity->action, 'description' => $activity->description, 'created_at' => $activity->created_at, ]); return response()->json([ 'success' => true, 'data' => array_merge($this->projectCard($project), [ 'description' => $project->description, 'members_count' => $project->members_count, 'today_tasks_count' => $project->today_tasks_count, 'tasks' => $project->tasks->map(fn (Task $task) => $this->taskCard($task))->values(), 'members' => $project->members->map(fn (User $member) => [ 'id' => $member->id, 'name' => $member->name, 'job_title' => $member->job_title, 'avatar_url' => $member->avatar ? asset('storage/'.$member->avatar) : null, 'role_in_project' => $member->pivot?->role_in_project, ])->values(), 'activities' => $activities, ]), 'message' => 'جزئیات پروژه', ]); } public function updateProjectStatus(Request $request, Project $project): JsonResponse { $this->abortUnlessProjectVisible($request, $project); $data = $request->validate([ 'status' => ['required', Rule::in(['active', 'waiting', 'pending', 'paused', 'completed', 'done'])], ], [ 'status.required' => 'انتخاب وضعیت پروژه الزامی است.', 'status.in' => 'وضعیت انتخاب‌شده معتبر نیست.', ]); $status = $data['status'] === 'done' ? 'completed' : $data['status']; $status = $status === 'pending' ? 'waiting' : $status; $project->update(['status' => $status]); $project->load(['projectManager:id,name,job_title,avatar', 'members:id,name,job_title,avatar']) ->loadCount([ 'tasks', 'tasks as done_tasks_count' => fn ($taskQuery) => $taskQuery->where('status', 'done'), 'tasks as overdue_tasks_count' => fn ($taskQuery) => $taskQuery ->whereDate('due_date', '<', now()->toDateString()) ->whereNotIn('status', ['done', 'canceled', 'cancelled']), ]); return response()->json([ 'success' => true, 'data' => $this->projectCard($project), 'message' => 'وضعیت پروژه تغییر کرد.', ]); } public function notifications(Request $request): JsonResponse { $query = $request->user()->notifications() ->whereNull('dismissed_at') ->where(function ($innerQuery) { $innerQuery->whereNull('remind_at')->orWhere('remind_at', '<=', now()); }); $filter = $request->input('filter', 'all'); match ($filter) { 'unread' => $query->where(function ($innerQuery) { $innerQuery->whereNull('read_at')->orWhere('is_read', false); }), 'tasks' => $query->whereIn('type', ['task_assigned', 'task_new', 'task_created', 'task_status_changed', 'task_overdue']), 'comments' => $query->whereIn('type', ['comment_created', 'comment_new']), 'mentions' => $query->where('type', 'mention'), 'deadlines' => $query->whereIn('type', ['deadline_soon', 'task_overdue']), 'system' => $query->where('type', 'system'), default => null, }; $notifications = $query ->latest() ->paginate(min((int) $request->input('per_page', 20), 50)); return response()->json([ 'success' => true, 'data' => collect($notifications->items())->map(fn (Notification $notification) => $this->notificationPayload($notification))->values(), 'meta' => [ 'current_page' => $notifications->currentPage(), 'last_page' => $notifications->lastPage(), 'per_page' => $notifications->perPage(), 'total' => $notifications->total(), 'unread_count' => $this->unreadNotificationsCount($request), ], 'message' => 'اعلان‌های موبایل', ]); } public function notificationRead(Request $request, Notification $notification): JsonResponse { abort_unless((int) $notification->user_id === (int) $request->user()->id, 403, 'شما به این اعلان دسترسی ندارید.'); $notification->update([ 'read_at' => now(), 'is_read' => true, ]); return response()->json([ 'success' => true, 'data' => $this->notificationPayload($notification->fresh()), 'meta' => ['unread_count' => $this->unreadNotificationsCount($request)], 'message' => 'اعلان خوانده شد.', ]); } public function notificationUnread(Request $request, Notification $notification): JsonResponse { abort_unless((int) $notification->user_id === (int) $request->user()->id, 403, 'شما به این اعلان دسترسی ندارید.'); $notification->update([ 'read_at' => null, 'is_read' => false, ]); return response()->json([ 'success' => true, 'data' => $this->notificationPayload($notification->fresh()), 'meta' => ['unread_count' => $this->unreadNotificationsCount($request)], 'message' => 'اعلان به حالت خوانده‌نشده برگشت.', ]); } public function notificationRemind(Request $request, Notification $notification): JsonResponse { abort_unless((int) $notification->user_id === (int) $request->user()->id, 403, 'شما به این اعلان دسترسی ندارید.'); $notification->update([ 'remind_at' => now()->addMinutes(30), 'dismissed_at' => null, 'read_at' => null, 'is_read' => false, ]); return response()->json([ 'success' => true, 'data' => $this->notificationPayload($notification->fresh()), 'meta' => ['unread_count' => $this->unreadNotificationsCount($request)], 'message' => '۳۰ دقیقه دیگر یادآوری می‌شود.', ]); } public function notificationDismiss(Request $request, Notification $notification): JsonResponse { abort_unless((int) $notification->user_id === (int) $request->user()->id, 403, 'شما به این اعلان دسترسی ندارید.'); $notification->update([ 'dismissed_at' => now(), ]); return response()->json([ 'success' => true, 'meta' => ['unread_count' => $this->unreadNotificationsCount($request)], 'message' => 'اعلان از لیست خارج شد.', ]); } public function notificationsReadAll(Request $request): JsonResponse { $request->user()->notifications() ->where(function ($query) { $query->whereNull('read_at')->orWhere('is_read', false); }) ->update([ 'read_at' => now(), 'is_read' => true, ]); return response()->json([ 'success' => true, 'meta' => ['unread_count' => 0], 'message' => 'همه اعلان‌ها خوانده شدند.', ]); } public function tasks(Request $request): JsonResponse { $user = $request->user()->loadMissing(['roles.permissions']); $query = $this->visibleTasksQuery($user) ->with(['project:id,title,status', 'assignee:id,name,job_title']) ->withCount(['comments', 'files']); $filter = $request->input('filter', 'all'); $today = now()->toDateString(); match ($filter) { 'today' => $query->whereDate('due_date', $today), 'overdue' => $query->whereDate('due_date', '<', $today)->whereNotIn('status', ['done', 'canceled', 'cancelled']), 'active' => $query->whereIn('status', ['waiting', 'todo', 'in_progress', 'review']), 'done' => $query->where('status', 'done'), 'urgent' => $query->where('priority', 'urgent'), default => null, }; if ($request->filled('search')) { $search = trim($request->input('search')); $query->where(function ($innerQuery) use ($search) { $innerQuery->where('title', 'like', "%{$search}%") ->orWhereHas('project', fn ($projectQuery) => $projectQuery->where('title', 'like', "%{$search}%")); }); } if ($request->filled('project_id')) { $query->where('project_id', $request->input('project_id')); } $tasks = $query ->orderByRaw("case priority when 'urgent' then 0 when 'high' then 1 when 'medium' then 2 else 3 end") ->orderByRaw('case when due_date is null then 1 else 0 end') ->orderBy('due_date') ->latest() ->paginate(min((int) $request->input('per_page', 20), 50)); return response()->json([ 'success' => true, 'data' => collect($tasks->items())->map(fn (Task $task) => $this->taskCard($task))->values(), 'meta' => [ 'current_page' => $tasks->currentPage(), 'last_page' => $tasks->lastPage(), 'per_page' => $tasks->perPage(), 'total' => $tasks->total(), ], 'message' => 'تسک‌های موبایل', ]); } public function taskDetail(Request $request, Task $task): JsonResponse { $this->abortUnlessTaskVisible($request, $task); $task->load([ 'project:id,title,status', 'assignee:id,name,job_title', 'reporter:id,name,job_title', 'comments.user:id,name,job_title', 'files.user:id,name,job_title', ])->loadCount(['comments', 'files']); return response()->json([ 'success' => true, 'data' => $this->taskDetailPayload($task), 'message' => 'جزئیات تسک', ]); } public function updateTaskStatus(Request $request, Task $task): JsonResponse { $this->abortUnlessTaskVisible($request, $task); $data = $request->validate([ 'status' => ['required', Rule::in(['waiting', 'todo', 'in_progress', 'done'])], ], [ 'status.required' => 'انتخاب وضعیت الزامی است.', 'status.in' => 'وضعیت انتخاب‌شده معتبر نیست.', ]); $task->update(['status' => $data['status']]); $this->projectProgressService->calculateProgress($task->project_id); $task->load(['project:id,title,status', 'assignee:id,name,job_title'])->loadCount(['comments', 'files']); return response()->json([ 'success' => true, 'data' => $this->taskCard($task->fresh(['project', 'assignee'])->loadCount(['comments', 'files'])), 'message' => 'وضعیت تسک تغییر کرد.', ]); } public function addTaskComment(Request $request, Task $task): JsonResponse { $this->abortUnlessTaskVisible($request, $task); $data = $request->validate([ 'body' => 'required|string|max:3000', ], [ 'body.required' => 'متن کامنت الزامی است.', 'body.max' => 'متن کامنت بیش از حد طولانی است.', ]); $comment = Comment::create([ 'body' => $data['body'], 'user_id' => $request->user()->id, 'commentable_id' => $task->id, 'commentable_type' => Task::class, ])->load('user'); return response()->json([ 'success' => true, 'data' => new CommentResource($comment), 'message' => 'کامنت ثبت شد.', ], 201); } public function addTaskAttachment(Request $request, Task $task): JsonResponse { $this->abortUnlessTaskVisible($request, $task); $request->validate([ 'file' => 'required|file|max:10240|mimes:pdf,doc,docx,xls,xlsx,ppt,pptx,txt,csv,jpg,jpeg,png,webp,zip', ], [ 'file.required' => 'انتخاب فایل الزامی است.', 'file.max' => 'حجم فایل نباید بیشتر از ۱۰ مگابایت باشد.', 'file.mimes' => 'نوع فایل انتخاب‌شده مجاز نیست.', ]); $uploadedFile = $request->file('file'); $extension = strtolower($uploadedFile->getClientOriginalExtension()); $name = Str::uuid()->toString().($extension ? ".{$extension}" : ''); $path = $uploadedFile->storeAs('files', $name, 'local'); $file = File::create([ 'name' => $name, 'original_name' => $uploadedFile->getClientOriginalName(), 'path' => $path, 'mime_type' => $uploadedFile->getMimeType(), 'size' => $uploadedFile->getSize(), 'fileable_id' => $task->id, 'fileable_type' => Task::class, 'user_id' => $request->user()->id, ])->load('user'); return response()->json([ 'success' => true, 'data' => new FileResource($file), 'message' => 'فایل با موفقیت آپلود شد.', ], 201); } public function downloadTaskAttachment(Request $request, Task $task, File $file) { $this->abortUnlessTaskVisible($request, $task); if ($file->fileable_type !== Task::class || (int) $file->fileable_id !== (int) $task->id) { abort(404); } $disk = Storage::disk('local')->exists($file->path) ? 'local' : 'public'; abort_unless(Storage::disk($disk)->exists($file->path), 404); return response()->download(Storage::disk($disk)->path($file->path), $file->original_name); } public function taskOptions(Request $request): JsonResponse { $user = $request->user()->loadMissing(['roles.permissions']); if (! $user->hasPermission('tasks.create')) { return response()->json([ 'success' => false, 'message' => 'شما مجوز ایجاد تسک را ندارید.', ], 403); } $projectQuery = Project::query() ->with(['members:id,name,job_title,avatar', 'projectManager:id,name,job_title,avatar']) ->when(! $user->hasPermission('reports.view'), function ($query) use ($user) { $query->where(function ($scopeQuery) use ($user) { $scopeQuery->where('project_manager_id', $user->id) ->orWhere('created_by', $user->id) ->orWhereHas('members', fn ($memberQuery) => $memberQuery->where('users.id', $user->id)); }); }) ->where(function ($query) { $query->where('is_archived', false)->orWhereNull('is_archived'); }) ->orderBy('title'); $projects = $projectQuery->limit(100)->get(); $projectUserIds = $projects ->flatMap(fn (Project $project) => $project->members->pluck('id')->push($project->project_manager_id)) ->filter() ->unique() ->values(); $users = User::query() ->when(! $user->hasPermission('reports.view'), function ($query) use ($user, $projectUserIds) { $query->where('id', $user->id) ->when($projectUserIds->isNotEmpty(), fn ($innerQuery) => $innerQuery->orWhereIn('id', $projectUserIds)); }) ->where(function ($query) { $query->where('status', 'active')->orWhereNull('status'); }) ->orderBy('name') ->limit(200) ->get(['id', 'name', 'job_title', 'avatar']); return response()->json([ 'success' => true, 'data' => [ 'projects' => $projects->map(fn (Project $project) => [ 'id' => $project->id, 'title' => $project->title, 'members' => $project->members ->push($project->projectManager) ->filter() ->unique('id') ->values() ->map(fn (User $member) => [ 'id' => $member->id, 'name' => $member->name, 'job_title' => $member->job_title, ]), ])->values(), 'users' => $users->map(fn (User $optionUser) => [ 'id' => $optionUser->id, 'name' => $optionUser->name, 'job_title' => $optionUser->job_title, ])->values(), 'priorities' => [ ['value' => 'low', 'label' => 'کم'], ['value' => 'medium', 'label' => 'متوسط'], ['value' => 'high', 'label' => 'زیاد'], ['value' => 'urgent', 'label' => 'فوری'], ], ], 'message' => 'گزینه‌های ایجاد تسک', ]); } public function sprints(Request $request): JsonResponse { $user = $request->user()->loadMissing(['roles.permissions']); $query = Sprint::query()->with('project:id,title,status'); if (! $user->hasPermission('reports.view')) { $visibleProjectIds = $this->visibleProjectsQuery($user)->pluck('id'); $query->where(function ($scopeQuery) use ($user, $visibleProjectIds) { $scopeQuery ->when($visibleProjectIds->isNotEmpty(), fn ($innerQuery) => $innerQuery->whereIn('project_id', $visibleProjectIds)) ->orWhereHas('members', fn ($memberQuery) => $memberQuery->where('users.id', $user->id)) ->orWhereHas('tasks', function ($taskQuery) use ($user) { $taskQuery ->where('assignee_id', $user->id) ->orWhere('reporter_id', $user->id) ->orWhere('created_by', $user->id); }); }); } $sprints = $query ->orderByRaw("case when status = 'active' then 0 when status in ('planning','planned') then 1 else 2 end") ->orderByRaw('case when end_date is null then 1 else 0 end') ->orderByDesc('end_date') ->orderByDesc('start_date') ->limit(8) ->get(); return response()->json([ 'success' => true, 'data' => $sprints->map(fn (Sprint $sprint) => $this->pwaSprintPayload($sprint, $user))->values(), 'message' => 'اسپرینت‌های موبایل', ]); } public function home(Request $request): JsonResponse { $user = $request->user()->loadMissing(['roles.permissions']); $today = now()->toDateString(); $openStatuses = ['waiting', 'todo', 'in_progress', 'review']; $myTasks = Task::query() ->with(['project:id,title,status']) ->where('assignee_id', $user->id); $todayTasks = (clone $myTasks) ->whereDate('due_date', $today) ->whereIn('status', $openStatuses); $overdueTasks = (clone $myTasks) ->whereDate('due_date', '<', $today) ->whereNotIn('status', ['done', 'canceled', 'cancelled']); $activeTasks = (clone $myTasks) ->whereIn('status', ['todo', 'in_progress', 'review', 'waiting']); $importantTasks = (clone $myTasks) ->whereIn('status', $openStatuses) ->orderByRaw("case priority when 'urgent' then 0 when 'high' then 1 when 'medium' then 2 else 3 end") ->orderByRaw('case when due_date is null then 1 else 0 end') ->orderBy('due_date') ->limit(5) ->get() ->map(fn (Task $task) => $this->taskPreview($task)) ->values(); $notifications = Notification::query() ->where('user_id', $user->id) ->latest() ->limit(5) ->get() ->map(fn (Notification $notification) => [ 'id' => $notification->id, 'type' => $notification->type, 'title' => $notification->title, 'body' => $notification->body, 'data' => $notification->data, 'notifiable_type' => $notification->notifiable_type, 'notifiable_id' => $notification->notifiable_id, 'is_read' => (bool) $notification->is_read, 'created_at' => $notification->created_at, ]) ->values(); $permissions = $user->roles ->flatMap(fn ($role) => $role->permissions->pluck('name')) ->unique() ->values(); $canViewTeamScope = $user->hasAnyPermission(['reports.view', 'team.view', 'tasks.edit']); $managerSummary = null; if ($canViewTeamScope) { $manageableProjectIds = $this->manageableProjectIds($user); $teamTaskQuery = Task::query() ->when($manageableProjectIds->isNotEmpty(), fn ($query) => $query->whereIn('project_id', $manageableProjectIds)) ->when($manageableProjectIds->isEmpty() && ! $user->hasPermission('reports.view'), fn ($query) => $query->whereRaw('1 = 0')); $managerSummary = [ 'overdue_team_tasks' => (clone $teamTaskQuery) ->whereDate('due_date', '<', $today) ->whereNotIn('status', ['done', 'canceled', 'cancelled']) ->count(), 'review_tasks' => (clone $teamTaskQuery)->where('status', 'review')->count(), 'active_sprint' => Sprint::query() ->with('project:id,title') ->where('status', 'active') ->when($manageableProjectIds->isNotEmpty(), fn ($query) => $query->whereIn('project_id', $manageableProjectIds)) ->latest() ->first()?->only(['id', 'title', 'project_id', 'start_date', 'end_date']), 'delayed_members' => (clone $teamTaskQuery) ->whereDate('due_date', '<', $today) ->whereNotIn('status', ['done', 'canceled', 'cancelled']) ->whereNotNull('assignee_id') ->distinct('assignee_id') ->count('assignee_id'), ]; } return response()->json([ 'success' => true, 'data' => [ 'user' => [ 'id' => $user->id, 'name' => $user->name, 'job_title' => $user->job_title, 'role_label' => $user->roles->first()?->display_name, ], 'permissions' => $permissions, 'stats' => [ 'today_tasks' => $todayTasks->count(), 'overdue_tasks' => $overdueTasks->count(), 'active_tasks' => $activeTasks->count(), 'unread_notifications' => Notification::where('user_id', $user->id)->where('is_read', false)->count(), ], 'tasks' => $importantTasks, 'notifications' => $notifications, 'manager_summary' => $managerSummary, ], 'message' => 'خانه موبایل', ]); } private function taskPreview(Task $task): array { return [ 'id' => $task->id, 'title' => $task->title, 'status' => $task->status, 'priority' => $task->priority, 'due_date' => $task->due_date?->format('Y-m-d'), 'project' => $task->project ? [ 'id' => $task->project->id, 'title' => $task->project->title, ] : null, ]; } private function profileAccessSummary($permissions, User $user): array { $items = [ ['permissions' => ['projects.view'], 'label' => 'مشاهده پروژه‌ها'], ['permissions' => ['projects.edit', 'projects.create'], 'label' => 'مدیریت پروژه‌ها'], ['permissions' => ['tasks.view'], 'label' => 'مشاهده تسک‌ها'], ['permissions' => ['tasks.create'], 'label' => 'ایجاد تسک'], ['permissions' => ['tasks.edit'], 'label' => 'تغییر وضعیت تسک'], ['permissions' => ['comments.create'], 'label' => 'ثبت کامنت'], ['permissions' => ['files.upload'], 'label' => 'آپلود فایل'], ['permissions' => ['notifications.view'], 'label' => 'مشاهده اعلان‌ها'], ['permissions' => ['reports.view'], 'label' => 'مشاهده گزارش‌ها'], ]; $permissionSet = $permissions->flip(); $summary = collect($items) ->filter(fn ($item) => collect($item['permissions'])->contains(fn ($permission) => $permissionSet->has($permission))) ->map(fn ($item) => ['label' => $item['label']]) ->values(); $isAdmin = $user->roles->contains(fn ($role) => in_array($role->name, ['admin', 'super_admin', 'system_admin'], true)) || $permissionSet->has('roles.edit') || $permissionSet->has('settings.edit'); return [ 'is_admin' => $isAdmin, 'items' => $summary, ]; } private function manageableProjectIds($user) { if ($user->hasPermission('reports.view')) { return collect(); } return $user->managedProjects()->pluck('id') ->merge($user->projects()->pluck('projects.id')) ->unique() ->values(); } private function visibleTasksQuery(User $user) { return $this->resourceAccess->tasks($user); } private function abortUnlessTaskVisible(Request $request, Task $task): void { $user = $request->user()->loadMissing(['roles.permissions']); $allowed = $this->visibleTasksQuery($user)->where('id', $task->id)->exists(); abort_unless($allowed, 403, 'شما به این تسک دسترسی ندارید.'); } private function visibleProjectsQuery(User $user) { return $this->resourceAccess->projects($user); } private function abortUnlessProjectVisible(Request $request, Project $project): void { $user = $request->user()->loadMissing(['roles.permissions']); $allowed = $this->visibleProjectsQuery($user)->where('id', $project->id)->exists(); abort_unless($allowed, 403, 'شما به این پروژه دسترسی ندارید.'); } private function projectCard(Project $project): array { return [ 'id' => $project->id, 'title' => $project->title, 'description' => $project->description, 'status' => $project->status, 'progress' => (int) ($project->progress ?? 0), 'due_date' => $project->end_date?->format('Y-m-d'), 'manager' => $project->projectManager ? [ 'id' => $project->projectManager->id, 'name' => $project->projectManager->name, ] : null, 'task_counts' => [ 'total' => $project->tasks_count ?? 0, 'done' => $project->done_tasks_count ?? 0, 'overdue' => $project->overdue_tasks_count ?? 0, ], 'members_preview' => $project->relationLoaded('members') ? $project->members->take(4)->map(fn (User $member) => [ 'id' => $member->id, 'name' => $member->name, 'avatar_url' => $member->avatar ? asset('storage/'.$member->avatar) : null, ])->values() : [], ]; } private function notificationPayload(Notification $notification): array { $data = $notification->data ?? []; return [ 'id' => $notification->id, 'type' => $notification->type, 'title' => $notification->title, 'body' => $notification->body, 'message' => $notification->body, 'data' => [ 'task_id' => $data['task_id'] ?? null, 'project_id' => $data['project_id'] ?? null, 'comment_id' => $data['comment_id'] ?? null, 'target_type' => $data['target_type'] ?? $data['targetType'] ?? null, 'target_id' => $data['target_id'] ?? $data['targetId'] ?? null, ], 'notifiable_type' => $notification->notifiable_type, 'notifiable_id' => $notification->notifiable_id, 'is_read' => (bool) $notification->is_read || $notification->read_at !== null, 'read_at' => $notification->read_at, 'remind_at' => $notification->remind_at, 'dismissed_at' => $notification->dismissed_at, 'created_at' => $notification->created_at, ]; } private function unreadNotificationsCount(Request $request): int { return $request->user()->notifications() ->whereNull('dismissed_at') ->where(function ($outerQuery) { $outerQuery->whereNull('remind_at')->orWhere('remind_at', '<=', now()); }) ->where(function ($query) { $query->whereNull('read_at')->orWhere('is_read', false); }) ->count(); } private function taskCard(Task $task): array { $latestComment = $task->relationLoaded('comments') ? $task->comments->sortByDesc('created_at')->first() : $task->comments()->latest()->first(); return [ 'id' => $task->id, 'title' => $task->title, 'status' => $task->status, 'priority' => $task->priority, 'due_date' => $task->due_date?->format('Y-m-d'), 'created_at' => $task->created_at, 'project' => $task->project ? [ 'id' => $task->project->id, 'title' => $task->project->title, ] : null, 'assignee' => $task->assignee ? [ 'id' => $task->assignee->id, 'name' => $task->assignee->name, ] : null, 'comments_count' => $task->comments_count, 'files_count' => $task->files_count, 'latest_comment' => $latestComment ? [ 'id' => $latestComment->id, 'body' => Str::limit($latestComment->body, 90), 'created_at' => $latestComment->created_at, ] : null, ]; } private function pwaSprintPayload(Sprint $sprint, User $user): array { $today = now()->toDateString(); $tasks = $this->visibleTasksQuery($user) ->whereHas('sprints', fn ($sprintQuery) => $sprintQuery->where('sprints.id', $sprint->id)) ->with(['project:id,title,status', 'assignee:id,name,job_title']) ->withCount(['comments', 'files']) ->orderByRaw("case priority when 'urgent' then 0 when 'high' then 1 when 'medium' then 2 else 3 end") ->orderByRaw('case when due_date is null then 1 else 0 end') ->orderBy('due_date') ->get(); $total = $tasks->count(); $done = $tasks->where('status', 'done')->count(); $inProgress = $tasks->whereIn('status', ['in_progress', 'review'])->count(); $overdue = $tasks ->filter(fn (Task $task) => $task->due_date && $task->due_date->format('Y-m-d') < $today && ! in_array($task->status, ['done', 'canceled', 'cancelled'], true)) ->count(); $progress = $total > 0 ? (int) round(($done / $total) * 100) : 0; $remainingDays = $sprint->end_date ? now()->startOfDay()->diffInDays($sprint->end_date->copy()->startOfDay(), false) : null; return [ 'id' => $sprint->id, 'title' => $sprint->title, 'status' => $sprint->status, 'goal' => $sprint->goal, 'start_date' => $sprint->start_date?->format('Y-m-d'), 'end_date' => $sprint->end_date?->format('Y-m-d'), 'remaining_days' => $remainingDays, 'progress' => $progress, 'project' => $sprint->project ? [ 'id' => $sprint->project->id, 'title' => $sprint->project->title, ] : null, 'task_counts' => [ 'total' => $total, 'done' => $done, 'in_progress' => $inProgress, 'overdue' => $overdue, ], 'tasks' => $tasks->map(fn (Task $task) => $this->taskCard($task))->values(), 'capabilities' => [ 'can_create_task' => $user->hasPermission('tasks.create'), 'can_update_task_status' => $user->hasPermission('tasks.edit'), 'can_comment' => $user->hasPermission('comments.create'), ], ]; } private function taskDetailPayload(Task $task): array { return array_merge($this->taskCard($task), [ 'description' => $task->description, 'reporter' => $task->reporter ? [ 'id' => $task->reporter->id, 'name' => $task->reporter->name, ] : null, 'comments' => CommentResource::collection($task->comments->sortByDesc('created_at')->values()), 'files' => FileResource::collection($task->files->sortByDesc('created_at')->values()), ]); } }