getFilteredLeadQuery($filters); $perPage = min(max((int) ($filters['per_page'] ?? 15), 1), 100); return $query->paginate($perPage); } public function getFilteredLeadQuery(array $filters): Builder { $filters = $this->normalizeFilters($filters); $query = Lead::with([ 'leadStatus', 'pipelineStage', 'campaign', 'assignedAgent', 'team', 'contacts.phones.lastCaller:id,name', ]); if (!empty($filters['search'])) { $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('first_name', 'like', "%{$search}%") ->orWhere('last_name', 'like', "%{$search}%") ->orWhere('phone', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%") ->orWhere('company', 'like', "%{$search}%"); }); } if (isset($filters['status_id'])) { $query->where('lead_status_id', $filters['status_id']); } if (isset($filters['pipeline_stage_id'])) { $query->where('pipeline_stage_id', $filters['pipeline_stage_id']); } if (!empty($filters['include_unassigned_for_agent']) && isset($filters['assigned_to'])) { $query->where(function ($q) use ($filters) { $q->where('assigned_to', $filters['assigned_to']) ->orWhere(function ($inner) { $inner->whereNull('assigned_to')->orWhere('is_unassigned', true); }); }); } elseif (isset($filters['assigned_to'])) { $query->where('assigned_to', $filters['assigned_to']); } if (isset($filters['team_id'])) { $query->where('team_id', $filters['team_id']); } if (isset($filters['campaign_id'])) { $query->where('campaign_id', $filters['campaign_id']); } if (isset($filters['call_result'])) { $query->where('last_call_result', $filters['call_result']); } if (isset($filters['interest_level'])) { $query->where('interest_level', $filters['interest_level']); } if (isset($filters['overdue_follow_up']) && filter_var($filters['overdue_follow_up'], FILTER_VALIDATE_BOOLEAN)) { $query->whereNotNull('next_follow_up_at')->where('next_follow_up_at', '<', now()); } if (isset($filters['source'])) { $query->where('source', $filters['source']); } if (isset($filters['priority'])) { $query->where('priority', $filters['priority']); } if (isset($filters['is_unassigned'])) { $query->where('is_unassigned', filter_var($filters['is_unassigned'], FILTER_VALIDATE_BOOLEAN)); } if (isset($filters['date_from'])) { $query->whereDate('created_at', '>=', $filters['date_from']); } if (isset($filters['date_to'])) { $query->whereDate('created_at', '<=', $filters['date_to']); } if (isset($filters['has_follow_up'])) { if (filter_var($filters['has_follow_up'], FILTER_VALIDATE_BOOLEAN)) { $query->whereNotNull('next_follow_up_at'); } else { $query->whereNull('next_follow_up_at'); } } $allowedSortFields = [ 'created_at', 'updated_at', 'first_name', 'last_name', 'company', 'priority', 'lead_score', 'last_call_at', 'next_follow_up_at', ]; $sortField = in_array($filters['sort_field'] ?? null, $allowedSortFields, true) ? $filters['sort_field'] : 'created_at'; $sortDirection = strtolower((string) ($filters['sort_direction'] ?? 'desc')) === 'asc' ? 'asc' : 'desc'; $query->orderBy($sortField, $sortDirection); return $query; } public function getGroupedFunnel(array $filters, int $perStage = 25): array { $perStage = min(max($perStage, 1), 100); $baseQuery = $this->getFilteredLeadQuery($filters); $counts = (clone $baseQuery) ->reorder() ->selectRaw('pipeline_stage_id, count(*) as aggregate') ->groupBy('pipeline_stage_id') ->pluck('aggregate', 'pipeline_stage_id'); $stages = PipelineStage::orderBy('sort_order')->get(); return [ 'total' => (int) $counts->sum(), 'stages' => $stages->map(function (PipelineStage $stage) use ($baseQuery, $counts, $perStage) { $page = (clone $baseQuery) ->where('pipeline_stage_id', $stage->id) ->paginate($perStage, ['*'], "stage_{$stage->id}_page"); return [ 'stage' => $stage, 'count' => (int) ($counts[$stage->id] ?? 0), 'leads' => $page->items(), 'pagination' => [ 'current_page' => $page->currentPage(), 'last_page' => $page->lastPage(), 'per_page' => $page->perPage(), 'total' => $page->total(), ], ]; })->values(), ]; } private function normalizeFilters(array $filters): array { $aliases = [ 'status' => 'status_id', 'stage' => 'pipeline_stage_id', 'agent' => 'assigned_to', 'sort' => 'sort_field', 'direction' => 'sort_direction', ]; foreach ($aliases as $alias => $canonical) { if (!isset($filters[$canonical]) && isset($filters[$alias])) { $filters[$canonical] = $filters[$alias]; } } return array_filter($filters, fn($value) => $value !== '' && $value !== null); } public function assignLead(Lead $lead, int $agentId, int $assignedById, string $method = 'manual'): Lead { $lead->update([ 'assigned_to' => $agentId, 'assigned_by' => $assignedById, 'is_unassigned' => false, 'pipeline_stage_id' => PipelineStage::where('slug', 'waiting_call')->value('id') ?? $lead->pipeline_stage_id, ]); \App\Models\LeadAssignment::create([ 'lead_id' => $lead->id, 'user_id' => $agentId, 'assigned_by' => $assignedById, 'method' => $method, ]); NotificationService::notifyAssignment($agentId, $lead->full_name ?: ($lead->company ?? "لید {$lead->id}")); ActivityLogger::log('lead_assigned', "Lead {$lead->id} assigned to user {$agentId}", $lead); return $lead->fresh(); } public function reassignLead(Lead $lead, int $newAgentId, int $assignedById): Lead { return $this->assignLead($lead, $newAgentId, $assignedById, 'reassign'); } public function roundRobinAssign($leads, array $agentIds, int $assignedById): array { $results = []; $index = 0; foreach ($leads as $lead) { $agentId = $agentIds[$index % count($agentIds)]; $results[] = $this->assignLead($lead, $agentId, $assignedById, 'round_robin'); $index++; } return $results; } }