PM_Console/backend/app/Http/Controllers/Api/PwaController.php

989 خطوط
41 KiB
PHP
خام سرزنش تاریخچه

مخزن ambiguous runes header

راهنمای مخزن ambiguous runes توضیحات

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\CommentResource;
use App\Http\Resources\FileResource;
use App\Models\Notification;
use App\Models\ActivityLog;
use App\Models\Comment;
use App\Models\File;
use App\Models\Project;
use App\Models\Sprint;
use App\Models\Task;
use App\Models\User;
use App\Services\ProjectProgressService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class PwaController extends Controller
{
public function __construct(protected ProjectProgressService $projectProgressService) {}
public function profile(Request $request): JsonResponse
{
$user = $request->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)
{
if ($user->hasPermission('reports.view')) {
return Task::query();
}
$manageableProjectIds = $this->manageableProjectIds($user);
return Task::query()->where(function ($query) use ($user, $manageableProjectIds) {
$query->where('assignee_id', $user->id)
->orWhere('reporter_id', $user->id)
->orWhere('created_by', $user->id)
->when($manageableProjectIds->isNotEmpty(), fn($innerQuery) => $innerQuery->orWhereIn('project_id', $manageableProjectIds));
});
}
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)
{
if ($user->hasPermission('reports.view')) {
return Project::query();
}
return Project::query()->where(function ($query) use ($user) {
$query->where('project_manager_id', $user->id)
->orWhere('created_by', $user->id)
->orWhereHas('members', fn($memberQuery) => $memberQuery->where('users.id', $user->id));
});
}
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()),
]);
}
}