validate(['date_from' => 'nullable|date', 'date_to' => 'nullable|date|after_or_equal:date_from']); $leads = Lead::query(); AccessControl::scopeLeads($leads, $request->user()); $invoices = Invoice::query()->whereHas('lead', function ($query) use ($request) { AccessControl::scopeLeads($query, $request->user()); }); $tasks = Task::query(); AccessControl::scopeTasks($tasks, $request->user()); if (! empty($filters['date_from'])) { $leads->whereDate('leads.created_at', '>=', $filters['date_from']); $invoices->whereDate('invoices.created_at', '>=', $filters['date_from']); $tasks->whereDate('tasks.created_at', '>=', $filters['date_from']); } if (! empty($filters['date_to'])) { $leads->whereDate('leads.created_at', '<=', $filters['date_to']); $invoices->whereDate('invoices.created_at', '<=', $filters['date_to']); $tasks->whereDate('tasks.created_at', '<=', $filters['date_to']); } $pipeline = LeadStatus::query()->where('is_active', true)->orderBy('sort_order')->get() ->map(fn (LeadStatus $status) => [ 'stage' => $status->name, 'count' => (clone $leads)->where('lead_status_id', $status->id)->count(), ]); $sla = SlaBreach::query(); if ($request->user()->hasRole('agent')) { $sla->where('assigned_to', $request->user()->id); } elseif ($request->user()->hasRole('supervisor')) { $sla->whereIn('assigned_to', AccessControl::teamMemberIds($request->user())); } return response()->json([ 'summary' => [ 'open_leads' => (clone $leads)->whereNull('final_result')->count(), 'issued_invoices' => (clone $invoices)->where('status', 'issued')->count(), 'invoiced_total' => (float) (clone $invoices)->where('status', 'issued')->sum('total'), 'outstanding_total' => max( 0, (float) (clone $invoices)->where('status', 'issued')->sum('total') - (float) (clone $invoices)->where('status', 'issued')->sum('paid_amount'), ), 'sla_breaches' => (clone $sla)->where('status', 'breached')->count(), 'overdue_tasks' => (clone $tasks)->whereIn('status', ['open', 'in_progress'])->where('due_at', '<', now())->count(), ], 'pipeline' => $pipeline, 'sla_by_status' => (clone $sla)->selectRaw('status, COUNT(*) as count')->groupBy('status')->get(), 'tasks_by_status' => (clone $tasks)->selectRaw('status, COUNT(*) as count')->groupBy('status')->get(), ]); } public function kpi(Request $request): JsonResponse { Gate::authorize('view-report-data'); $validated = $this->validateScopedReport($request); $agentIds = $this->requestedAgentScope($validated['agent_id'] ?? null); if ($agentIds === false) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } return response()->json($this->reportService->kpiDashboard( $validated['date_from'] ?? null, $validated['date_to'] ?? null, $validated['agent_id'] ?? null, $agentIds )); } public function agentPerformance(Request $request): JsonResponse { Gate::authorize('view-report-data'); $validated = $request->validate([ 'agent_id' => 'nullable|exists:users,id', 'date_from' => 'nullable|date', 'date_to' => 'nullable|date', ]); $allowedAgentIds = $this->allowedAgentIds(); if (! empty($validated['agent_id']) && ! in_array((int) $validated['agent_id'], $allowedAgentIds, true)) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } if (empty($validated['agent_id'])) { $agents = User::role('agent')->whereIn('id', $allowedAgentIds)->orderBy('name')->get(); return response()->json($agents->map(fn (User $agent) => $this->reportService->agentPerformance( $agent->id, $validated['date_from'] ?? null, $validated['date_to'] ?? null ))->values()); } return response()->json( $this->reportService->agentPerformance( $validated['agent_id'], $validated['date_from'] ?? null, $validated['date_to'] ?? null ) ); } public function teamPerformance(Request $request): JsonResponse { Gate::authorize('view-report-data'); abort_if($request->user()->hasRole('agent'), 403, 'کارشناس فقط به گزارش عملکرد خود دسترسی دارد.'); $validated = $request->validate([ 'team_id' => 'nullable|exists:teams,id', 'date_from' => 'nullable|date', 'date_to' => 'nullable|date', ]); $allowedTeamIds = $this->allowedTeamIds(); if (! empty($validated['team_id']) && ! in_array((int) $validated['team_id'], $allowedTeamIds, true)) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } if (empty($validated['team_id'])) { $teams = Team::whereIn('id', $allowedTeamIds)->orderBy('name')->get(); return response()->json($teams->map(fn (Team $team) => $this->reportService->teamPerformance( $team->id, $validated['date_from'] ?? null, $validated['date_to'] ?? null ))->values()); } return response()->json( $this->reportService->teamPerformance( $validated['team_id'], $validated['date_from'] ?? null, $validated['date_to'] ?? null ) ); } public function campaignReport(Request $request, int $campaignId): JsonResponse { Gate::authorize('view-report-data'); abort_if($request->user()->hasRole('agent'), 403, 'کارشناس فقط به گزارش عملکرد خود دسترسی دارد.'); $validated = $request->validate([ 'date_from' => 'nullable|date', 'date_to' => 'nullable|date', ]); $campaign = Campaign::findOrFail($campaignId); Gate::authorize('view', $campaign); return response()->json($this->reportService->campaignReport( $campaignId, $validated['date_from'] ?? null, $validated['date_to'] ?? null )); } public function conversionReport(Request $request): JsonResponse { Gate::authorize('view-report-data'); $validated = $request->validate([ 'agent_id' => 'nullable|exists:users,id', 'date_from' => 'nullable|date', 'date_to' => 'nullable|date', ]); $agentIds = $this->requestedAgentScope($validated['agent_id'] ?? null); if ($agentIds === false) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } return response()->json( $this->reportService->conversionReport( $validated['date_from'] ?? null, $validated['date_to'] ?? null, $validated['agent_id'] ?? null, $agentIds ) ); } public function callReport(Request $request): JsonResponse { Gate::authorize('view-report-data'); $validated = $request->validate([ 'agent_id' => 'nullable|exists:users,id', 'date_from' => 'nullable|date', 'date_to' => 'nullable|date', ]); $agentIds = $this->requestedAgentScope($validated['agent_id'] ?? null); if ($agentIds === false) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } return response()->json( $this->reportService->callReport( $validated['date_from'] ?? null, $validated['date_to'] ?? null, $validated['agent_id'] ?? null, $agentIds ) ); } public function followUpReport(Request $request): JsonResponse { Gate::authorize('view-report-data'); $validated = $request->validate([ 'agent_id' => 'nullable|exists:users,id', 'date_from' => 'nullable|date', 'date_to' => 'nullable|date', ]); $agentIds = $this->requestedAgentScope($validated['agent_id'] ?? null); if ($agentIds === false) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } return response()->json( $this->reportService->followUpReport( $validated['date_from'] ?? null, $validated['date_to'] ?? null, $validated['agent_id'] ?? null, $agentIds ) ); } public function lostReasonReport(Request $request): JsonResponse { Gate::authorize('view-report-data'); $validated = $this->validateScopedReport($request); $agentIds = $this->requestedAgentScope($validated['agent_id'] ?? null); if ($agentIds === false) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } return response()->json($this->reportService->lostReasonReport($validated['date_from'] ?? null, $validated['date_to'] ?? null, $validated['agent_id'] ?? null, $agentIds)); } public function sourcePerformance(Request $request): JsonResponse { Gate::authorize('view-report-data'); $validated = $this->validateScopedReport($request); $agentIds = $this->requestedAgentScope($validated['agent_id'] ?? null); if ($agentIds === false) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } return response()->json($this->reportService->sourcePerformanceReport($validated['date_from'] ?? null, $validated['date_to'] ?? null, $validated['agent_id'] ?? null, $agentIds)); } public function duplicateLeads(Request $request): JsonResponse { Gate::authorize('view-report-data'); $validated = $this->validateScopedReport($request); $agentIds = $this->requestedAgentScope($validated['agent_id'] ?? null); if ($agentIds === false) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } return response()->json($this->reportService->duplicateLeadsReport($validated['date_from'] ?? null, $validated['date_to'] ?? null, $validated['agent_id'] ?? null, $agentIds)); } public function importQuality(Request $request): JsonResponse { Gate::authorize('view-report-data'); abort_if($request->user()->hasRole('agent'), 403, 'کارشناس فقط به گزارش عملکرد خود دسترسی دارد.'); $validated = $request->validate(['date_from' => 'nullable|date', 'date_to' => 'nullable|date']); return response()->json($this->reportService->importQualityReport($validated['date_from'] ?? null, $validated['date_to'] ?? null)); } public function callQuality(Request $request): JsonResponse { Gate::authorize('view-report-data'); $validated = $this->validateScopedReport($request); $agentIds = $this->requestedAgentScope($validated['agent_id'] ?? null); if ($agentIds === false) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } return response()->json($this->reportService->callQualityReport($validated['date_from'] ?? null, $validated['date_to'] ?? null, $validated['agent_id'] ?? null, $agentIds)); } public function bestContactTime(Request $request): JsonResponse { Gate::authorize('view-report-data'); $validated = $this->validateScopedReport($request); $agentIds = $this->requestedAgentScope($validated['agent_id'] ?? null); if ($agentIds === false) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } return response()->json($this->reportService->bestContactTimeReport($validated['date_from'] ?? null, $validated['date_to'] ?? null, $validated['agent_id'] ?? null, $agentIds)); } public function exportExcel(Request $request): StreamedResponse|JsonResponse { Gate::authorize('export-report-data'); $type = $request->type ?? 'agent'; $agentExportTypes = ['kpi', 'agent', 'conversion', 'call', 'follow_up', 'lost_reason', 'source', 'duplicate', 'call_quality', 'best_contact_time']; if ($request->user()->hasRole('agent') && ! in_array($type, $agentExportTypes, true)) { return response()->json(['message' => 'کارشناس فقط می‌تواند گزارش عملکرد خود را خروجی بگیرد.'], 403); } $agentIds = $this->requestedAgentScope($request->integer('agent_id') ?: null); if ($agentIds === false) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } if ($request->filled('team_id') && ! in_array($request->integer('team_id'), $this->allowedTeamIds(), true)) { return response()->json(['message' => 'دسترسی غیرمجاز'], 403); } ActivityLogger::log('report_exported', "Report export requested: {$type}"); $data = match ($type) { 'kpi' => $this->reportService->kpiDashboard($request->date_from, $request->date_to, $request->integer('agent_id') ?: null, $agentIds), 'agent' => $this->reportService->agentPerformance($request->integer('agent_id') ?: auth()->id(), $request->date_from, $request->date_to), 'team' => $this->reportService->teamPerformance($request->integer('team_id') ?: ($this->allowedTeamIds()[0] ?? 0), $request->date_from, $request->date_to), 'campaign' => $request->integer('campaign_id') ? $this->reportService->campaignReport($request->integer('campaign_id'), $request->date_from, $request->date_to) : [], 'conversion' => $this->reportService->conversionReport($request->date_from, $request->date_to, null, $agentIds), 'call' => $this->reportService->callReport($request->date_from, $request->date_to, null, $agentIds), 'follow_up' => $this->reportService->followUpReport($request->date_from, $request->date_to, null, $agentIds), 'lost_reason' => $this->reportService->lostReasonReport($request->date_from, $request->date_to, null, $agentIds), 'source' => $this->reportService->sourcePerformanceReport($request->date_from, $request->date_to, null, $agentIds), 'duplicate' => $this->reportService->duplicateLeadsReport($request->date_from, $request->date_to, null, $agentIds), 'import_quality' => $this->reportService->importQualityReport($request->date_from, $request->date_to), 'call_quality' => $this->reportService->callQualityReport($request->date_from, $request->date_to, null, $agentIds), 'best_contact_time' => $this->reportService->bestContactTimeReport($request->date_from, $request->date_to, null, $agentIds), default => [], }; $rows = $this->flattenForCsv($data); $filename = 'report-'.$type.'-'.now()->format('Ymd-His').'.csv'; return response()->streamDownload(function () use ($rows) { $out = fopen('php://output', 'w'); fwrite($out, "\xEF\xBB\xBF"); if ($rows === []) { fputcsv($out, ['message'], ','); fputcsv($out, ['داده‌ای برای خروجی وجود ندارد'], ','); fclose($out); return; } fputcsv($out, array_keys($rows[0]), ','); foreach ($rows as $row) { fputcsv($out, array_map(fn ($value) => is_scalar($value) || $value === null ? $value : json_encode($value, JSON_UNESCAPED_UNICODE), $row), ','); } fclose($out); }, $filename, ['Content-Type' => 'text/csv; charset=UTF-8']); } private function validateScopedReport(Request $request): array { return $request->validate([ 'agent_id' => 'nullable|exists:users,id', 'date_from' => 'nullable|date', 'date_to' => 'nullable|date', ]); } /** * @return list */ private function allowedAgentIds(): array { $user = auth()->user(); if ($user->hasRole('admin')) { return User::role('agent')->pluck('id')->all(); } if ($user->hasRole('supervisor')) { return AccessControl::teamMemberIds($user); } return [$user->id]; } /** * @return list */ private function allowedTeamIds(): array { $user = auth()->user(); if ($user->hasRole('admin')) { return Team::pluck('id')->all(); } if ($user->hasRole('supervisor')) { return AccessControl::teamIds($user); } return []; } /** * @return list|false|null */ private function requestedAgentScope(?int $agentId): array|false|null { $allowedAgentIds = $this->allowedAgentIds(); if ($agentId) { return in_array($agentId, $allowedAgentIds, true) ? [$agentId] : false; } return auth()->user()->hasRole('admin') ? null : $allowedAgentIds; } private function flattenForCsv(mixed $data): array { if ($data instanceof Collection) { $data = $data->toArray(); } if ($data instanceof Model) { $data = $data->toArray(); } if (is_array($data) && array_is_list($data)) { return array_map(fn ($row) => is_array($row) ? $row : ['value' => $row], $data); } if (is_array($data)) { foreach (['agents', 'by_stage', 'by_result', 'items', 'by_reason', 'sources', 'phone_duplicates', 'batches', 'by_agent', 'by_hour'] as $key) { if (isset($data[$key]) && is_iterable($data[$key])) { return collect($data[$key])->map(fn ($row) => $row instanceof Model ? $row->toArray() : (array) $row)->values()->all(); } } return [$data]; } return []; } }