authorize($request); $courseModel = $this->course($course); $filters = $request->validate([ 'search' => ['nullable', 'string', 'max:100'], 'status' => ['nullable', Rule::in(['draft', 'in_review', 'published'])], 'page' => ['nullable', 'integer', 'min:1'], 'perPage' => ['nullable', 'integer', 'min:1', 'max:50'], ]); $base = CourseVersion::query()->where('organization_id', $this->tenant->id())->where('course_id', $courseModel->getKey()); $summary = [ 'total' => (clone $base)->count(), 'published' => (clone $base)->where('status', 'published')->count(), 'drafts' => (clone $base)->whereIn('status', ['draft', 'in_review'])->count(), ]; $search = trim((string) ($filters['search'] ?? '')); $versions = $base ->when($search !== '', function ($query) use ($search) { $query->where(function ($nested) use ($search) { $nested->where('title', 'like', '%'.$search.'%')->orWhere('description', 'like', '%'.$search.'%'); if (ctype_digit($search)) { $nested->orWhere('version_number', (int) $search); } }); }) ->when($filters['status'] ?? null, fn ($query, string $status) => $query->where('status', $status)) ->with(['creator:id,name', 'publisher:id,name', 'modules.lessons.blocks', 'assessments.questions']) ->withCount(['modules', 'lessons', 'blocks', 'assessments']) ->orderByDesc('version_number') ->paginate($filters['perPage'] ?? 10); return response()->json(['data' => [ 'summary' => $summary, 'items' => $versions->getCollection()->map(fn (CourseVersion $version) => $this->payload($version))->values(), 'meta' => ['currentPage' => $versions->currentPage(), 'lastPage' => $versions->lastPage(), 'total' => $versions->total()], ]]); } public function compare(Request $request, string $course): JsonResponse { $this->authorize($request); $courseModel = $this->course($course); $data = $request->validate(['ids' => ['required', 'array', 'size:2'], 'ids.*' => ['required', 'string', 'distinct']]); $versions = CourseVersion::query()->where('organization_id', $this->tenant->id())->where('course_id', $courseModel->getKey())->whereIn('id', $data['ids'])->get()->keyBy(fn (CourseVersion $version) => (string) $version->getKey()); abort_unless($versions->count() === 2, 422, 'نسخه‌های انتخاب‌شده قابل مقایسه نیستند.'); $before = $versions->get($data['ids'][0]); $after = $versions->get($data['ids'][1]); return response()->json(['data' => $this->comparison->compare($before, $after)]); } private function payload(CourseVersion $version): array { $actor = $version->status->value === 'published' ? ($version->publisher ?? $version->creator) : $version->creator; return [ 'id' => $version->getKey(), 'number' => $version->version_number, 'status' => $version->status->value, 'title' => $version->title, 'description' => $version->description, 'sourceVersionId' => $version->source_version_id, 'createdAt' => $version->created_at?->toISOString(), 'updatedAt' => $version->updated_at?->toISOString(), 'publishedAt' => $version->published_at?->toISOString(), 'actor' => $actor ? ['id' => $actor->getKey(), 'name' => $actor->name] : null, 'moduleCount' => (int) $version->modules_count, 'lessonCount' => (int) $version->lessons_count, 'assessmentCount' => (int) $version->assessments_count, 'blockCount' => (int) $version->blocks_count, 'unpublishedChanges' => $this->comparison->unpublishedChangeCount($version), ]; } private function course(string $id): Course { return Course::query()->where('organization_id', $this->tenant->id())->findOrFail($id); } private function authorize(Request $request): void { abort_unless($this->permissions->allows($request->user(), Permission::CoursesAuthor), 403); } }