CRM/backend/app/Http/Controllers/Api/ScriptController.php

198 خطوط
8.9 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Campaign;
use App\Models\Product;
use App\Models\SalesScript;
use App\Models\User;
use App\Services\ActivityLogger;
use App\Services\NotificationService;
use App\Support\AccessControl;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
class ScriptController extends Controller
{
public function index(Request $request): JsonResponse
{
Gate::authorize('viewAny', SalesScript::class);
$query = SalesScript::with('sections', 'campaign:id,name,sales_script_id', 'product:id,name,sales_script_id', 'assignees:id,name');
if (! $request->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',
]);
}
}
}