id); $this->assertAssignable($actor, $assigneeId, $assigneeId !== $actor->id); if (! empty($data['parent_task_id'])) { $parent = Task::findOrFail($data['parent_task_id']); Gate::forUser($actor)->authorize('view', $parent); } $task = DB::transaction(function () use ($data, $actor, $entity, $assigneeId): Task { $task = Task::create([ ...Arr::only($data, ['subject', 'description', 'priority', 'due_at', 'reminder_at', 'parent_task_id', 'estimated_minutes', 'visibility']), 'taskable_type' => $entity ? $entity::class : null, 'taskable_id' => $entity?->getKey(), 'assigned_to' => $assigneeId, 'assigned_by' => $actor->id, 'created_by' => $actor->id, 'status' => TaskStatus::Open->value, 'version' => 1, ]); ActivityLogger::log('task_created', "Task {$task->id} created", $task, null, $this->auditState($task)); return $task; }); try { TaskAssignedNotification::send($task, $actor); } catch (\Throwable) { // Notification failure must not roll back task creation. } return $this->load($task); } public function update(Task $task, array $data, User $actor): Task { return DB::transaction(function () use ($task, $data): Task { $locked = $this->locked($task, (int) $data['version']); $before = $this->auditState($locked); $locked->fill(Arr::only($data, ['subject', 'description', 'priority', 'due_at', 'reminder_at', 'estimated_minutes', 'visibility'])); $locked->version++; $locked->save(); ActivityLogger::log('task_updated', "Task {$locked->id} updated", $locked, $before, $this->auditState($locked)); return $this->load($locked); }); } public function assign(Task $task, int $assigneeId, int $version, User $actor): Task { $this->assertAssignable($actor, $assigneeId, true); $reassigned = $task->assigned_to !== null && $task->assigned_to !== $assigneeId; $updated = DB::transaction(function () use ($task, $assigneeId, $version, $actor, $reassigned): Task { $locked = $this->locked($task, $version); Gate::forUser($actor)->authorize('assign', [$locked, $assigneeId]); $before = $this->auditState($locked); $locked->update([ 'assigned_to' => $assigneeId, 'assigned_by' => $actor->id, 'version' => $locked->version + 1, ]); ActivityLogger::log($reassigned ? 'task_reassigned' : 'task_assigned', "Task {$locked->id} assignment changed", $locked, $before, $this->auditState($locked)); return $this->load($locked); }); try { TaskAssignedNotification::send($updated, $actor, $reassigned); } catch (\Throwable) { // Notification failure must not roll back assignment. } return $updated; } public function transition(Task $task, TaskStatus $target, int $version, User $actor): Task { return DB::transaction(function () use ($task, $target, $version, $actor): Task { $locked = $this->locked($task, $version); Gate::forUser($actor)->authorize('transition', $locked); $this->assertTransition($locked, $target); $before = $this->auditState($locked); $attributes = ['status' => $target->value, 'version' => $locked->version + 1]; if ($target === TaskStatus::InProgress) { $attributes['started_at'] = $locked->started_at ?? now(); $attributes['completed_at'] = null; } elseif ($target === TaskStatus::Done) { $attributes['completed_at'] = now(); } elseif ($target === TaskStatus::Open) { $attributes['completed_at'] = null; } elseif ($target === TaskStatus::Cancelled) { $attributes['completed_at'] = null; } $locked->update($attributes); ActivityLogger::log('task_'.$target->value, "Task {$locked->id} status changed", $locked, $before, $this->auditState($locked)); return $this->load($locked); }); } public function bulkAssign(array $ids, int $assigneeId, User $actor): array { Gate::forUser($actor)->authorize('bulkManage', Task::class); $this->assertAssignable($actor, $assigneeId, true); $tasks = DB::transaction(function () use ($ids, $assigneeId, $actor) { $tasks = $this->bulkTasks($ids, $actor, 'assign', $assigneeId); foreach ($tasks as $task) { $before = $this->auditState($task); $task->update(['assigned_to' => $assigneeId, 'assigned_by' => $actor->id, 'version' => $task->version + 1]); ActivityLogger::log('task_bulk_assigned', "Task {$task->id} bulk assigned", $task, $before, $this->auditState($task)); } return $tasks; }); foreach ($tasks as $task) { try { TaskAssignedNotification::send($task->fresh(), $actor, true); } catch (\Throwable) { // Assignment remains valid if notification delivery fails. } } return $tasks->map(fn (Task $task) => $this->load($task->fresh()))->all(); } public function bulkComplete(array $ids, User $actor): array { Gate::forUser($actor)->authorize('bulkManage', Task::class); $tasks = DB::transaction(function () use ($ids, $actor) { $tasks = $this->bulkTasks($ids, $actor, 'transition'); foreach ($tasks as $task) { $before = $this->auditState($task); $task->update(['status' => TaskStatus::Done->value, 'completed_at' => now(), 'version' => $task->version + 1]); ActivityLogger::log('task_bulk_completed', "Task {$task->id} bulk completed", $task, $before, $this->auditState($task)); } return $tasks; }); return $tasks->map(fn (Task $task) => $this->load($task->fresh()))->all(); } public function delete(Task $task): void { DB::transaction(function () use ($task): void { $before = $this->auditState($task); $task->delete(); ActivityLogger::log('task_deleted', "Task {$task->id} deleted", $task, $before, ['deleted' => true]); }); } private function assertAssignable(User $actor, int $assigneeId, bool $requiresPermission): User { $assignee = User::whereKey($assigneeId)->where('is_active', true)->first(); if (! $assignee) { throw ValidationException::withMessages(['assigned_to' => ['کاربر انتخاب‌شده فعال نیست.']]); } if ($requiresPermission && ! $actor->can('assign_tasks') && ! $actor->can('reassign_tasks')) { abort(403, 'مجوز تخصیص کار به دیگران را ندارید.'); } abort_unless(AccessControl::canAssignUser($actor, $assigneeId), 403, 'کاربر انتخاب‌شده خارج از محدوده تیم شما است.'); return $assignee; } private function locked(Task $task, int $version): Task { $locked = Task::whereKey($task->id)->lockForUpdate()->firstOrFail(); if ($locked->version !== $version) { throw new TaskVersionConflictException; } return $locked; } private function assertTransition(Task $task, TaskStatus $target): void { $allowed = match ($target) { TaskStatus::InProgress => [$task->status === TaskStatus::Open], TaskStatus::Done => [in_array($task->status, [TaskStatus::Open, TaskStatus::InProgress], true)], TaskStatus::Open => [in_array($task->status, [TaskStatus::Done, TaskStatus::Cancelled], true)], TaskStatus::Cancelled => [in_array($task->status, [TaskStatus::Open, TaskStatus::InProgress], true)], }; if (! $allowed[0]) { throw ValidationException::withMessages(['status' => ['این تغییر وضعیت برای وضعیت فعلی کار مجاز نیست.']]); } } private function bulkTasks(array $ids, User $actor, string $ability, ?int $assigneeId = null) { $uniqueIds = collect($ids)->map(fn ($id) => (int) $id)->unique()->values(); $tasks = Task::whereKey($uniqueIds)->lockForUpdate()->get(); if ($tasks->count() !== $uniqueIds->count()) { throw ValidationException::withMessages(['task_ids' => ['یک یا چند کار معتبر نیست.']]); } foreach ($tasks as $task) { $arguments = $ability === 'assign' ? [$task, $assigneeId] : $task; Gate::forUser($actor)->authorize($ability, $arguments); } return $tasks; } private function auditState(Task $task): array { return $task->only(['assigned_to', 'assigned_by', 'priority', 'status', 'due_at', 'reminder_at', 'visibility', 'version']); } private function load(Task $task): Task { return $task->load(['assignee:id,name,avatar', 'assigner:id,name', 'creator:id,name', 'taskable', 'parent:id,subject']); } }