CRM/backend/app/Services/ReportService.php

417 خطوط
17 KiB
PHP

<?php
namespace App\Services;
use App\Models\Lead;
use App\Models\Call;
use App\Models\FollowUp;
use App\Models\ImportBatch;
use App\Models\MergeHistory;
use App\Models\QualityReview;
use App\Models\User;
use App\Models\Team;
use App\Models\CallResult;
use Carbon\Carbon;
class ReportService
{
public function agentPerformance(int $agentId, ?string $dateFrom = null, ?string $dateTo = null): array
{
$agent = User::findOrFail($agentId);
$callsQuery = Call::where('user_id', $agentId);
$leadsQuery = Lead::where('assigned_to', $agentId);
if ($dateFrom) {
$callsQuery->whereDate('created_at', '>=', $dateFrom);
$leadsQuery->whereDate('created_at', '>=', $dateFrom);
}
if ($dateTo) {
$callsQuery->whereDate('created_at', '<=', $dateTo);
$leadsQuery->whereDate('created_at', '<=', $dateTo);
}
$totalCalls = (clone $callsQuery)->count();
$answeredCalls = (clone $callsQuery)->whereIn('result', $this->successfulResultNames())->count();
$totalLeads = (clone $leadsQuery)->count();
$wonLeads = (clone $leadsQuery)->where('final_result', 'موفق')->count();
return [
'agent' => $agent->only(['id', 'name', 'email']),
'total_calls' => $totalCalls,
'answered_calls' => $answeredCalls,
'no_answer_calls' => $totalCalls - $answeredCalls,
'answer_rate' => $totalCalls > 0 ? round(($answeredCalls / $totalCalls) * 100, 1) : 0,
'total_leads_assigned' => $totalLeads,
'won_sales' => $wonLeads,
'conversion_rate' => $totalLeads > 0 ? round(($wonLeads / $totalLeads) * 100, 1) : 0,
'avg_call_duration' => (clone $callsQuery)->avg('duration') ?? 0,
];
}
public function teamPerformance(int $teamId, ?string $dateFrom = null, ?string $dateTo = null): array
{
$team = Team::with('members')->findOrFail($teamId);
$agentIds = $team->members->pluck('id')->toArray();
$totalCalls = Call::whereIn('user_id', $agentIds);
$totalLeads = Lead::whereIn('assigned_to', $agentIds);
if ($dateFrom) {
$totalCalls->whereDate('created_at', '>=', $dateFrom);
$totalLeads->whereDate('created_at', '>=', $dateFrom);
}
if ($dateTo) {
$totalCalls->whereDate('created_at', '<=', $dateTo);
$totalLeads->whereDate('created_at', '<=', $dateTo);
}
$wonLeads = (clone $totalLeads)->where('final_result', 'موفق')->count();
$agentPerformance = [];
foreach ($team->members as $member) {
$agentPerformance[] = $this->agentPerformance($member->id, $dateFrom, $dateTo);
}
return [
'team' => $team->only(['id', 'name']),
'member_count' => count($agentIds),
'total_calls' => $totalCalls->count(),
'total_leads' => $totalLeads->count(),
'won_sales' => $wonLeads,
'conversion_rate' => $totalLeads->count() > 0 ? round(($wonLeads / $totalLeads->count()) * 100, 1) : 0,
'agents' => $agentPerformance,
];
}
public function campaignReport(int $campaignId, ?string $dateFrom = null, ?string $dateTo = null): array
{
$campaign = \App\Models\Campaign::with('assignedAgents')->findOrFail($campaignId);
$leads = Lead::where('campaign_id', $campaignId);
if ($dateFrom) {
$leads->whereDate('created_at', '>=', $dateFrom);
}
if ($dateTo) {
$leads->whereDate('created_at', '<=', $dateTo);
}
$leadIds = (clone $leads)->pluck('id');
$calls = Call::whereIn('lead_id', $leadIds);
if ($dateFrom) {
$calls->whereDate('created_at', '>=', $dateFrom);
}
if ($dateTo) {
$calls->whereDate('created_at', '<=', $dateTo);
}
$wonLeads = (clone $leads)->where('final_result', 'موفق')->count();
$totalLeads = (clone $leads)->count();
return [
'campaign' => $campaign->only(['id', 'name', 'target', 'start_date', 'end_date', 'status']),
'total_leads' => $totalLeads,
'assigned_leads' => (clone $leads)->where('is_unassigned', false)->count(),
'total_calls' => (clone $calls)->count(),
'won_sales' => $wonLeads,
'target_progress' => $campaign->target > 0 ? round(($wonLeads / $campaign->target) * 100, 1) : 0,
'conversion_rate' => $totalLeads > 0 ? round(($wonLeads / $totalLeads) * 100, 1) : 0,
];
}
public function conversionReport(?string $dateFrom = null, ?string $dateTo = null, ?int $agentId = null, ?array $agentIds = null): array
{
$leadsQuery = Lead::query();
if ($dateFrom) $leadsQuery->whereDate('created_at', '>=', $dateFrom);
if ($dateTo) $leadsQuery->whereDate('created_at', '<=', $dateTo);
if ($agentId) $leadsQuery->where('assigned_to', $agentId);
elseif (is_array($agentIds) && $agentIds !== []) $leadsQuery->whereIn('assigned_to', $agentIds);
elseif (is_array($agentIds)) $leadsQuery->whereRaw('1 = 0');
$total = (clone $leadsQuery)->count();
$won = (clone $leadsQuery)->where('final_result', 'موفق')->count();
$lost = (clone $leadsQuery)->where('final_result', 'ناموفق')->count();
$today = Carbon::today();
$now = Carbon::now();
$stages = \App\Models\PipelineStage::orderBy('sort_order')->get();
$byStage = [];
foreach ($stages as $stage) {
$count = (clone $leadsQuery)->where('pipeline_stage_id', $stage->id)->count();
$byStage[] = [
'name' => $stage->name,
'color' => $stage->color,
'count' => $count,
'percentage' => $total > 0 ? round(($count / $total) * 100, 1) : 0,
];
}
return [
'total_leads' => $total,
'by_stage' => $byStage,
'conversion_rate' => $total > 0 ? round(($won / $total) * 100, 1) : 0,
'won_leads' => $won,
'lost_leads' => $lost,
'follow_ups_due_today' => $this->filterFollowUpsByAgent(
FollowUp::whereDate('scheduled_at', $today)->where('status', 'pending'),
$agentId,
$agentIds
)->count(),
'overdue_follow_ups' => $this->filterFollowUpsByAgent(
FollowUp::where('scheduled_at', '<', $now)->where('status', 'pending'),
$agentId,
$agentIds
)->count(),
'lost_reason_analysis' => (clone $leadsQuery)
->where('final_result', 'ناموفق')
->whereNotNull('lost_reason')
->selectRaw('lost_reason as name, count(*) as count')
->groupBy('lost_reason')
->orderByDesc('count')
->get(),
'proposal_sent_not_closed' => (clone $leadsQuery)
->whereHas('pipelineStage', fn($q) => $q->where('slug', 'proposal_sent'))
->whereNull('final_result')
->count(),
];
}
public function callReport(?string $dateFrom = null, ?string $dateTo = null, ?int $agentId = null, ?array $agentIds = null): array
{
$query = Call::query();
if ($dateFrom) $query->whereDate('created_at', '>=', $dateFrom);
if ($dateTo) $query->whereDate('created_at', '<=', $dateTo);
if ($agentId) $query->where('user_id', $agentId);
elseif (is_array($agentIds) && $agentIds !== []) $query->whereIn('user_id', $agentIds);
elseif (is_array($agentIds)) $query->whereRaw('1 = 0');
$total = $query->count();
$results = \App\Models\CallResult::all();
$byResult = [];
foreach ($results as $result) {
$count = (clone $query)->where('result', $result->name)->count();
$byResult[] = [
'name' => $result->name,
'color' => $result->color,
'count' => $count,
'percentage' => $total > 0 ? round(($count / $total) * 100, 1) : 0,
];
}
return [
'total_calls' => $total,
'avg_duration' => $query->avg('duration') ?? 0,
'by_result' => $byResult,
];
}
public function followUpReport(?string $dateFrom = null, ?string $dateTo = null, ?int $agentId = null, ?array $agentIds = null): array
{
$query = FollowUp::query();
if ($dateFrom) $query->whereDate('scheduled_at', '>=', $dateFrom);
if ($dateTo) $query->whereDate('scheduled_at', '<=', $dateTo);
if ($agentId) $query->where('user_id', $agentId);
elseif (is_array($agentIds) && $agentIds !== []) $query->whereIn('user_id', $agentIds);
elseif (is_array($agentIds)) $query->whereRaw('1 = 0');
return [
'total' => $query->count(),
'pending' => (clone $query)->where('status', 'pending')->count(),
'completed' => (clone $query)->where('status', 'completed')->count(),
'overdue' => (clone $query)->where('is_overdue', true)->count(),
'items' => (clone $query)->with(['user:id,name', 'lead:id,company,first_name,last_name'])
->where(fn($q) => $q->where('is_overdue', true)->orWhere('scheduled_at', '<', Carbon::now()))
->latest('scheduled_at')
->limit(50)
->get(),
];
}
public function lostReasonReport(?string $dateFrom = null, ?string $dateTo = null, ?int $agentId = null, ?array $agentIds = null): array
{
$query = $this->scopedLeads($dateFrom, $dateTo, $agentId, $agentIds)
->where('final_result', 'ناموفق')
->whereNotNull('lost_reason');
$total = (clone $query)->count();
return [
'total_lost' => $total,
'by_reason' => (clone $query)->selectRaw('lost_reason as name, count(*) as count')
->groupBy('lost_reason')
->orderByDesc('count')
->get()
->map(fn($row) => [
'name' => $row->name,
'count' => (int) $row->count,
'percentage' => $total > 0 ? round(($row->count / $total) * 100, 1) : 0,
])->values(),
];
}
public function sourcePerformanceReport(?string $dateFrom = null, ?string $dateTo = null, ?int $agentId = null, ?array $agentIds = null): array
{
$query = $this->scopedLeads($dateFrom, $dateTo, $agentId, $agentIds);
return [
'sources' => (clone $query)
->selectRaw("COALESCE(source, 'نامشخص') as source, count(*) as total, sum(case when final_result = 'موفق' then 1 else 0 end) as won, sum(case when final_result = 'ناموفق' then 1 else 0 end) as lost")
->groupBy('source')
->orderByDesc('total')
->get()
->map(fn($row) => [
'source' => $row->source,
'total' => (int) $row->total,
'won' => (int) $row->won,
'lost' => (int) $row->lost,
'conversion_rate' => $row->total > 0 ? round(($row->won / $row->total) * 100, 1) : 0,
])->values(),
];
}
public function duplicateLeadsReport(?string $dateFrom = null, ?string $dateTo = null, ?int $agentId = null, ?array $agentIds = null): array
{
$query = $this->scopedLeads($dateFrom, $dateTo, $agentId, $agentIds);
$duplicatePhones = (clone $query)
->whereNotNull('phone')
->selectRaw('phone, count(*) as count')
->groupBy('phone')
->havingRaw('count(*) > 1')
->orderByDesc('count')
->limit(50)
->get();
$duplicateEmails = (clone $query)
->whereNotNull('email')
->selectRaw('email, count(*) as count')
->groupBy('email')
->havingRaw('count(*) > 1')
->orderByDesc('count')
->limit(50)
->get();
$duplicateCompanies = (clone $query)
->whereNotNull('company')
->selectRaw('company, count(*) as count')
->groupBy('company')
->havingRaw('count(*) > 1')
->orderByDesc('count')
->limit(50)
->get();
return [
'phone_duplicates' => $duplicatePhones,
'email_duplicates' => $duplicateEmails,
'company_duplicates' => $duplicateCompanies,
'merge_history_count' => MergeHistory::where('entity_type', 'like', '%Lead%')->count(),
];
}
public function importQualityReport(?string $dateFrom = null, ?string $dateTo = null): array
{
$query = ImportBatch::with(['user:id,name', 'campaign:id,name']);
if ($dateFrom) $query->whereDate('created_at', '>=', $dateFrom);
if ($dateTo) $query->whereDate('created_at', '<=', $dateTo);
$batches = $query->latest()->limit(50)->get();
$totalRows = max($batches->sum('total_rows'), 1);
return [
'total_batches' => $batches->count(),
'total_rows' => $batches->sum('total_rows'),
'imported_rows' => $batches->sum('imported_rows'),
'failed_rows' => $batches->sum('failed_rows'),
'skipped_rows' => $batches->sum('skipped_rows'),
'success_rate' => round(($batches->sum('imported_rows') / $totalRows) * 100, 1),
'batches' => $batches,
];
}
public function callQualityReport(?string $dateFrom = null, ?string $dateTo = null, ?int $agentId = null, ?array $agentIds = null): array
{
$query = QualityReview::with(['agent:id,name', 'reviewer:id,name']);
if ($dateFrom) $query->whereDate('created_at', '>=', $dateFrom);
if ($dateTo) $query->whereDate('created_at', '<=', $dateTo);
if ($agentId) $query->where('agent_id', $agentId);
elseif (is_array($agentIds) && $agentIds !== []) $query->whereIn('agent_id', $agentIds);
elseif (is_array($agentIds)) $query->whereRaw('1 = 0');
return [
'reviewed_calls' => (clone $query)->count(),
'average_score' => round((float) ((clone $query)->avg('overall_score') ?? 0), 1),
'low_score_count' => (clone $query)->where('overall_score', '<', 60)->count(),
'by_agent' => (clone $query)->selectRaw('agent_id, count(*) as reviews, avg(overall_score) as average_score')
->groupBy('agent_id')
->with('agent:id,name')
->get(),
'recent_reviews' => (clone $query)->latest()->limit(20)->get(),
];
}
public function bestContactTimeReport(?string $dateFrom = null, ?string $dateTo = null, ?int $agentId = null, ?array $agentIds = null): array
{
$query = Call::query();
if ($dateFrom) $query->whereDate('created_at', '>=', $dateFrom);
if ($dateTo) $query->whereDate('created_at', '<=', $dateTo);
if ($agentId) $query->where('user_id', $agentId);
elseif (is_array($agentIds) && $agentIds !== []) $query->whereIn('user_id', $agentIds);
elseif (is_array($agentIds)) $query->whereRaw('1 = 0');
$total = (clone $query)->count();
if ($total < 20) {
return [
'enough_data' => false,
'message' => 'برای تحلیل بهترین زمان تماس، حداقل ۲۰ تماس ثبت‌شده لازم است.',
'total_calls' => $total,
'by_hour' => [],
];
}
return [
'enough_data' => true,
'total_calls' => $total,
'by_hour' => (clone $query)->get(['created_at', 'result'])
->groupBy(fn(Call $call) => $call->created_at->hour)
->sortKeys()
->map(function ($calls, int $hour) {
$successful = $calls->whereIn('result', $this->successfulResultNames())->count();
return [
'hour' => $hour,
'total' => $calls->count(),
'successful' => $successful,
'success_rate' => $calls->count() > 0 ? round(($successful / $calls->count()) * 100, 1) : 0,
];
})->values(),
];
}
private function filterFollowUpsByAgent($query, ?int $agentId, ?array $agentIds = null)
{
if ($agentId) {
$query->where('user_id', $agentId);
} elseif (is_array($agentIds) && $agentIds !== []) {
$query->whereIn('user_id', $agentIds);
} elseif (is_array($agentIds)) {
$query->whereRaw('1 = 0');
}
return $query;
}
private function scopedLeads(?string $dateFrom, ?string $dateTo, ?int $agentId, ?array $agentIds)
{
$query = Lead::query();
if ($dateFrom) $query->whereDate('created_at', '>=', $dateFrom);
if ($dateTo) $query->whereDate('created_at', '<=', $dateTo);
if ($agentId) $query->where('assigned_to', $agentId);
elseif (is_array($agentIds) && $agentIds !== []) $query->whereIn('assigned_to', $agentIds);
elseif (is_array($agentIds)) $query->whereRaw('1 = 0');
return $query;
}
private function successfulResultNames(): array
{
return CallResult::where('is_positive', true)->pluck('name')->all();
}
}