Files
SkinbaseNova/app/Http/Controllers/Settings/AcademyCourseBuilderController.php

308 lines
14 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Models\AcademyCourse;
use App\Models\AcademyCourseLesson;
use App\Models\AcademyCourseSection;
use App\Models\AcademyLesson;
use App\Services\Academy\AcademyCacheService;
use App\Services\Academy\AcademyCourseLessonOrderingService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Inertia\Response;
final class AcademyCourseBuilderController extends Controller
{
public function __construct(
private readonly AcademyCacheService $cache,
private readonly AcademyCourseLessonOrderingService $courseLessonOrdering,
)
{
}
public function edit(AcademyCourse $academyCourse): Response
{
$academyCourse->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]),
];
}
}