resourceAccess->canSeeAll($request->user())) { $query->where(function ($scope) use ($request) { $scope->where('created_by', $request->user()->id) ->orWhereIn('project_id', $this->resourceAccess->projects($request->user())->select('projects.id')); }); } if ($request->filled('project_id')) { $query->where('project_id', $request->project_id); } if ($request->filled('type')) { $query->where('type', $request->type); } if ($request->filled('status')) { $query->where('status', $request->status); } if ($request->filled('priority')) { $query->where('priority', $request->priority); } $archiveState = $request->input('archive_state', 'active'); if ($archiveState === 'archived') { $query->whereNotNull('archived_at'); } elseif ($archiveState !== 'all') { $query->whereNull('archived_at'); } $perPage = min(max((int) $request->input('per_page', 15), 1), 100); $sortBy = in_array($request->input('sort_by'), ['id', 'title', 'priority', 'status', 'created_at', 'updated_at'], true) ? $request->input('sort_by') : 'created_at'; $sortDir = $request->input('sort_dir', 'desc'); $items = $query->orderBy($sortBy, $sortDir)->paginate($perPage); return response()->json([ 'success' => true, 'data' => BacklogItemResource::collection($items), 'meta' => [ 'current_page' => $items->currentPage(), 'last_page' => $items->lastPage(), 'per_page' => $items->perPage(), 'total' => $items->total(), ], 'message' => 'لیست بکلاگ', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در دریافت بکلاگ', ], 500); } } public function show(BacklogItem $backlogItem): JsonResponse { try { $backlogItem->load(['project', 'assignedSprint', 'creator']); return response()->json([ 'success' => true, 'data' => new BacklogItemResource($backlogItem), 'message' => 'اطلاعات آیتم بکلاگ', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در دریافت اطلاعات آیتم بکلاگ', ], 500); } } public function store(StoreBacklogItemRequest $request): JsonResponse { try { $data = $request->validated(); if (! empty($data['project_id'])) { abort_unless( $this->resourceAccess->projects($request->user())->whereKey($data['project_id'])->exists(), 403, 'شما به پروژه انتخاب‌شده دسترسی ندارید.' ); } $data['created_by'] = $request->user()->id; $data['status'] = 'active'; $item = BacklogItem::create($data); $item->load(['project', 'assignedSprint', 'creator']); return response()->json([ 'success' => true, 'data' => new BacklogItemResource($item), 'message' => 'آیتم بکلاگ ایجاد شد', ], 201); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در ایجاد آیتم بکلاگ', ], 500); } } public function update(Request $request, BacklogItem $backlogItem): JsonResponse { if ($backlogItem->archived_at !== null) { return response()->json([ 'success' => false, 'message' => 'بک‌لاگ آرشیوشده قابل ویرایش نیست.', ], 422); } $data = $request->validate([ 'title' => 'sometimes|required|string|max:255', 'description' => 'nullable|string', 'type' => 'sometimes|required|string|max:50', 'project_id' => 'nullable|exists:projects,id', 'priority' => ['nullable', Rule::in(['low', 'medium', 'high', 'urgent'])], 'estimated_effort' => 'nullable|numeric', 'assigned_sprint_id' => 'nullable|exists:sprints,id', ]); if (array_key_exists('project_id', $data) && $data['project_id']) { abort_unless( $this->resourceAccess->projects($request->user())->whereKey($data['project_id'])->exists(), 403, 'شما به پروژه انتخاب‌شده دسترسی ندارید.' ); } try { $backlogItem->update([...$data, 'status' => 'active']); $backlogItem->load(['project', 'assignedSprint', 'creator']); return response()->json([ 'success' => true, 'data' => new BacklogItemResource($backlogItem->fresh()), 'message' => 'آیتم بکلاگ به‌روزرسانی شد', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در به‌روزرسانی آیتم بکلاگ', ], 500); } } public function destroy(BacklogItem $backlogItem): JsonResponse { try { $backlogItem->delete(); return response()->json([ 'success' => true, 'message' => 'آیتم بکلاگ حذف شد', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'خطا در حذف آیتم بکلاگ', ], 500); } } public function convertToTask(Request $request, BacklogItem $backlogItem): JsonResponse { $data = $request->validate([ 'assignee_id' => 'required|exists:users,id', 'priority' => ['required', Rule::in(['low', 'medium', 'high', 'urgent'])], 'start_date' => 'nullable|date', 'due_date' => 'nullable|date', 'estimated_time' => 'nullable|numeric|min:0', ]); if (! $backlogItem->project_id) { throw ValidationException::withMessages([ 'project_id' => 'برای تبدیل بک‌لاگ به تسک، انتخاب پروژه الزامی است.', ]); } if (! empty($data['start_date']) && ! empty($data['due_date']) && $data['due_date'] < $data['start_date']) { throw ValidationException::withMessages([ 'due_date' => 'تاریخ پایان نمی‌تواند قبل از تاریخ شروع باشد.', ]); } $project = Project::with('members:id')->findOrFail($backlogItem->project_id); abort_unless($this->resourceAccess->canAccessProject($request->user(), $project), 403); $assigneeId = (int) $data['assignee_id']; $canAssign = $this->resourceAccess->canSeeAll($request->user()) || $assigneeId === (int) $request->user()->id || $assigneeId === (int) $project->project_manager_id || $project->members->contains('id', $assigneeId); if (! $canAssign) { throw ValidationException::withMessages([ 'assignee_id' => 'مسئول انتخاب‌شده عضو این پروژه نیست.', ]); } $task = DB::transaction(function () use ($backlogItem, $data, $request) { $lockedItem = BacklogItem::query()->lockForUpdate()->findOrFail($backlogItem->id); if ($lockedItem->archived_at !== null || $lockedItem->converted_to_task_id !== null) { throw ValidationException::withMessages([ 'backlog_item' => 'این بک‌لاگ قبلاً به تسک تبدیل و آرشیو شده است.', ]); } $task = Task::create([ 'title' => $lockedItem->title, 'description' => $lockedItem->description, 'project_id' => $lockedItem->project_id, 'assignee_id' => $data['assignee_id'], 'reporter_id' => $request->user()->id, 'created_by' => $request->user()->id, 'priority' => $data['priority'] ?? $lockedItem->priority, 'status' => 'todo', 'start_date' => $data['start_date'] ?? null, 'due_date' => $data['due_date'] ?? null, 'estimated_time' => $data['estimated_time'] ?? null, ]); $lockedItem->update([ 'status' => 'converted', 'converted_to_task_id' => $task->id, 'archived_at' => now(), ]); $this->notificationService->create( $task->assignee_id, 'task_assigned', [ 'task_id' => $task->id, 'project_id' => $task->project_id, 'url' => '/tasks', 'notifiable_type' => Task::class, 'notifiable_id' => $task->id, ], "وظیفه جدید {$task->title} به شما اختصاص داده شد" ); $this->activityLogService->log( $request->user()->id, 'convert_backlog_to_task', "بک‌لاگ {$lockedItem->title} به وظیفه تبدیل و آرشیو شد", 'backlog_item', $lockedItem->id, $task->project_id, $task->id, ['backlog_item_id' => $lockedItem->id, 'task_id' => $task->id] ); return $task; }); $task->load(['project', 'assignee', 'reporter']); return response()->json([ 'success' => true, 'data' => new TaskResource($task), 'message' => 'آیتم بک‌لاگ به وظیفه تبدیل و آرشیو شد', ], 201); } }