CRM/backend/app/Services/DashboardService.php

582 خطوط
29 KiB
PHP

<?php
namespace App\Services;
use App\Models\ActivityLog;
use App\Models\Call;
use App\Models\CallResult;
use App\Models\Campaign;
use App\Models\FollowUp;
use App\Models\ImportBatch;
use App\Models\Lead;
use App\Models\LeadStatus;
use App\Models\PipelineStage;
use App\Models\QualityReview;
use App\Models\Setting;
use App\Models\Task;
use App\Models\Team;
use App\Models\User;
use Carbon\Carbon;
class DashboardService
{
public function admin(): array
{
$today = Carbon::today();
$now = Carbon::now();
$personalAgenda = $this->personalAgenda((int) auth()->id(), $today, $now);
return [
'total_leads' => Lead::count(),
'new_leads_today' => Lead::whereDate('created_at', $today)->count(),
'calls_today' => Call::whereDate('created_at', $today)->count(),
'successful_calls_today' => Call::whereDate('created_at', $today)
->whereIn('result', $this->successfulResultNames())
->count(),
'conversions_this_month' => Lead::where('final_result', 'موفق')
->whereMonth('updated_at', $today->month)
->whereYear('updated_at', $today->year)
->count(),
'assigned_leads' => Lead::where('is_unassigned', false)->count(),
'unassigned_leads' => Lead::where('is_unassigned', true)->count(),
'leads_contacted_today' => Lead::whereDate('last_call_at', $today)->count(),
'total_calls_today' => Call::whereDate('created_at', $today)->count(),
'answered_calls' => Call::whereDate('created_at', $today)
->whereIn('result', $this->successfulResultNames())
->count(),
'no_answer_calls' => Call::whereDate('created_at', $today)
->whereIn('result', ['پاسخ نداد', 'اشغال بود', 'خاموش بود'])
->count(),
'average_call_duration' => Call::whereDate('created_at', $today)->avg('duration') ?? 0,
'won_sales' => Lead::where('final_result', 'موفق')->count(),
'lost_sales' => Lead::where('final_result', 'ناموفق')->count(),
'conversion_rate' => $this->calculateConversionRate(),
'follow_up_backlog' => FollowUp::where('status', 'pending')->whereDate('scheduled_at', '<=', $now)->count(),
'overdue_follow_ups' => FollowUp::where('status', 'pending')
->where(fn ($q) => $q->where('is_overdue', true)->orWhere('scheduled_at', '<', $now))
->count(),
'overdue_leads' => Lead::whereNotNull('next_follow_up_at')->where('next_follow_up_at', '<', $now)->whereNull('final_result')->count(),
'pipeline_data' => $this->pipelineSummary(),
'funnel_conversion_chart' => $this->funnelConversion(),
'call_trend_chart' => $this->callTrend(),
'source_performance_chart' => $this->sourcePerformance(),
'campaign_performance' => $this->campaignPerformance(),
'team_performance' => $this->teamPerformance(),
'top_agents' => $this->agentPerformance(null, 5),
'low_performance_alerts' => $this->lowPerformanceAlerts(),
'recent_imports' => ImportBatch::with('user:id,name')
->latest()
->limit(5)
->get(['id', 'user_id', 'filename', 'total_rows', 'imported_rows', 'failed_rows', 'skipped_rows', 'status', 'created_at']),
'voip_status' => $this->voipStatus(),
'system_health' => [
'pending_follow_ups' => FollowUp::where('status', 'pending')->count(),
'failed_imports_today' => ImportBatch::whereDate('created_at', $today)->where('failed_rows', '>', 0)->count(),
],
'leads_by_status' => LeadStatus::withCount('leads')
->orderBy('sort_order')
->get()
->map(fn (LeadStatus $status) => [
'status' => $status->name,
'count' => $status->leads_count,
'color' => $status->color,
])
->values(),
'recent_activities' => ActivityLog::with('user:id,name')
->latest()
->limit(10)
->get(),
'task_widgets' => [
'overdue' => Task::active()->whereNotNull('due_at')->where('due_at', '<', $now)->count(),
'unassigned' => Task::active()->whereNull('assigned_to')->count(),
'by_status' => Task::selectRaw('status, count(*) as total')->groupBy('status')->pluck('total', 'status'),
'by_priority' => Task::selectRaw('priority, count(*) as total')->groupBy('priority')->pluck('total', 'priority'),
],
'my_task_widgets' => $personalAgenda['tasks'],
'my_follow_up_widgets' => $personalAgenda['follow_ups'],
'live_charts' => $this->liveCharts(),
];
}
public function supervisor(?int $teamId = null): array
{
$teamId = $teamId ?? auth()->user()->teams->first()?->id;
$agentIds = $teamId ? Team::find($teamId)?->members->pluck('id')->toArray() : [];
$today = Carbon::today();
$now = Carbon::now();
$personalAgenda = $this->personalAgenda((int) auth()->id(), $today, $now);
return [
'total_leads' => Lead::whereIn('assigned_to', $agentIds)->count(),
'new_leads_today' => Lead::whereIn('assigned_to', $agentIds)->whereDate('created_at', $today)->count(),
'calls_today' => Call::whereIn('user_id', $agentIds)->whereDate('created_at', $today)->count(),
'conversions_this_month' => Lead::whereIn('assigned_to', $agentIds)->where('final_result', 'موفق')
->whereMonth('updated_at', $today->month)->whereYear('updated_at', $today->year)->count(),
'total_team_leads' => Lead::whereIn('assigned_to', $agentIds)->count(),
'team_calls_today' => Call::whereIn('user_id', $agentIds)->whereDate('created_at', $today)->count(),
'team_successful_calls_today' => Call::whereIn('user_id', $agentIds)->whereDate('created_at', $today)->whereIn('result', $this->successfulResultNames())->count(),
'team_won_sales' => Lead::whereIn('assigned_to', $agentIds)
->where('final_result', 'موفق')
->count(),
'team_conversion_rate' => $this->calculateTeamConversionRate($agentIds),
'online_agents' => User::whereIn('id', $agentIds)
->where('last_login_at', '>=', now()->subMinutes(15))->count(),
'overdue_follow_ups' => FollowUp::whereIn('user_id', $agentIds)
->where('status', 'pending')->where(fn ($q) => $q->where('is_overdue', true)->orWhere('scheduled_at', '<', $now))->count(),
'leads_without_calls' => Lead::whereIn('assigned_to', $agentIds)->whereNull('last_call_at')->count(),
'team_stats' => $this->agentPerformance($agentIds),
'leads_stuck_by_stage' => $this->stuckLeadsByStage($agentIds),
'calls_needing_review' => Call::whereIn('user_id', $agentIds)
->whereNotNull('recording_url')
->whereDoesntHave('qualityReview')
->latest()
->limit(10)
->get(['id', 'lead_id', 'user_id', 'result', 'duration', 'created_at']),
'quality_review_summary' => $this->qualitySummary($agentIds),
'agents_behind_target' => $this->agentsBehindTarget($agentIds),
'hot_leads' => Lead::with('assignedAgent:id,name')
->whereIn('assigned_to', $agentIds)
->where(fn ($q) => $q->where('interest_level', 'hot')->orWhere('priority', '>=', 8))
->latest()
->limit(10)
->get(['id', 'company', 'first_name', 'last_name', 'interest_level', 'priority', 'assigned_to', 'next_follow_up_at']),
'important_team_alerts' => $this->teamAlerts($agentIds),
'task_widgets' => [
'overdue' => Task::active()->whereIn('assigned_to', $agentIds)->whereNotNull('due_at')->where('due_at', '<', $now)->count(),
'unassigned' => Task::active()->whereNull('assigned_to')->whereIn('created_by', array_values(array_unique(array_merge($agentIds, [auth()->id()]))))->count(),
'workload' => User::whereIn('id', $agentIds)->orderBy('name')->get(['id', 'name'])->map(fn (User $agent) => [
'user_id' => $agent->id,
'name' => $agent->name,
'open_tasks' => Task::active()->where('assigned_to', $agent->id)->count(),
'overdue_tasks' => Task::active()->where('assigned_to', $agent->id)->whereNotNull('due_at')->where('due_at', '<', $now)->count(),
])->values(),
],
'my_task_widgets' => $personalAgenda['tasks'],
'my_follow_up_widgets' => $personalAgenda['follow_ups'],
'live_charts' => $this->liveCharts($agentIds),
];
}
public function agent(?int $userId = null): array
{
$userId = $userId ?? auth()->id();
$today = Carbon::today();
$pendingFollowUps = FollowUp::where('user_id', $userId)->where('status', 'pending');
$todayFollowUps = (clone $pendingFollowUps)->whereDate('scheduled_at', $today)->count();
$overdueFollowUps = (clone $pendingFollowUps)
->where(fn ($query) => $query->where('is_overdue', true)->orWhere('scheduled_at', '<', Carbon::now()))
->count();
return [
'my_leads' => Lead::where('assigned_to', $userId)->count(),
'my_calls_today' => Call::where('user_id', $userId)->whereDate('created_at', $today)->count(),
'my_follow_ups_today' => $todayFollowUps,
'today_follow_ups' => $todayFollowUps,
'overdue_follow_ups' => $overdueFollowUps,
'calls_today' => Call::where('user_id', $userId)->whereDate('created_at', $today)->count(),
'answered_calls_today' => Call::where('user_id', $userId)->whereDate('created_at', $today)
->whereIn('result', $this->successfulResultNames())->count(),
'won_sales' => Lead::where('assigned_to', $userId)
->where('final_result', 'موفق')
->count(),
'conversion_rate' => $this->calculateAgentConversionRate($userId),
'daily_call_target' => $this->intSetting('daily_call_target', 40),
'successful_call_target' => $this->intSetting('successful_call_target', 15),
'follow_up_target' => $this->intSetting('follow_up_target', 20),
'progress' => [
'calls' => $this->progress(Call::where('user_id', $userId)->whereDate('created_at', $today)->count(), $this->intSetting('daily_call_target', 40)),
'successful_calls' => $this->progress(Call::where('user_id', $userId)->whereDate('created_at', $today)->whereIn('result', $this->successfulResultNames())->count(), $this->intSetting('successful_call_target', 15)),
'follow_ups' => $this->progress(FollowUp::where('user_id', $userId)->whereDate('scheduled_at', $today)->count(), $this->intSetting('follow_up_target', 20)),
],
'next_call' => Lead::where('assigned_to', $userId)
->whereNull('final_result')
->orderByRaw('CASE WHEN next_follow_up_at IS NOT NULL AND next_follow_up_at <= ? THEN 0 ELSE 1 END', [Carbon::now()])
->orderByDesc('priority')
->oldest('last_call_at')
->first(['id', 'company', 'first_name', 'last_name', 'priority', 'last_call_result', 'next_follow_up_at']),
'hot_leads' => Lead::where('assigned_to', $userId)
->where(fn ($q) => $q->where('interest_level', 'hot')->orWhere('priority', '>=', 8))
->latest()
->limit(5)
->get(['id', 'company', 'first_name', 'last_name', 'interest_level', 'priority', 'next_follow_up_at']),
'urgent_tasks' => FollowUp::with('lead:id,company,first_name,last_name')
->where('user_id', $userId)
->where('status', 'pending')
->where('scheduled_at', '<=', Carbon::now()->addDay())
->orderBy('scheduled_at')
->limit(8)
->get(['id', 'lead_id', 'scheduled_at', 'notes', 'status']),
'follow_up_widgets' => [
'today' => $todayFollowUps,
'overdue' => $overdueFollowUps,
'next' => (clone $pendingFollowUps)
->with('lead:id,company,first_name,last_name')
->orderBy('scheduled_at')
->limit(8)
->get(['id', 'lead_id', 'scheduled_at', 'notes', 'status', 'is_overdue']),
],
'suggested_next_action' => $this->suggestedNextAction($userId),
'task_widgets' => [
'today' => Task::active()->where('assigned_to', $userId)->whereDate('due_at', $today)->count(),
'overdue' => Task::active()->where('assigned_to', $userId)->whereNotNull('due_at')->where('due_at', '<', Carbon::now())->count(),
'next' => Task::active()->where('assigned_to', $userId)->whereNotNull('due_at')
->orderBy('due_at')->limit(5)->get(['id', 'subject', 'priority', 'status', 'due_at', 'version']),
],
'live_charts' => $this->liveCharts([$userId]),
];
}
private function liveCharts(?array $agentIds = null): array
{
$leadScope = fn ($query) => is_array($agentIds) ? $query->whereIn('assigned_to', $agentIds) : $query;
$callScope = fn ($query) => is_array($agentIds) ? $query->whereIn('user_id', $agentIds) : $query;
$followUpScope = fn ($query) => is_array($agentIds) ? $query->whereIn('user_id', $agentIds) : $query;
$leads = $leadScope(Lead::query());
$leadCount = (clone $leads)->count();
$statusRows = LeadStatus::query()->where('is_active', true)->orderBy('sort_order')->get()
->map(fn (LeadStatus $status) => [
'label' => $status->name,
'value' => $leadScope(Lead::where('lead_status_id', $status->id))->count(),
])
->filter(fn (array $row) => $row['value'] > 0)
->sortByDesc('value')
->values();
$leadsWithoutStatus = max(0, $leadCount - $statusRows->sum('value'));
if ($leadsWithoutStatus > 0) {
$statusRows->push(['label' => 'بدون وضعیت', 'value' => $leadsWithoutStatus]);
$statusRows = $statusRows->sortByDesc('value')->values();
}
if ($statusRows->count() > 5) {
$other = $statusRows->slice(4)->sum('value');
$statusRows = $statusRows->take(4)->push(['label' => 'سایر وضعیت‌ها', 'value' => $other]);
}
$todayCalls = $callScope(Call::whereDate('created_at', Carbon::today()));
$successfulCalls = (clone $todayCalls)->whereIn('result', $this->successfulResultNames())->count();
$unansweredCalls = (clone $todayCalls)->whereIn('result', ['پاسخ نداد', 'اشغال بود', 'خاموش بود'])->count();
$otherCalls = max(0, (clone $todayCalls)->count() - $successfulCalls - $unansweredCalls);
$wonCount = (clone $leads)->where('final_result', 'موفق')->count();
$todayFollowUps = $followUpScope(FollowUp::whereDate('scheduled_at', Carbon::today()));
$followUpCount = (clone $todayFollowUps)->count();
$completedFollowUps = (clone $todayFollowUps)->where('status', 'completed')->count();
$callCount = (clone $todayCalls)->count();
return [
'updated_at' => Carbon::now()->toISOString(),
'lead_status' => $this->chartRows($statusRows->all()),
'call_outcomes' => $this->chartRows([
['label' => 'موفق', 'value' => $successfulCalls],
['label' => 'بدون پاسخ', 'value' => $unansweredCalls],
['label' => 'سایر', 'value' => $otherCalls],
]),
'performance' => $this->chartRows([
['label' => 'تبدیل فروش', 'value' => $this->percent($wonCount, $leadCount)],
['label' => 'موفقیت تماس', 'value' => $this->percent($successfulCalls, $callCount)],
['label' => 'هدف تماس روزانه', 'value' => $this->percent($callCount, $this->intSetting('daily_call_target', 40))],
['label' => 'تکمیل پیگیری', 'value' => $this->percent($completedFollowUps, $followUpCount)],
['label' => 'کامل بودن داده', 'value' => $this->percent((clone $leads)->whereNotNull('email')->whereNotNull('phone')->count(), $leadCount)],
]),
];
}
private function personalAgenda(int $userId, Carbon $today, Carbon $now): array
{
$pendingFollowUps = FollowUp::where('user_id', $userId)->where('status', 'pending');
return [
'tasks' => [
'today' => Task::active()->where('assigned_to', $userId)->whereDate('due_at', $today)->count(),
'overdue' => Task::active()->where('assigned_to', $userId)->whereNotNull('due_at')->where('due_at', '<', $now)->count(),
'next' => Task::active()->where('assigned_to', $userId)->whereNotNull('due_at')
->orderBy('due_at')->limit(8)->get(['id', 'subject', 'priority', 'status', 'due_at', 'version']),
],
'follow_ups' => [
'today' => (clone $pendingFollowUps)->whereDate('scheduled_at', $today)->count(),
'overdue' => (clone $pendingFollowUps)
->where(fn ($query) => $query->where('is_overdue', true)->orWhere('scheduled_at', '<', $now))
->count(),
'next' => (clone $pendingFollowUps)
->with('lead:id,company,first_name,last_name')
->orderBy('scheduled_at')
->limit(8)
->get(['id', 'lead_id', 'scheduled_at', 'notes', 'status', 'is_overdue']),
],
];
}
private function chartRows(array $rows): array
{
$palette = ['#2563EB', '#0D9488', '#D97706', '#7C3AED', '#DC2626'];
return collect($rows)->values()->map(fn (array $row, int $index) => [
...$row,
'color' => $palette[$index % count($palette)],
])->all();
}
private function percent(int $value, int $total): float
{
return $total > 0 ? min(100, round(($value / $total) * 100, 1)) : 0;
}
private function calculateConversionRate(): float
{
$total = Lead::count();
if ($total === 0) {
return 0;
}
$won = Lead::where('final_result', 'موفق')->count();
return round(($won / $total) * 100, 1);
}
private function pipelineSummary()
{
return PipelineStage::query()
->where('is_active', true)
->orderBy('sort_order')
->get()
->groupBy('name')
->map(function ($stages, string $name) {
$ids = $stages->pluck('id');
$first = $stages->first();
return [
'stage' => $name,
'count' => Lead::whereIn('pipeline_stage_id', $ids)->count(),
'color' => $first->color,
];
})
->values();
}
private function calculateTeamConversionRate(array $agentIds): float
{
$total = Lead::whereIn('assigned_to', $agentIds)->count();
if ($total === 0) {
return 0;
}
$won = Lead::whereIn('assigned_to', $agentIds)
->where('final_result', 'موفق')
->count();
return round(($won / $total) * 100, 1);
}
private function calculateAgentConversionRate(int $userId): float
{
$total = Lead::where('assigned_to', $userId)->count();
if ($total === 0) {
return 0;
}
$won = Lead::where('assigned_to', $userId)
->where('final_result', 'موفق')
->count();
return round(($won / $total) * 100, 1);
}
private function successfulResultNames(): array
{
return CallResult::where('is_positive', true)->pluck('name')->all();
}
private function intSetting(string $key, int $fallback): int
{
return (int) (Setting::where('key', $key)->value('value') ?: $fallback);
}
private function progress(int $value, int $target): array
{
return ['value' => $value, 'target' => $target, 'percent' => $target > 0 ? min(100, round(($value / $target) * 100, 1)) : 0];
}
private function callTrend(?array $agentIds = null): array
{
return collect(range(6, 0))->map(function (int $daysAgo) use ($agentIds) {
$date = Carbon::today()->subDays($daysAgo);
$query = Call::whereDate('created_at', $date);
if (is_array($agentIds)) {
$query->whereIn('user_id', $agentIds);
}
return [
'date' => $date->toDateString(),
'calls' => (clone $query)->count(),
'successful' => (clone $query)->whereIn('result', $this->successfulResultNames())->count(),
];
})->values()->all();
}
private function sourcePerformance(?array $agentIds = null): array
{
$query = Lead::query();
if (is_array($agentIds)) {
$query->whereIn('assigned_to', $agentIds);
}
return $query->selectRaw("COALESCE(source, 'نامشخص') as source, count(*) as total, sum(case when final_result = 'موفق' then 1 else 0 end) as won")
->groupBy('source')
->orderByDesc('total')
->limit(10)
->get()
->map(fn ($row) => [
'source' => $row->source,
'total' => (int) $row->total,
'won' => (int) $row->won,
'conversion_rate' => $row->total > 0 ? round(($row->won / $row->total) * 100, 1) : 0,
])->all();
}
private function campaignPerformance(): array
{
return Campaign::withCount('leads')
->latest()
->limit(8)
->get()
->map(fn (Campaign $campaign) => [
'id' => $campaign->id,
'name' => $campaign->name,
'target' => $campaign->target,
'leads' => $campaign->leads_count,
'won' => Lead::where('campaign_id', $campaign->id)->where('final_result', 'موفق')->count(),
])->values()->all();
}
private function teamPerformance(): array
{
return Team::with('members:id,name')->get()->map(function (Team $team) {
$agentIds = $team->members->pluck('id')->all();
return [
'id' => $team->id,
'name' => $team->name,
'agents' => count($agentIds),
'leads' => Lead::whereIn('assigned_to', $agentIds)->count(),
'calls_today' => Call::whereIn('user_id', $agentIds)->whereDate('created_at', Carbon::today())->count(),
'conversion_rate' => $this->calculateTeamConversionRate($agentIds),
];
})->values()->all();
}
private function agentPerformance(?array $agentIds = null, ?int $limit = null): array
{
$users = User::role('agent')->when(is_array($agentIds), fn ($q) => $q->whereIn('id', $agentIds))->orderBy('name')->get();
$rows = $users->map(function (User $agent) {
$todayCalls = Call::where('user_id', $agent->id)->whereDate('created_at', Carbon::today())->count();
$successful = Call::where('user_id', $agent->id)->whereDate('created_at', Carbon::today())->whereIn('result', $this->successfulResultNames())->count();
return [
'agent_id' => $agent->id,
'agent_name' => $agent->name,
'leads' => Lead::where('assigned_to', $agent->id)->count(),
'calls' => $todayCalls,
'successful_calls' => $successful,
'conversions' => Lead::where('assigned_to', $agent->id)->where('final_result', 'موفق')->count(),
'call_target_percent' => $this->progress($todayCalls, $this->intSetting('daily_call_target', 40))['percent'],
];
})->sortByDesc('calls')->values();
return $limit ? $rows->take($limit)->values()->all() : $rows->all();
}
private function lowPerformanceAlerts(): array
{
return collect($this->agentPerformance())->filter(fn ($row) => $row['call_target_percent'] < 50)
->map(fn ($row) => ['agent_id' => $row['agent_id'], 'agent_name' => $row['agent_name'], 'message' => 'کمتر از ۵۰٪ هدف تماس امروز انجام شده است.'])
->values()
->all();
}
private function stuckLeadsByStage(array $agentIds): array
{
$threshold = Carbon::now()->subDays(7);
return PipelineStage::orderBy('sort_order')->get()->map(fn (PipelineStage $stage) => [
'stage' => $stage->name,
'count' => Lead::whereIn('assigned_to', $agentIds)->where('pipeline_stage_id', $stage->id)->where('updated_at', '<=', $threshold)->whereNull('final_result')->count(),
])->values()->all();
}
private function qualitySummary(array $agentIds): array
{
$query = QualityReview::whereIn('agent_id', $agentIds);
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(),
];
}
private function agentsBehindTarget(array $agentIds): array
{
return collect($this->agentPerformance($agentIds))
->filter(fn ($row) => $row['call_target_percent'] < 80)
->values()
->all();
}
private function teamAlerts(array $agentIds): array
{
$now = Carbon::now();
return [
['label' => 'پیگیری عقب‌افتاده', 'count' => FollowUp::whereIn('user_id', $agentIds)->where('status', 'pending')->where('scheduled_at', '<', $now)->count()],
['label' => 'لید بدون تماس', 'count' => Lead::whereIn('assigned_to', $agentIds)->whereNull('last_call_at')->count()],
['label' => 'تماس نیازمند بازبینی', 'count' => Call::whereIn('user_id', $agentIds)->whereNotNull('recording_url')->whereDoesntHave('qualityReview')->count()],
];
}
private function voipStatus(): array
{
$provider = Setting::where('key', 'voip_provider')->value('value') ?: 'none';
$configured = match ($provider) {
'ami' => (bool) Setting::where('key', 'voip_ami_host')->value('value')
&& (bool) Setting::where('key', 'voip_ami_port')->value('value')
&& (bool) Setting::where('key', 'voip_ami_username')->value('value')
&& (bool) Setting::where('key', 'voip_ami_secret')->value('value'),
'api' => (bool) Setting::where('key', 'voip_api_base_url')->value('value')
&& (bool) Setting::where('key', 'voip_api_token')->value('value'),
'socket' => (bool) Setting::where('key', 'voip_socket_host')->value('value')
&& (bool) Setting::where('key', 'voip_socket_port')->value('value'),
'mock' => true,
default => false,
};
return [
'provider' => $provider,
'configured' => $configured,
'message' => $configured ? 'تنظیمات اصلی تلفن اینترنتی ثبت شده است.' : 'تلفن اینترنتی کامل پیکربندی نشده است.',
];
}
private function funnelConversion(): array
{
$total = max(Lead::count(), 1);
return collect($this->pipelineSummary())->map(fn ($row) => [
'stage' => $row['stage'],
'count' => $row['count'],
'percentage' => round(($row['count'] / $total) * 100, 1),
'color' => $row['color'],
])->all();
}
private function suggestedNextAction(int $userId): string
{
if (FollowUp::where('user_id', $userId)->where('status', 'pending')->where('scheduled_at', '<', Carbon::now())->exists()) {
return 'ابتدا پیگیری‌های عقب‌افتاده را انجام دهید.';
}
if (Lead::where('assigned_to', $userId)->whereNull('last_call_at')->exists()) {
return 'با لیدهای بدون تماس شروع کنید.';
}
return 'تماس بعدی پیشنهادی را از صف تماس بردارید.';
}
}