user()->can('manage_scripts')) { $query->where('is_active', true)->whereHas('assignees', fn ($users) => $users->whereKey($request->user()->id)); } foreach (['category', 'lead_source'] as $filter) { if ($request->filled($filter)) { $query->where($filter, $request->string($filter)); } } if ($request->filled('search')) { $search = $request->string('search'); $query->where(fn ($scope) => $scope->where('title', 'like', "%{$search}%")->orWhere('description', 'like', "%{$search}%")); } return response()->json($query->latest()->paginate(min((int) $request->get('per_page', 15), 100))); } public function store(Request $request): JsonResponse { Gate::authorize('create', SalesScript::class); $validated = $request->validate($this->rules()); $this->authorizeParents($validated); $this->authorizeAssignees($validated['assigned_user_ids'] ?? [], $request->user()); $script = DB::transaction(function () use ($validated, $request): SalesScript { $script = SalesScript::create(collect($validated)->except(['sections', 'campaign_id', 'product_id', 'assigned_user_ids'])->all()); $this->syncSections($script, $validated['sections'] ?? []); $this->syncParents($script, $validated); $this->syncAssignees($script, $validated['assigned_user_ids'] ?? [], $request->user()); return $script; }); ActivityLogger::log('script_created', "Script {$script->title} created", $script); return response()->json($script->load('sections', 'campaign:id,name,sales_script_id', 'product:id,name,sales_script_id', 'assignees:id,name'), 201); } public function show(SalesScript $script): JsonResponse { Gate::authorize('view', $script); return response()->json($script->load('sections', 'campaign:id,name,sales_script_id', 'product:id,name,sales_script_id', 'assignees:id,name')); } public function update(Request $request, SalesScript $script): JsonResponse { Gate::authorize('update', $script); $validated = $request->validate($this->rules(partial: true)); $this->authorizeParents($validated); if ($request->has('assigned_user_ids')) { $this->authorizeAssignees($validated['assigned_user_ids'] ?? [], $request->user()); } DB::transaction(function () use ($script, $validated, $request): void { $script->update(collect($validated)->except(['sections', 'campaign_id', 'product_id', 'assigned_user_ids'])->all()); if ($request->has('sections')) { $this->syncSections($script, $validated['sections'] ?? []); } if ($request->has('campaign_id') || $request->has('product_id')) { $this->syncParents($script, $validated, true); } if ($request->has('assigned_user_ids')) { $this->syncAssignees($script, $validated['assigned_user_ids'] ?? [], $request->user()); } }); ActivityLogger::log('script_updated', "Script {$script->title} updated", $script); return response()->json($script->fresh(['sections', 'campaign:id,name,sales_script_id', 'product:id,name,sales_script_id', 'assignees:id,name'])); } public function destroy(SalesScript $script): JsonResponse { Gate::authorize('delete', $script); $title = $script->title; $script->delete(); ActivityLogger::log('script_deleted', "Script {$title} deleted"); return response()->json(['message' => 'اسکریپت حذف شد']); } private function rules(bool $partial = false): array { $required = $partial ? 'sometimes' : 'required'; return [ 'title' => "{$required}|string|max:255", 'description' => 'nullable|string', 'campaign_id' => 'nullable|exists:campaigns,id', 'product_id' => 'nullable|exists:products,id', 'version' => 'nullable|string|max:20', 'is_active' => 'nullable|boolean', 'checklist' => 'nullable|array', 'objection_handling' => 'nullable|array', 'category' => 'nullable|string|max:100', 'lead_source' => 'nullable|string|max:100', 'suggested_questions' => 'nullable|array|max:50', 'required_disclosures' => 'nullable|array|max:50', 'is_template' => 'nullable|boolean', 'assigned_user_ids' => 'nullable|array|max:200', 'assigned_user_ids.*' => 'integer|distinct|exists:users,id', 'sections' => 'nullable|array', 'sections.*.id' => 'nullable|integer|exists:script_sections,id', 'sections.*.title' => 'required|string|max:255', 'sections.*.content' => 'required|string', 'sections.*.sort_order' => 'nullable|integer|min:0|distinct', ]; } private function authorizeParents(array $validated): void { if (! empty($validated['campaign_id'])) { Gate::authorize('update', Campaign::findOrFail($validated['campaign_id'])); } if (! empty($validated['product_id'])) { Gate::authorize('update', Product::findOrFail($validated['product_id'])); } } private function syncParents(SalesScript $script, array $validated, bool $partial = false): void { if (! $partial || array_key_exists('campaign_id', $validated)) { Campaign::where('sales_script_id', $script->id)->update(['sales_script_id' => null]); if (! empty($validated['campaign_id'])) { Campaign::whereKey($validated['campaign_id'])->update(['sales_script_id' => $script->id]); } } if (! $partial || array_key_exists('product_id', $validated)) { Product::where('sales_script_id', $script->id)->update(['sales_script_id' => null]); if (! empty($validated['product_id'])) { Product::whereKey($validated['product_id'])->update(['sales_script_id' => $script->id]); } } } private function syncSections(SalesScript $script, array $sections): void { $existingIds = $script->sections()->pluck('id')->all(); $keptIds = []; DB::table('script_sections')->where('sales_script_id', $script->id)->update(['sort_order' => DB::raw('-id')]); foreach (array_values($sections) as $index => $section) { $values = ['title' => $section['title'], 'content' => $section['content'], 'sort_order' => $section['sort_order'] ?? $index]; if (! empty($section['id']) && in_array($section['id'], $existingIds, true)) { $script->sections()->whereKey($section['id'])->update($values); $keptIds[] = $section['id']; } else { $keptIds[] = $script->sections()->create($values)->id; } } $script->sections()->whereNotIn('id', $keptIds)->delete(); } private function authorizeAssignees(array $ids, User $actor): void { $users = User::whereKey($ids)->where('is_active', true)->get(); abort_unless($users->count() === count(array_unique($ids)), 422, 'یک یا چند کارشناس انتخاب‌شده معتبر نیستند.'); foreach ($users as $user) { abort_unless($user->hasRole('agent'), 422, 'اسکریپت فقط به کارشناس فروش قابل تخصیص است.'); abort_unless($actor->hasRole('admin') || AccessControl::canAssignUser($actor, $user->id), 403, 'کارشناس انتخاب‌شده خارج از تیم شما است.'); } } private function syncAssignees(SalesScript $script, array $ids, User $actor): void { $previous = $script->assignees()->pluck('users.id')->all(); $script->assignees()->syncWithPivotValues($ids, ['assigned_by' => $actor->id]); foreach (array_diff($ids, $previous) as $userId) { NotificationService::send((int) $userId, 'اسکریپت فروش جدید', "اسکریپت «{$script->title}» به شما اختصاص داده شد", 'script_assigned', [ 'script_id' => $script->id, 'url' => '/sales-scripts', ]); } } }