New_Micro_Learning/backend/app/Modules/Analytics/Http/CourseAnalyticsController.php

114 خطوط
6.4 KiB
PHP

<?php
namespace App\Modules\Analytics\Http;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Modules\Analytics\Application\CourseAnalyticsDashboard;
use App\Modules\Courses\Domain\Course;
use App\Modules\Courses\Domain\CourseVersion;
use App\Modules\Identity\Application\RolePermissions;
use App\Modules\Identity\Domain\Enums\Permission;
use Carbon\CarbonImmutable;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\StreamedResponse;
final class CourseAnalyticsController extends Controller
{
public function show(Request $request, string $course, RolePermissions $permissions, CourseAnalyticsDashboard $dashboard): JsonResponse
{
$user = $this->authorizeAnalytics($request, $permissions);
$filters = $this->filters($request);
$courseModel = Course::query()->where('organization_id', $user->organization_id)->findOrFail($course);
$version = $this->version($courseModel->getKey(), $user->organization_id, $filters['version'] ?? null);
$team = $this->team($user->organization_id, $filters['team'] ?? null);
[$from, $to] = $this->range($filters);
$data = $dashboard->build($version, $from, $to, $team, $filters['sort'] ?? 'position', $filters['direction'] ?? 'asc', (int) ($filters['page'] ?? 1), (int) ($filters['pageSize'] ?? 10));
$data['course'] = ['id' => $courseModel->getKey(), 'title' => $courseModel->title, 'versionId' => $version->getKey(), 'version' => $version->version_number, 'publishedAt' => $version->published_at?->toISOString()];
$data['filters'] = [
'versions' => CourseVersion::query()->where('organization_id', $user->organization_id)->where('course_id', $courseModel->getKey())->orderByDesc('version_number')->get(['id', 'version_number', 'published_at'])->map(fn (CourseVersion $item) => ['id' => $item->getKey(), 'version' => $item->version_number, 'publishedAt' => $item->published_at?->toISOString()]),
'teams' => DB::table('teams')->where('organization_id', $user->organization_id)->orderBy('name')->get(['id', 'name']),
];
return response()->json(['data' => $data]);
}
public function report(Request $request, string $course, RolePermissions $permissions, CourseAnalyticsDashboard $dashboard): StreamedResponse
{
$user = $this->authorizeAnalytics($request, $permissions);
$filters = $this->filters($request);
$courseModel = Course::query()->where('organization_id', $user->organization_id)->findOrFail($course);
$version = $this->version($courseModel->getKey(), $user->organization_id, $filters['version'] ?? null);
$team = $this->team($user->organization_id, $filters['team'] ?? null);
[$from, $to] = $this->range($filters);
$data = $dashboard->build($version, $from, $to, $team, $filters['sort'] ?? 'position', $filters['direction'] ?? 'asc', 1, 5000);
$filename = (preg_replace('/[^\pL\pN_-]+/u', '-', $courseModel->title) ?: 'course').'-تحلیل-نسخه-'.$version->version_number.'.csv';
return response()->streamDownload(function () use ($data): void {
$stream = fopen('php://output', 'wb');
if (! $stream) {
return;
}
fwrite($stream, "\xEF\xBB\xBF");
fputcsv($stream, ['شاخص', 'مقدار', 'واحد', 'تعریف']);
$labels = ['learners' => 'یادگیرندگان', 'starts' => 'شروع دوره', 'completions' => 'تکمیل', 'averageProgress' => 'میانگین پیشرفت', 'averageScore' => 'میانگین امتیاز', 'dropOff' => 'نرخ ریزش'];
foreach ($data['metrics'] as $key => $metric) {
fputcsv($stream, [$labels[$key] ?? $key, $metric['value'] ?? '—', $metric['unit'], $data['definitions'][$key]]);
}
fputcsv($stream, []);
fputcsv($stream, ['درس', 'ماژول', 'بازدید', 'میانگین زمان (دقیقه)', 'نرخ تکمیل', 'امتیاز ارزیابی', 'وضعیت']);
foreach ($data['content']['items'] as $lesson) {
fputcsv($stream, [$lesson['title'], $lesson['moduleTitle'], $lesson['views'], $lesson['averageTimeMinutes'] ?? '—', $lesson['completionRate'] ?? '—', $lesson['assessmentScore'] ?? '—', $lesson['status']]);
}
fclose($stream);
}, $filename, ['Content-Type' => 'text/csv; charset=UTF-8']);
}
private function authorizeAnalytics(Request $request, RolePermissions $permissions): User
{
$user = $request->user();
abort_unless($user instanceof User && $permissions->allows($user, Permission::OrganizationAnalyticsView), 403);
return $user;
}
/** @return array<string, mixed> */
private function filters(Request $request): array
{
return $request->validate([
'from' => ['nullable', 'date'], 'to' => ['nullable', 'date', 'after_or_equal:from'],
'version' => ['nullable', 'string'], 'team' => ['nullable', 'string'],
'sort' => ['nullable', 'in:position,title,views,time,completion,score,status'], 'direction' => ['nullable', 'in:asc,desc'],
'page' => ['nullable', 'integer', 'min:1'], 'pageSize' => ['nullable', 'integer', 'in:5,10,25,50'],
]);
}
/** @param array<string, mixed> $filters @return array{CarbonImmutable, CarbonImmutable} */
private function range(array $filters): array
{
$from = CarbonImmutable::parse($filters['from'] ?? now()->subDays(29)->toDateString())->startOfDay();
$to = CarbonImmutable::parse($filters['to'] ?? now()->toDateString())->endOfDay();
abort_if($from->diffInDays($to) > 366, 422, 'Date range cannot exceed 366 days.');
return [$from, $to];
}
private function version(string $courseId, string $organizationId, ?string $versionId): CourseVersion
{
$query = CourseVersion::query()->where('organization_id', $organizationId)->where('course_id', $courseId);
return $versionId ? $query->findOrFail($versionId) : $query->orderByDesc('version_number')->firstOrFail();
}
private function team(string $organizationId, ?string $teamId): ?string
{
if ($teamId) {
abort_unless(DB::table('teams')->where('organization_id', $organizationId)->where('id', $teamId)->exists(), 404);
}
return $teamId;
}
}