989 خطوط
41 KiB
PHP
989 خطوط
41 KiB
PHP
<?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()),
|
||
]);
|
||
}
|
||
}
|