load(['sections', 'courseLessons.section', 'courseLessons.lesson.category']); return Inertia::render('Admin/Academy/CourseBuilder', [ 'course' => $this->serializeCourse($academyCourse), 'sections' => $academyCourse->sections ->sortBy([['order_num', 'asc'], ['id', 'asc']]) ->values() ->map(fn (AcademyCourseSection $section): array => $this->serializeSection($section)) ->all(), 'courseLessons' => $academyCourse->courseLessons ->sortBy([['order_num', 'asc'], ['id', 'asc']]) ->values() ->map(fn (AcademyCourseLesson $courseLesson): array => $this->serializeCourseLesson($courseLesson)) ->all(), 'availableLessons' => AcademyLesson::query() ->with('category') ->orderBy('title') ->get() ->map(fn (AcademyLesson $lesson): array => [ 'id' => (int) $lesson->id, 'title' => (string) $lesson->title, 'slug' => (string) $lesson->slug, 'excerpt' => (string) ($lesson->excerpt ?? ''), 'difficulty' => (string) $lesson->difficulty, 'access_level' => (string) $lesson->access_level, 'active' => (bool) $lesson->active, 'published_at' => $lesson->published_at?->toISOString(), 'category' => $lesson->category ? (string) $lesson->category->name : '', 'attached' => $academyCourse->courseLessons->contains(fn (AcademyCourseLesson $courseLesson): bool => (int) $courseLesson->lesson_id === (int) $lesson->id), ]) ->values() ->all(), 'routes' => [ 'index' => route('admin.academy.courses.index'), 'edit' => route('admin.academy.courses.edit', ['academyCourse' => $academyCourse]), 'preview' => route('academy.courses.show', ['course' => $academyCourse->slug]), 'sectionStore' => route('admin.academy.courses.sections.store', ['academyCourse' => $academyCourse]), 'attachLesson' => route('admin.academy.courses.lessons.attach', ['academyCourse' => $academyCourse]), 'reorder' => route('admin.academy.courses.reorder', ['academyCourse' => $academyCourse]), ], ]); } public function storeSection(Request $request, AcademyCourse $academyCourse): RedirectResponse { $data = $request->validate([ 'title' => ['required', 'string', 'max:180'], 'slug' => ['nullable', 'string', 'max:180'], 'description' => ['nullable', 'string'], 'order_num' => ['nullable', 'integer', 'min:0'], 'is_visible' => ['nullable', 'boolean'], ]); $academyCourse->sections()->create([ 'title' => $data['title'], 'slug' => filled($data['slug'] ?? null) ? $data['slug'] : Str::slug($data['title']), 'description' => $data['description'] ?? null, 'order_num' => (int) ($data['order_num'] ?? ($academyCourse->sections()->max('order_num') + 1)), 'is_visible' => (bool) ($data['is_visible'] ?? true), ]); $this->cache->clearAll(); return back()->with('success', 'Course section created.'); } public function updateSection(Request $request, AcademyCourse $academyCourse, AcademyCourseSection $academyCourseSection): RedirectResponse { abort_unless((int) $academyCourseSection->course_id === (int) $academyCourse->id, 404); $data = $request->validate([ 'title' => ['required', 'string', 'max:180'], 'slug' => ['nullable', 'string', 'max:180'], 'description' => ['nullable', 'string'], 'order_num' => ['nullable', 'integer', 'min:0'], 'is_visible' => ['nullable', 'boolean'], ]); $academyCourseSection->forceFill([ 'title' => $data['title'], 'slug' => filled($data['slug'] ?? null) ? $data['slug'] : Str::slug($data['title']), 'description' => $data['description'] ?? null, 'order_num' => (int) ($data['order_num'] ?? 0), 'is_visible' => (bool) ($data['is_visible'] ?? true), ])->save(); $this->cache->clearAll(); return back()->with('success', 'Course section updated.'); } public function destroySection(AcademyCourse $academyCourse, AcademyCourseSection $academyCourseSection): RedirectResponse { abort_unless((int) $academyCourseSection->course_id === (int) $academyCourse->id, 404); $academyCourseSection->delete(); $this->cache->clearAll(); return back()->with('success', 'Course section deleted.'); } public function attachLesson(Request $request, AcademyCourse $academyCourse): RedirectResponse { $data = $request->validate([ 'lesson_id' => ['required', 'integer', 'exists:academy_lessons,id'], 'section_id' => ['nullable', 'integer', 'exists:academy_course_sections,id'], 'order_num' => ['nullable', 'integer', 'min:0'], 'is_required' => ['nullable', 'boolean'], 'access_override' => ['nullable', 'string', 'in:free,premium,creator,pro'], 'unlock_after_lesson_id' => ['nullable', 'integer', 'exists:academy_lessons,id'], ]); if (AcademyCourseLesson::query()->where('course_id', $academyCourse->id)->where('lesson_id', $data['lesson_id'])->exists()) { return back()->with('error', 'That lesson is already attached to this course.'); } $sectionId = $data['section_id'] ?? null; if ($sectionId !== null) { abort_unless(AcademyCourseSection::query()->where('course_id', $academyCourse->id)->whereKey($sectionId)->exists(), 404); } AcademyCourseLesson::query()->create([ 'course_id' => $academyCourse->id, 'section_id' => $sectionId, 'lesson_id' => (int) $data['lesson_id'], 'order_num' => (int) ($data['order_num'] ?? ($academyCourse->courseLessons()->max('order_num') + 1)), 'is_required' => (bool) ($data['is_required'] ?? true), 'access_override' => $data['access_override'] ?? null, 'unlock_after_lesson_id' => $data['unlock_after_lesson_id'] ?? null, ]); $this->courseLessonOrdering->syncCourse($academyCourse); $this->syncCourseCounts($academyCourse); return back()->with('success', 'Lesson attached to course.'); } public function updateCourseLesson(Request $request, AcademyCourse $academyCourse, AcademyCourseLesson $academyCourseLesson): RedirectResponse { abort_unless((int) $academyCourseLesson->course_id === (int) $academyCourse->id, 404); $data = $request->validate([ 'section_id' => ['nullable', 'integer', 'exists:academy_course_sections,id'], 'order_num' => ['nullable', 'integer', 'min:0'], 'is_required' => ['nullable', 'boolean'], 'access_override' => ['nullable', 'string', 'in:free,premium,creator,pro'], 'unlock_after_lesson_id' => ['nullable', 'integer', 'exists:academy_lessons,id'], ]); $sectionId = $data['section_id'] ?? null; if ($sectionId !== null) { abort_unless(AcademyCourseSection::query()->where('course_id', $academyCourse->id)->whereKey($sectionId)->exists(), 404); } $academyCourseLesson->forceFill([ 'section_id' => $sectionId, 'order_num' => (int) ($data['order_num'] ?? 0), 'is_required' => (bool) ($data['is_required'] ?? true), 'access_override' => $data['access_override'] ?? null, 'unlock_after_lesson_id' => $data['unlock_after_lesson_id'] ?? null, ])->save(); $this->courseLessonOrdering->syncCourse($academyCourse); $this->syncCourseCounts($academyCourse); return back()->with('success', 'Course lesson updated.'); } public function detachLesson(AcademyCourse $academyCourse, AcademyCourseLesson $academyCourseLesson): RedirectResponse { abort_unless((int) $academyCourseLesson->course_id === (int) $academyCourse->id, 404); $academyCourseLesson->delete(); $this->courseLessonOrdering->syncCourse($academyCourse); $this->syncCourseCounts($academyCourse); return back()->with('success', 'Lesson removed from course.'); } public function reorder(Request $request, AcademyCourse $academyCourse): RedirectResponse { $data = $request->validate([ 'sections' => ['nullable', 'array'], 'sections.*.id' => ['required', 'integer', 'exists:academy_course_sections,id'], 'sections.*.order_num' => ['required', 'integer', 'min:0'], 'lessons' => ['nullable', 'array'], 'lessons.*.id' => ['required', 'integer', 'exists:academy_course_lessons,id'], 'lessons.*.order_num' => ['required', 'integer', 'min:0'], 'lessons.*.section_id' => ['nullable', 'integer', 'exists:academy_course_sections,id'], ]); foreach ((array) ($data['sections'] ?? []) as $sectionData) { AcademyCourseSection::query() ->where('course_id', $academyCourse->id) ->whereKey((int) $sectionData['id']) ->update(['order_num' => (int) $sectionData['order_num']]); } foreach ((array) ($data['lessons'] ?? []) as $lessonData) { AcademyCourseLesson::query() ->where('course_id', $academyCourse->id) ->whereKey((int) $lessonData['id']) ->update([ 'order_num' => (int) $lessonData['order_num'], 'section_id' => $lessonData['section_id'] ?? null, ]); } $this->courseLessonOrdering->syncCourse($academyCourse); $this->syncCourseCounts($academyCourse); return back()->with('success', 'Course order updated.'); } private function syncCourseCounts(AcademyCourse $academyCourse): void { $academyCourse->forceFill([ 'lessons_count_cache' => $academyCourse->courseLessons()->count(), ])->save(); $this->cache->clearAll(); } private function serializeCourse(AcademyCourse $course): array { return [ 'id' => (int) $course->id, 'title' => (string) $course->title, 'slug' => (string) $course->slug, 'subtitle' => (string) ($course->subtitle ?? ''), 'excerpt' => (string) ($course->excerpt ?? ''), 'description' => (string) ($course->description ?? ''), 'access_level' => (string) $course->access_level, 'difficulty' => (string) $course->difficulty, 'status' => (string) $course->status, 'lessons_count_cache' => (int) ($course->lessons_count_cache ?? 0), 'cover_image' => (string) ($course->cover_image ?? ''), 'published_at' => $course->published_at?->toISOString(), ]; } private function serializeSection(AcademyCourseSection $section): array { return [ 'id' => (int) $section->id, 'title' => (string) $section->title, 'slug' => (string) ($section->slug ?? ''), 'description' => (string) ($section->description ?? ''), 'order_num' => (int) ($section->order_num ?? 0), 'is_visible' => (bool) ($section->is_visible ?? true), 'updateUrl' => route('admin.academy.courses.sections.update', ['academyCourse' => $section->course_id, 'academyCourseSection' => $section]), 'destroyUrl' => route('admin.academy.courses.sections.destroy', ['academyCourse' => $section->course_id, 'academyCourseSection' => $section]), ]; } private function serializeCourseLesson(AcademyCourseLesson $courseLesson): array { $lesson = $courseLesson->lesson; return [ 'id' => (int) $courseLesson->id, 'section_id' => $courseLesson->section_id ? (int) $courseLesson->section_id : null, 'lesson_id' => (int) $courseLesson->lesson_id, 'title' => (string) ($lesson?->title ?? ''), 'slug' => (string) ($lesson?->slug ?? ''), 'excerpt' => (string) ($lesson?->excerpt ?? ''), 'difficulty' => (string) ($lesson?->difficulty ?? ''), 'access_level' => (string) ($lesson?->access_level ?? ''), 'category' => (string) ($lesson?->category?->name ?? ''), 'order_num' => (int) ($courseLesson->order_num ?? 0), 'is_required' => (bool) $courseLesson->is_required, 'access_override' => $courseLesson->access_override, 'unlock_after_lesson_id' => $courseLesson->unlock_after_lesson_id ? (int) $courseLesson->unlock_after_lesson_id : null, 'updateUrl' => route('admin.academy.courses.lessons.update', ['academyCourse' => $courseLesson->course_id, 'academyCourseLesson' => $courseLesson]), 'destroyUrl' => route('admin.academy.courses.lessons.destroy', ['academyCourse' => $courseLesson->course_id, 'academyCourseLesson' => $courseLesson]), ]; } }