CRM/backend/app/Services/LeadService.php

217 خطوط
7.5 KiB
PHP

<?php
namespace App\Services;
use App\Models\Lead;
use App\Models\PipelineStage;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
class LeadService
{
public function getFilteredLeads(array $filters): LengthAwarePaginator
{
$query = $this->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;
}
}