217 خطوط
7.5 KiB
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;
|
|
}
|
|
}
|