114 خطوط
6.4 KiB
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;
|
|
}
|
|
}
|