user(); Gate::authorize('viewAny', Lead::class); $filters = $request->only([ 'search', 'status_id', 'pipeline_stage_id', 'assigned_to', 'status', 'stage', 'agent', 'team_id', 'campaign_id', 'source', 'priority', 'call_result', 'interest_level', 'overdue_follow_up', 'is_unassigned', 'date_from', 'date_to', 'has_follow_up', 'sort_field', 'sort_direction', 'sort', 'direction', 'per_page', ]); if ($user->hasRole('agent')) { $filters['assigned_to'] = $user->id; } elseif ($user->hasRole('supervisor')) { $teamId = $user->teams->first()?->id; if ($teamId) { $filters['team_id'] = $teamId; } } $leads = $this->leadService->getFilteredLeads($filters); return response()->json($leads); } public function funnel(Request $request): JsonResponse { $user = auth()->user(); Gate::authorize('viewAny', Lead::class); $filters = $request->only([ 'search', 'status_id', 'pipeline_stage_id', 'assigned_to', 'status', 'stage', 'agent', 'team_id', 'campaign_id', 'source', 'priority', 'call_result', 'interest_level', 'overdue_follow_up', 'is_unassigned', 'date_from', 'date_to', 'has_follow_up', 'sort_field', 'sort_direction', 'sort', 'direction', ]); if ($user->hasRole('agent')) { $filters['assigned_to'] = $user->id; } elseif ($user->hasRole('supervisor')) { $teamId = $user->teams->first()?->id; if ($teamId) { $filters['team_id'] = $teamId; } } return response()->json($this->leadService->getGroupedFunnel($filters, (int) $request->get('per_stage', 25))); } public function export(Request $request): StreamedResponse|JsonResponse { $user = auth()->user(); if (! $user?->hasRole('admin') && ! $user?->can('export_leads')) { return response()->json(['message' => 'شما مجوز خروجی گرفتن از لیدها را ندارید.'], 403); } $filters = $request->only([ 'search', 'status_id', 'pipeline_stage_id', 'assigned_to', 'status', 'stage', 'agent', 'team_id', 'campaign_id', 'source', 'priority', 'call_result', 'interest_level', 'overdue_follow_up', 'is_unassigned', 'date_from', 'date_to', 'has_follow_up', 'sort_field', 'sort_direction', 'sort', 'direction', ]); if ($user->hasRole('agent')) { $filters['assigned_to'] = $user->id; } elseif ($user->hasRole('supervisor')) { $teamId = $user->teams->first()?->id; if ($teamId) { $filters['team_id'] = $teamId; } } $exportPhoneMode = Setting::where('key', 'export_phone_mode')->value('value') ?: 'permission_based'; $canViewFullPhone = $exportPhoneMode !== 'masked_only' && ($user->hasRole('admin') || $user->can('view_full_phone')); $query = $this->leadService->getFilteredLeadQuery($filters); ActivityLogger::log('leads_exported', 'Lead export requested'); return response()->streamDownload(function () use ($query, $canViewFullPhone) { $handle = fopen('php://output', 'w'); fputcsv($handle, ['id', 'company', 'first_name', 'last_name', 'phone', 'email', 'city', 'source', 'stage', 'status', 'campaign', 'assigned_to', 'created_at']); $query->chunk(500, function ($leads) use ($handle, $canViewFullPhone) { foreach ($leads as $lead) { fputcsv($handle, [ $lead->id, $lead->company, $lead->first_name, $lead->last_name, $canViewFullPhone ? $lead->phone : $lead->masked_phone, $lead->email, $lead->city, $lead->source, $lead->pipelineStage?->name, $lead->leadStatus?->name, $lead->campaign?->name, $lead->assignedAgent?->name, optional($lead->created_at)->toDateTimeString(), ]); } }); fclose($handle); }, 'leads-export.csv', ['Content-Type' => 'text/csv; charset=UTF-8']); } public function store(Request $request): JsonResponse { Gate::authorize('create', Lead::class); $validated = $request->validate([ 'company' => 'required|string|max:255', 'first_name' => 'nullable|string|max:255', 'last_name' => 'nullable|string|max:255', 'phone' => 'required|string|max:20', 'phone_secondary' => 'nullable|string|max:20', 'email' => 'nullable|email|max:255', 'city' => 'nullable|string|max:255', 'province' => 'nullable|string|max:255', 'source' => 'nullable|string|max:255', 'product_interest' => 'nullable|string|max:255', 'priority' => 'nullable|integer|min:0|max:10', 'lead_score' => 'nullable|integer|min:0|max:100', 'interest_level' => 'nullable|in:cold,warm,hot', 'last_call_result' => 'nullable|string|max:50', 'notes' => 'nullable|string', 'tags' => 'nullable|string', 'lead_status_id' => 'nullable|exists:lead_statuses,id', 'pipeline_stage_id' => 'nullable|exists:pipeline_stages,id', 'campaign_id' => 'nullable|exists:campaigns,id', 'assigned_to' => 'nullable|exists:users,id', ]); $user = auth()->user(); $duplicatePolicy = Setting::where('key', 'duplicate_phone_policy')->value('value') ?? 'warn'; $duplicateLead = Lead::where('phone', $validated['phone'])->first(); if ($duplicateLead && $duplicatePolicy === 'block') { return response()->json(['message' => 'شماره تلفن تکراری است', 'duplicate_lead_id' => $duplicateLead->id], 422); } if ($user?->hasRole('agent')) { $validated['assigned_to'] = $user->id; } elseif ($user?->hasRole('supervisor') && ! empty($validated['assigned_to'])) { $assignee = User::find($validated['assigned_to']); $teamIds = AccessControl::teamIds($user); $assigneeTeamIds = $assignee?->teams()->pluck('teams.id')->all() ?? []; if (! array_intersect($teamIds, $assigneeTeamIds)) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } } $validated['first_name'] = $validated['first_name'] ?: $validated['company']; $validated['last_name'] = $validated['last_name'] ?: 'رابط'; $validated['pipeline_stage_id'] = $validated['pipeline_stage_id'] ?? PipelineStage::where('is_default', true)->value('id') ?? PipelineStage::orderBy('sort_order')->value('id'); $assignedTo = $validated['assigned_to'] ?? null; if (! $assignedTo) { $strategy = Setting::where('key', 'assignment_strategy')->value('value') ?? 'round_robin'; if ($strategy !== 'manual' || auth()->user()?->hasRole('admin')) { $assignedTo = User::role('agent') ->withCount('assignedLeads') ->orderBy('assigned_leads_count') ->value('id'); } } if ($assignedTo) { $validated['assigned_to'] = $assignedTo; $assignee = User::with('teams')->find($assignedTo); $validated['assigned_by'] = auth()->id(); $validated['team_id'] = $assignee?->teams->first()?->id; $validated['is_unassigned'] = false; $validated['pipeline_stage_id'] = $request->filled('pipeline_stage_id') ? $validated['pipeline_stage_id'] : (PipelineStage::where('slug', 'waiting_call')->value('id') ?? $validated['pipeline_stage_id']); } $lead = Lead::create($validated); $contact = Contact::create([ 'lead_id' => $lead->id, 'name' => trim(($lead->first_name ?? '').' '.($lead->last_name ?? '')) ?: ($lead->company ?? 'مخاطب اولیه'), 'role' => 'رابط', 'description' => 'مخاطب اولیه لید', 'status' => 'active', 'is_primary' => true, 'primary_reason' => 'مخاطب اولیه لید', 'created_by' => auth()->id(), ]); foreach (array_filter([$validated['phone'] ?? null, $validated['phone_secondary'] ?? null]) as $phone) { ContactPhone::create([ 'contact_id' => $contact->id, 'phone' => $phone, 'type' => 'mobile', 'status' => 'active', ]); } if (! empty($validated['assigned_to'])) { LeadAssignment::create([ 'lead_id' => $lead->id, 'user_id' => $validated['assigned_to'], 'assigned_by' => auth()->id(), 'method' => 'manual', ]); } ActivityLogger::log('lead_created', "Lead {$lead->full_name} created", $lead); $this->automations->dispatch('lead_created', $lead, "lead-created:{$lead->id}", ['source' => $lead->source, 'priority' => $lead->priority]); $payload = $lead->load(['leadStatus', 'pipelineStage', 'campaign'])->toArray(); if ($duplicateLead && in_array($duplicatePolicy, ['warn', 'merge_suggestion'], true)) { $payload['duplicate_warning'] = [ 'policy' => $duplicatePolicy, 'duplicate_lead_id' => $duplicateLead->id, ]; } return response()->json($payload, 201); } public function show(Lead $lead): JsonResponse { Gate::authorize('view', $lead); return response()->json( $lead->load([ 'leadStatus', 'pipelineStage', 'campaign', 'assignedAgent', 'team', 'contacts.phones.lastCaller:id,name', 'contactRelations.fromContact:id,name,role', 'contactRelations.toContact:id,name,role', 'callLogs.contact:id,name,role', 'callLogs.phone:id,phone,type,status', 'callLogs.user:id,name', 'calls' => fn ($q) => $q->with('contact:id,name,role', 'contactPhone:id,phone,type,status')->latest(), 'followUps' => fn ($q) => $q->latest(), 'notes.user:id,name', 'attachments.uploader:id,name', ]) ); } public function update(Request $request, Lead $lead): JsonResponse { Gate::authorize('update', $lead); $validated = $request->validate([ 'first_name' => 'sometimes|string|max:255', 'last_name' => 'sometimes|string|max:255', 'phone' => 'sometimes|string|max:20', 'phone_secondary' => 'nullable|string|max:20', 'email' => 'nullable|email|max:255', 'company' => 'nullable|string|max:255', 'city' => 'nullable|string|max:255', 'province' => 'nullable|string|max:255', 'source' => 'nullable|string|max:255', 'product_interest' => 'nullable|string|max:255', 'priority' => 'nullable|integer|min:0|max:10', 'lead_score' => 'nullable|integer|min:0|max:100', 'interest_level' => 'nullable|in:cold,warm,hot', 'last_call_result' => 'nullable|string|max:50', 'notes' => 'nullable|string', 'tags' => 'nullable|string', 'lead_status_id' => 'nullable|exists:lead_statuses,id', 'pipeline_stage_id' => 'nullable|exists:pipeline_stages,id', ]); if (($request->has('lead_status_id') || $request->has('pipeline_stage_id')) && Gate::denies('changeStage', $lead)) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } $lead->update($validated); ActivityLogger::log('lead_updated', "Lead {$lead->id} updated", $lead); return response()->json($lead->fresh()->load(['leadStatus', 'pipelineStage', 'campaign', 'assignedAgent'])); } public function destroy(Lead $lead): JsonResponse { Gate::authorize('delete', $lead); $lead->delete(); ActivityLogger::log('lead_deleted', "Lead {$lead->id} deleted", $lead); return response()->json(['message' => 'لید حذف شد']); } public function bulkDestroy(Request $request): JsonResponse { $validated = $request->validate([ 'ids' => 'required|array|min:1', 'ids.*' => 'integer|exists:leads,id', ]); $leads = Lead::whereIn('id', $validated['ids'])->get(); if ($leads->count() !== count(array_unique($validated['ids'])) || $leads->contains(fn (Lead $lead) => Gate::denies('delete', $lead))) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } $deleted = Lead::whereIn('id', $validated['ids'])->delete(); ActivityLogger::log('leads_bulk_deleted', "{$deleted} leads deleted"); return response()->json([ 'deleted' => $deleted, 'message' => "{$deleted} لید حذف شد", ]); } public function nextLead(): JsonResponse { $user = auth()->user(); $lead = Lead::where('assigned_to', $user->id) ->where(function ($q) { $q->whereNull('last_call_at') ->orWhere('next_follow_up_at', '<=', now()); }) ->orderBy('priority', 'desc') ->orderBy('created_at', 'asc') ->first(); if (! $lead) { $lead = Lead::where('assigned_to', $user->id) ->orderBy('priority', 'desc') ->orderBy('last_call_at', 'asc') ->first(); } if (! $lead) { return response()->json(['message' => 'لیدی برای تماس وجود ندارد'], 404); } ActivityLogger::log('lead_viewed', "Lead {$lead->id} viewed as next lead", $lead); return response()->json( $lead->load(['leadStatus', 'pipelineStage', 'campaign', 'calls' => fn ($q) => $q->latest()->limit(5)]) ); } private function canManageLead(Lead $lead): bool { $user = auth()->user(); if (! $user) { return false; } if ($user->hasRole('admin')) { return true; } if ($user->hasRole('agent')) { return $lead->assigned_to === $user->id; } if ($user->hasRole('supervisor')) { $teamIds = $user->teams->pluck('id')->toArray(); return in_array($lead->team_id, $teamIds, true) || $lead->assigned_to === $user->id; } return false; } }