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 'تماس بعدی پیشنهادی را از صف تماس بردارید.'; } }