Lead::class, 'company' => Company::class, 'contact' => Contact::class]; public function __construct(private AutomationEngine $engine) {} public function automations(Request $request): JsonResponse { abort_unless($request->user()->can('manage_automations') || $request->user()->can('view_automation_logs'), 403); $query = AutomationRule::withCount('runs')->with('creator:id,name') ->where('trigger', '!=', 'deal_stage_changed')->latest(); if (! $request->user()->hasRole('admin')) { $query->where(fn ($q) => $q->whereNull('team_id')->orWhereIn('team_id', AccessControl::teamIds($request->user()))); } return response()->json($query->get()); } public function storeAutomation(Request $request): JsonResponse { abort_unless($request->user()->can('manage_automations'), 403); $data = $request->validate([ 'name' => 'required|string|max:120', 'trigger' => 'required|in:manual,lead_created,lead_scored,sla_breached', 'conditions' => 'nullable|array|max:20', 'actions' => 'required|array|min:1|max:10', 'conditions.*.field' => 'required|string|max:80', 'conditions.*.operator' => 'required|in:equals,not_equals,greater_than,less_than,contains', 'conditions.*.value' => 'nullable', 'actions.*.type' => 'required|in:create_task,notify,set_lead_priority', 'team_id' => 'nullable|exists:teams,id', 'actions.*.subject' => 'nullable|string|max:255', 'actions.*.assigned_to' => 'nullable|exists:users,id', 'actions.*.priority' => 'nullable|in:low,normal,high,urgent', 'actions.*.due_in_minutes' => 'nullable|integer|min:1|max:525600', 'actions.*.user_id' => 'nullable|exists:users,id', 'actions.*.title' => 'nullable|string|max:255', 'actions.*.message' => 'nullable|string|max:1000', 'actions.*.value' => 'nullable|integer|min:0|max:4', 'is_active' => 'boolean', 'max_runs_per_record' => 'integer|min:1|max:20', ]); if (! $request->user()->hasRole('admin') && ! empty($data['team_id'])) { abort_unless(in_array((int) $data['team_id'], AccessControl::teamIds($request->user()), true), 403); } return response()->json(AutomationRule::create($data + ['created_by' => $request->user()->id]), 201); } public function runAutomation(Request $request, AutomationRule $automationRule): JsonResponse { abort_unless($request->user()->can('manage_automations'), 403); $data = $request->validate(['entity_type' => ['required', Rule::in(array_keys(self::ENTITIES))], 'entity_id' => 'required|integer|min:1', 'event_key' => 'nullable|string|max:150']); $subject = self::ENTITIES[$data['entity_type']]::findOrFail($data['entity_id']); abort_unless(AccessControl::canAccessEntity($request->user(), $subject), 403); $key = $data['event_key'] ?? "manual:{$automationRule->id}:{$data['entity_type']}:{$subject->id}:".Str::uuid(); return response()->json($this->engine->run($automationRule, $subject, $key), 202); } public function automationRuns(Request $request): JsonResponse { abort_unless($request->user()->can('view_automation_logs'), 403); return response()->json(AutomationRun::with('rule:id,name')->latest()->paginate(min((int) $request->get('per_page', 20), 100))); } public function customFields(Request $request): JsonResponse { $data = $request->validate(['entity_type' => ['required', Rule::in(array_keys(self::ENTITIES))]]); $role = $request->user()->roles->first()?->name; $fields = CustomFieldDefinition::where('entity_type', $data['entity_type'])->where('is_active', true) ->where(fn ($q) => $q->whereNull('visible_to_roles')->orWhereJsonContains('visible_to_roles', $role))->orderBy('sort_order')->get(); return response()->json($fields); } public function storeCustomField(Request $request): JsonResponse { abort_unless($request->user()->can('manage_custom_fields'), 403); $data = $request->validate([ 'entity_type' => ['required', Rule::in(array_keys(self::ENTITIES))], 'key' => ['required', 'alpha_dash', 'max:80'], 'label' => 'required|string|max:120', 'type' => 'required|in:text,textarea,number,date,datetime,boolean,select,multiselect', 'options' => 'nullable|array|max:100', 'validation' => 'nullable|array|max:20', 'visible_to_roles' => 'nullable|array|max:3', 'default_value' => 'nullable|string|max:1000', 'sort_order' => 'integer|min:0|max:1000', 'is_required' => 'boolean', 'is_active' => 'boolean', 'is_filterable' => 'boolean', 'is_searchable' => 'boolean', ]); abort_if(CustomFieldDefinition::where('entity_type', $data['entity_type'])->where('key', $data['key'])->exists(), 422, 'کلید فیلد تکراری است.'); return response()->json(CustomFieldDefinition::create($data + ['created_by' => $request->user()->id]), 201); } public function values(Request $request, string $entityType, int $entityId): JsonResponse { $subject = $this->subject($request, $entityType, $entityId); return response()->json(CustomFieldValue::with('definition')->where('fieldable_type', $subject::class)->where('fieldable_id', $subject->id)->get()); } public function updateValues(Request $request, string $entityType, int $entityId): JsonResponse { $subject = $this->subject($request, $entityType, $entityId); $data = $request->validate(['values' => 'required|array|max:100']); $definitions = CustomFieldDefinition::where('entity_type', $entityType)->where('is_active', true)->get()->keyBy('key'); foreach ($data['values'] as $key => $value) { $definition = $definitions->get($key); if (! $definition) { continue; } $column = match ($definition->type) { 'number' => 'value_number', 'date' => 'value_date', 'datetime' => 'value_datetime', 'boolean' => 'value_boolean', 'multiselect' => 'value_json', 'textarea' => 'value_text', default => 'value_string' }; CustomFieldValue::updateOrCreate([ 'custom_field_definition_id' => $definition->id, 'fieldable_type' => $subject::class, 'fieldable_id' => $subject->id, ], [$column => $value, 'updated_by' => $request->user()->id]); } ActivityLogger::log('custom_fields_updated', "Custom fields updated for {$entityType} {$entityId}", $subject); return $this->values($request, $entityType, $entityId); } private function subject(Request $request, string $type, int $id): Model { abort_unless(isset(self::ENTITIES[$type]), 404); $subject = self::ENTITIES[$type]::findOrFail($id); abort_unless(AccessControl::canAccessEntity($request->user(), $subject), 403); return $subject; } }