471 خطوط
19 KiB
PHP
471 خطوط
19 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Campaign;
|
|
use App\Models\Invoice;
|
|
use App\Models\Lead;
|
|
use App\Models\LeadStatus;
|
|
use App\Models\SlaBreach;
|
|
use App\Models\Task;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use App\Services\ActivityLogger;
|
|
use App\Services\ReportService;
|
|
use App\Support\AccessControl;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
class ReportController extends Controller
|
|
{
|
|
public function __construct(private ReportService $reportService) {}
|
|
|
|
public function operations(Request $request): JsonResponse
|
|
{
|
|
Gate::authorize('view-report-data');
|
|
$filters = $request->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<int>
|
|
*/
|
|
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<int>
|
|
*/
|
|
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<int>|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 [];
|
|
}
|
|
}
|